Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi-track support #1

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
4 changes: 2 additions & 2 deletions components/LevelThermometer.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @flow

import * as d3 from 'd3'
import { pointsToLevels, categoryPointsFromMilestoneMap, categoryColorScale, categoryIds, maxLevel } from '../constants'
import { pointsToLevels, categoryPointsFromMilestoneMap, categoryColorScale, maxLevel } from '../constants'
import React from 'react'
import type { MilestoneMap } from '../constants'

Expand Down Expand Up @@ -71,7 +71,7 @@ class LevelThermometer extends React.Component<Props> {
+ "z";
}
render() {
let categoryPoints = categoryPointsFromMilestoneMap(this.props.milestoneByTrack)
let categoryPoints = categoryPointsFromMilestoneMap(this.props.milestoneByTrack, this.props.trackIds, this.props.tracks)
let lastCategoryIndex = 0
categoryPoints.forEach((categoryPoint, i) => {
if (categoryPoint.points) lastCategoryIndex = i
Expand Down
14 changes: 7 additions & 7 deletions components/NightingaleChart.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import React from 'react'
import * as d3 from 'd3'
import { trackIds, milestones, tracks, categoryColorScale } from '../constants'
import { milestones, categoryColorScale } from '../constants'
import type { TrackId, Milestone, MilestoneMap } from '../constants'

const width = 400
Expand Down Expand Up @@ -33,8 +33,8 @@ class NightingaleChart extends React.Component<Props> {
this.arcFn = d3.arc()
.innerRadius(milestone => this.radiusScale(milestone))
.outerRadius(milestone => this.radiusScale(milestone) + this.radiusScale.bandwidth())
.startAngle(- Math.PI / trackIds.length)
.endAngle(Math.PI / trackIds.length)
.startAngle(- Math.PI / this.props.trackIds.length)
.endAngle(Math.PI / this.props.trackIds.length)
.padAngle(Math.PI / 200)
.padRadius(.45 * width)
.cornerRadius(2)
Expand Down Expand Up @@ -64,10 +64,10 @@ class NightingaleChart extends React.Component<Props> {
`}</style>
<svg>
<g transform={`translate(${width/2},${width/2}) rotate(-33.75)`}>
{trackIds.map((trackId, i) => {
{this.props.trackIds.map((trackId, i) => {
const isCurrentTrack = trackId == this.props.focusedTrackId
return (
<g key={trackId} transform={`rotate(${i * 360 / trackIds.length})`}>
<g key={trackId} transform={`rotate(${i * 360 / this.props.trackIds.length})`}>
{arcMilestones.map((milestone) => {
const isCurrentMilestone = isCurrentTrack && milestone == currentMilestoneId
const isMet = this.props.milestoneByTrack[trackId] >= milestone || milestone == 0
Expand All @@ -77,14 +77,14 @@ class NightingaleChart extends React.Component<Props> {
className={'track-milestone ' + (isMet ? 'is-met ' : ' ') + (isCurrentMilestone ? 'track-milestone-current' : '')}
onClick={() => this.props.handleTrackMilestoneChangeFn(trackId, milestone)}
d={this.arcFn(milestone)}
style={{fill: isMet ? categoryColorScale(tracks[trackId].category) : undefined}} />
style={{fill: isMet ? categoryColorScale(this.props.tracks[trackId].category) : undefined}} />
)
})}
<circle
r="8"
cx="0"
cy="-50"
style={{fill: categoryColorScale(tracks[trackId].category)}}
style={{fill: categoryColorScale(this.props.tracks[trackId].category)}}
className={"track-milestone " + (isCurrentTrack && !currentMilestoneId ? "track-milestone-current" : "")}
onClick={() => this.props.handleTrackMilestoneChangeFn(trackId, 0)} />
</g>
Expand Down
7 changes: 4 additions & 3 deletions components/PointSummaries.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
// @flow

import { pointsToLevels, milestoneToPoints, trackIds, totalPointsFromMilestoneMap, maxLevel } from '../constants'
import { pointsToLevels, milestoneToPoints, totalPointsFromMilestoneMap, maxLevel } from '../constants'
import type { MilestoneMap } from '../constants'
import React from 'react'

type Props = {
milestoneByTrack: MilestoneMap
milestoneByTrack: MilestoneMap,
trackIds: Array
}

class PointSummaries extends React.Component<Props> {
render() {
const totalPoints = totalPointsFromMilestoneMap(this.props.milestoneByTrack)
const totalPoints = totalPointsFromMilestoneMap(this.props.milestoneByTrack, this.props.trackIds)

let currentLevel, nextLevel

Expand Down
139 changes: 64 additions & 75 deletions components/SnowflakeApp.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
// @flow

import React from 'react'
import TrackSelector from '../components/TrackSelector'
import NightingaleChart from '../components/NightingaleChart'
import KeyboardListener from '../components/KeyboardListener'
import Track from '../components/Track'
import Wordmark from '../components/Wordmark'
import LevelThermometer from '../components/LevelThermometer'
import { eligibleTitles, trackIds, milestones, milestoneToPoints } from '../constants'
import PointSummaries from '../components/PointSummaries'
import type { Milestone, MilestoneMap, TrackId } from '../constants'
import React from 'react'
import TitleSelector from '../components/TitleSelector'
import { getTrackIds, eligibleTitles, milestones } from '../constants';

type SnowflakeAppState = {
milestoneByTrack: MilestoneMap,
Expand All @@ -19,9 +18,9 @@ type SnowflakeAppState = {
focusedTrackId: TrackId,
}

const hashToState = (hash: String): ?SnowflakeAppState => {
const hashToState = (hash: String, trackIds): ?SnowflakeAppState => {
if (!hash) return null
const result = defaultState()
const result = defaultState(trackIds)
const hashValues = hash.split('#')[1].split(',')
if (!hashValues) return null
trackIds.forEach((trackId, i) => {
Expand All @@ -45,59 +44,38 @@ const coerceMilestone = (value: number): Milestone => {
}
}

const emptyState = (): SnowflakeAppState => {
const emptyState = (trackIds): SnowflakeAppState => {
return {
name: '',
title: '',
milestoneByTrack: {
'MOBILE': 0,
'WEB_CLIENT': 0,
'FOUNDATIONS': 0,
'SERVERS': 0,
'PROJECT_MANAGEMENT': 0,
'COMMUNICATION': 0,
'CRAFT': 0,
'INITIATIVE': 0,
'CAREER_DEVELOPMENT': 0,
'ORG_DESIGN': 0,
'WELLBEING': 0,
'ACCOMPLISHMENT': 0,
'MENTORSHIP': 0,
'EVANGELISM': 0,
'RECRUITING': 0,
'COMMUNITY': 0
},
focusedTrackId: 'MOBILE'
milestoneByTrack: trackIds.reduce((acc, trackId) => {
acc[trackId] = 0
return acc
}, {}),
focusedTrackId: trackIds[0]
}
}

const defaultState = (): SnowflakeAppState => {
const getRandomIntInclusive = (min, max) => {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}

const defaultState = (trackIds): SnowflakeAppState => {
return {
name: 'Soapbox Simon',
title: 'Senior Engineer',
milestoneByTrack: {
'MOBILE': 1,
'WEB_CLIENT': 2,
'FOUNDATIONS': 3,
'SERVERS': 1,
'PROJECT_MANAGEMENT': 3,
'COMMUNICATION': 1,
'CRAFT': 1,
'INITIATIVE': 1,
'CAREER_DEVELOPMENT': 3,
'ORG_DESIGN': 2,
'WELLBEING': 0,
'ACCOMPLISHMENT': 2,
'MENTORSHIP': 2,
'EVANGELISM': 2,
'RECRUITING': 3,
'COMMUNITY': 0
},
focusedTrackId: 'MOBILE'
milestoneByTrack: trackIds.reduce((acc, trackId) => {
acc[trackId] = getRandomIntInclusive(0, 3)

return acc
}, {}),
focusedTrackId: trackIds[0]
}
}

const stateToHash = (state: SnowflakeAppState) => {
const stateToHash = (state: SnowflakeAppState, trackIds) => {
if (!state || !state.milestoneByTrack) return null
const values = trackIds.map(trackId => state.milestoneByTrack[trackId]).concat(encodeURI(state.name), encodeURI(state.title))
return values.join(',')
Expand All @@ -108,20 +86,22 @@ type Props = {}
class SnowflakeApp extends React.Component<Props, SnowflakeAppState> {
constructor(props: Props) {
super(props)
this.state = emptyState()
this.state = {...props.constants};
this.state.trackIds = getTrackIds(this.state.tracks);
this.state.user = emptyState(this.state.trackIds);
}

componentDidUpdate() {
const hash = stateToHash(this.state)
const hash = stateToHash(this.state.user, this.state.trackIds)
if (hash) window.location.replace(`#${hash}`)
}

componentDidMount() {
const state = hashToState(window.location.hash)
const state = hashToState(window.location.hash, this.state.trackIds)
if (state) {
this.setState(state)
} else {
this.setState(defaultState())
this.setState({ ...this.state, user: defaultState(this.state.trackIds) })
}
}

Expand Down Expand Up @@ -168,37 +148,45 @@ class SnowflakeApp extends React.Component<Props, SnowflakeAppState> {
<input
type="text"
className="name-input"
value={this.state.name}
onChange={e => this.setState({name: e.target.value})}
value={this.state.user.name}
onChange={e => this.setState({ user: { ...this.state.user, name: e.target.value} })}
placeholder="Name"
/>
<TitleSelector
milestoneByTrack={this.state.milestoneByTrack}
currentTitle={this.state.title}
milestoneByTrack={this.state.user.milestoneByTrack}
currentTitle={this.state.user.title}
titles={this.state.titles}
trackIds={this.state.trackIds}
setTitleFn={(title) => this.setTitle(title)} />
</form>
<PointSummaries milestoneByTrack={this.state.milestoneByTrack} />
<LevelThermometer milestoneByTrack={this.state.milestoneByTrack} />
<PointSummaries milestoneByTrack={this.state.user.milestoneByTrack} trackIds={this.state.trackIds} />
<LevelThermometer milestoneByTrack={this.state.user.milestoneByTrack} trackIds={this.state.trackIds} tracks={this.state.tracks} />
</div>
<div style={{flex: 0}}>
<NightingaleChart
milestoneByTrack={this.state.milestoneByTrack}
focusedTrackId={this.state.focusedTrackId}
milestoneByTrack={this.state.user.milestoneByTrack}
focusedTrackId={this.state.user.focusedTrackId}
trackIds={this.state.trackIds}
tracks={this.state.tracks}
handleTrackMilestoneChangeFn={(track, milestone) => this.handleTrackMilestoneChange(track, milestone)} />
</div>
</div>
<TrackSelector
milestoneByTrack={this.state.milestoneByTrack}
focusedTrackId={this.state.focusedTrackId}
milestoneByTrack={this.state.user.milestoneByTrack}
focusedTrackId={this.state.user.focusedTrackId}
trackIds={this.state.trackIds}
tracks={this.state.tracks}
setFocusedTrackIdFn={this.setFocusedTrackId.bind(this)} />
<KeyboardListener
selectNextTrackFn={this.shiftFocusedTrack.bind(this, 1)}
selectPrevTrackFn={this.shiftFocusedTrack.bind(this, -1)}
increaseFocusedMilestoneFn={this.shiftFocusedTrackMilestoneByDelta.bind(this, 1)}
decreaseFocusedMilestoneFn={this.shiftFocusedTrackMilestoneByDelta.bind(this, -1)} />
<Track
milestoneByTrack={this.state.milestoneByTrack}
trackId={this.state.focusedTrackId}
tracks={this.state.tracks}
milestoneByTrack={this.state.user.milestoneByTrack}
trackId={this.state.user.focusedTrackId}
milestones={this.state.milestones}
handleTrackMilestoneChangeFn={(track, milestone) => this.handleTrackMilestoneChange(track, milestone)} />
<div style={{display: 'flex', paddingBottom: '20px'}}>
<div style={{flex: 5}}>
Expand All @@ -207,48 +195,49 @@ class SnowflakeApp extends React.Component<Props, SnowflakeAppState> {
👩‍🔬 Learn about our Soapbox <a href="https://docs.google.com/document/d/1aGnE9t48aOCwrr_u0U80ArkcVhA9ANuUw0g_ClWzs8c/" target="_blank">growth framework</a>.
</div>
<div style={{flex: 1}}>
<a href="#" onClick={() => this.setState(emptyState())}>Reset</a>
<a href="#" onClick={() => this.setState({ ...this.state, user: emptyState(this.state.trackIds) })}>Reset</a>
</div>
</div>
</main>
)
}

handleTrackMilestoneChange(trackId: TrackId, milestone: Milestone) {
const milestoneByTrack = this.state.milestoneByTrack
const milestoneByTrack = this.state.user.milestoneByTrack
milestoneByTrack[trackId] = milestone

const titles = eligibleTitles(milestoneByTrack)
const titles = eligibleTitles(milestoneByTrack, this.state.titles, this.state.trackIds)
const title = titles.indexOf(this.state.title) === -1 ? titles[0] : this.state.title

this.setState({ milestoneByTrack, focusedTrackId: trackId, title })
this.setState({ user: { ...this.state.user, milestoneByTrack, focusedTrackId: trackId, title } })
}

shiftFocusedTrack(delta: number) {
let index = trackIds.indexOf(this.state.focusedTrackId)
const trackIds = this.state.trackIds
let index = trackIds.indexOf(this.state.user.focusedTrackId)
index = (index + delta + trackIds.length) % trackIds.length
const focusedTrackId = trackIds[index]
this.setState({ focusedTrackId })
const focusedTrackId = this.state.trackIds[index]
this.setState({ user: { ...this.state.user, focusedTrackId } })
}

setFocusedTrackId(trackId: TrackId) {
let index = trackIds.indexOf(trackId)
const focusedTrackId = trackIds[index]
this.setState({ focusedTrackId })
let index = this.state.trackIds.indexOf(trackId)
const focusedTrackId = this.state.trackIds[index]
this.setState({ user: { ...this.state.user, focusedTrackId } })
}

shiftFocusedTrackMilestoneByDelta(delta: number) {
let prevMilestone = this.state.milestoneByTrack[this.state.focusedTrackId]
let prevMilestone = this.state.user.milestoneByTrack[this.state.focusedTrackId]
let milestone = prevMilestone + delta
if (milestone < 0) milestone = 0
if (milestone > 5) milestone = 5
this.handleTrackMilestoneChange(this.state.focusedTrackId, milestone)
}

setTitle(title: string) {
let titles = eligibleTitles(this.state.milestoneByTrack)
let titles = eligibleTitles(this.state.user.milestoneByTrack, this.state.trackIds)
title = titles.indexOf(title) == -1 ? titles[0] : title
this.setState({ title })
this.setState({ user: { ...this.state.user, title } })
}
}

Expand Down
5 changes: 3 additions & 2 deletions components/TitleSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import type { MilestoneMap } from '../constants'
type Props = {
milestoneByTrack: MilestoneMap,
currentTitle: String,
setTitleFn: (string) => void
setTitleFn: (string) => void,
trackIds: Array
}

class TitleSelector extends React.Component {
render() {
const titles = eligibleTitles(this.props.milestoneByTrack)
const titles = eligibleTitles(this.props.milestoneByTrack, this.props.titles, this.props.trackIds)
return <select value={this.props.currentTitle} onChange={e => this.props.setTitleFn(e.target.value)}>
<style jsx>{`
select {
Expand Down
8 changes: 4 additions & 4 deletions components/Track.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
// @flow

import { tracks, milestones, categoryColorScale } from '../constants'
import React from 'react'
import type { MilestoneMap, TrackId, Milestone } from '../constants'
import { categoryColorScale, milestones } from '../constants'


type Props = {
milestoneByTrack: MilestoneMap,
Expand All @@ -12,7 +11,8 @@ type Props = {

class Track extends React.Component<Props> {
render() {
const track = tracks[this.props.trackId]
const track = this.props.tracks[this.props.trackId]
const trackIds = this.props.trackIds
const currentMilestoneId = this.props.milestoneByTrack[this.props.trackId]
const currentMilestone = track.milestones[currentMilestoneId - 1]
return (
Expand Down
Loading