instantjob-recruiter-client
Version:
a set of tools for creating an instantjob recruiter react client
370 lines (346 loc) • 12.9 kB
JSX
import React, {Component} from 'react'
import ReactList from 'react-list'
import {range} from '../common/utilities'
import {color} from '../common/constants'
import moment from '../common/moment'
import {event_intersects} from '../common/calendar'
const column_width = 40
const diameter = 24
const min_height = diameter * 24
const number_of_days = 365
let tolerant_equal = (previous, next) => {
if (previous === next) {
return true
} else {
return false
}
}
let props_selector = ((selectors, builder) => {
let previous_values = selectors.map(() => null)
let result
return (props) => {
let next_values = selectors.map((selector) => selector(props))
if (next_values.reduce((unchanged, value, index) => unchanged && tolerant_equal(value, previous_values[index]), true)) {
return result
} else {
return builder(...next_values)
}
}
})
let get_all_days_events = (props) => props.day_events
let get_all_hours_events = (props) => props.hour_events
let get_day = (day_number) => moment().startOf('day').add(day_number, 'days')
let make_get_day_events = (day_number) => props_selector(
[get_all_days_events, get_all_hours_events],
(all_days_events, all_hours_events) => {
let day = get_day(day_number)
let events = [all_days_events, all_hours_events].map((all_events) => {
return Object.keys(all_events).reduce((events, hue) => ({
...events,
[hue]: all_events[hue].filter((e) => event_intersects(e, day, 1, 'day'))
}), {})
})
return {day_events: events[0], hour_events: events[1]}
}
)
let make_get_day_hours = (day_number) => props_selector(
[make_get_day_events(day_number)],
(events) => {
return range(26).map((hour_id) => {
let hour = get_day(day_number).add(hour_id - 1, 'hours')
let find_corresponding_event = (events) => events.reduce((corresponding, e) => event_intersects(e, hour, 1, 'hour') ? e : corresponding, null)
return Object.keys(events.hour_events).reduce((result, hue) => {
let event = find_corresponding_event(events.hour_events[hue])
if (event) {
return {...result, event, hue}
} else {
return result
}
}, {hour})
}).map(({hour, event, hue}, index, hours) => {
if (index > 0 && index < 26) {
if (event) {
let start = hue == hours[index - 1].hue
let end = hue == hours[index + 1].hue
let category
if (start) {
if (end) {
category = 'middle'
} else {
category = 'start'
}
} else {
if (end) {
category = 'end'
} else {
category = 'one'
}
}
return {category, hue, event, hour}
} else {
return {category: 'none', hue: color('primary', 'light'), hour}
}
} else {
return null
}
}).slice(1, 25)
}
)
let make_get_day_event = (day_number) => props_selector(
[make_get_day_events(day_number)],
(events) => {
return Object.keys(events.day_events).reduce(
(result, hue) => events.day_events[hue].length > 0 ? {hue, day_event: events.day_events[hue][0]} : result,
Object.keys(events.hour_events).reduce(
(result, hue) => events.hour_events[hue].length > 0 ? {hue, day_event: events.hour_events[hue][0]} : result,
{hue: null, day_event: false}
)
)
}
)
class Week extends Component {
constructor(props) {
super(props)
this.state = {
width: 100,
height: min_height,
}
this.day_event_getters = range(number_of_days).map((day_number) => make_get_day_event(day_number))
this.day_hours_getters = range(number_of_days).map((day_number) => make_get_day_hours(day_number))
this.update_dimensions = this.update_dimensions.bind(this)
this.render_day_header = this.render_day_header.bind(this)
this.render_day = this.render_day.bind(this)
this.on_days_scroll = this.on_days_scroll.bind(this)
this.scroll_to_day = this.scroll_to_day.bind(this)
}
componentDidMount() {
this.update_dimensions()
$(window).resize(this.update_dimensions)
$(this.days_container).scroll(() => $(this.day_headers_container).scrollLeft($(this.days_container).scrollLeft()))
$(this.day_headers_container).scroll(() => $(this.days_container).scrollLeft($(this.day_headers_container).scrollLeft()))
}
componentWillUnmount() {
$(window).off("resize", this.update_dimensions)
}
scroll_to_day(day) {
$(this.days_container).scrollLeft(day.diff(get_day(0), 'days') * column_width)
}
render_day_header(day_number, key) {
let {hue, day_event} = this.day_event_getters[day_number](this.props)
let day = get_day(day_number)
return (
<DayHeader
key={key}
day={day}
hue={day_event ? hue : null}
on_click={() => this.props.on_day_click(day, day_event)}
/>
)
}
render_day(day_number, key) {
let {hue, day_event} = this.day_event_getters[day_number](this.props)
return (
<Day
key={key}
background_hue={day_event.full_days ? hue : null}
hours={this.day_hours_getters[day_number](this.props)}
on_click={day_event.full_days ? (() => {}) : this.props.on_hour_click}
height={(this.state.height > min_height ? this.state.height : min_height) - 84}
/>
)
}
update_dimensions() {
let width = $(this.container).width()
let height = $(this.container).height()
if (width != this.state.width || height != this.state.height) {
this.setState({width, height})
}
}
render() {
let start = moment().startOf('day')
return (
<div style={{flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'stretch'}} ref={(container) => this.container = container}>
<div style={{marginLeft: 40, height: 84 + this.props.scrollbar_size, flexShrink: 0, overflowX: 'scroll', width: this.state.width - 40}} ref={(day_headers_container) => this.day_headers_container = day_headers_container}>
<ReactList
ref={(day_headers) => this.day_headers = day_headers}
axis={'x'}
length={number_of_days}
itemRenderer={
(column, key) => this.render_day_header(column, key)
}
type='uniform'
/>
</div>
<div style={{height: this.state.height - 84, display: 'flex', flexDirection: 'row', overflowY: 'auto', width: this.state.width}}>
<div style={{width: 40, flexShrink: 0, display: 'flex', flexDirection: 'column', alignItems: 'stretch', height: (this.state.height > min_height ? this.state.height : min_height) - 84}}>
{range(13).map((hour) => <DisplayedHour hour={2 * hour} key={hour} />)}
</div>
<div style={{height: (this.state.height > min_height ? this.state.height : min_height) - 84 + this.props.scrollbar_size, width: this.state.width - 40, overflowX: 'scroll', overflowY: 'hidden'}} ref={(days_container) => this.days_container = days_container}>
<ReactList
axis={'x'}
length={number_of_days}
itemRenderer={
(column, key) => this.render_day(column, key)
}
type='uniform'
/>
</div>
</div>
</div>
)
}
on_days_scroll() {
this.day_headers.scrollTo($(this.days_container).scrollLeft() / column_width)
}
}
Week.defaultProps = {on_day_click: () => {}, on_hour_click: () => {}, day_events: {}, hour_events: {}}
class DayHeader extends Component {
render() {
return (
<div style={{width: column_width, display: 'flex', flexDirection: 'column', flexShrink: 0, alignItems: 'stretch', display: 'inline-block'}}>
<div style={{height: column_width, flexShrink: 0, display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
<div onClick={this.props.on_click} style={{width: diameter, height: diameter, borderRadius: diameter/2, backgroundColor: this.props.hue ? this.props.hue : 'white', color: this.props.hue ? 'white' : color('primary'), textAlign: 'center', verticalAlign: 'middle', display: 'flex', justifyContent: 'center', flexDirection: 'column'}}>
<span style={{fontSize: 12}}>{this.props.day.date()}</span>
</div>
</div>
<div style={{height: 14, flexShrink: 0, display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
{this.props.day.date() == 1 ? (
<span style={{color: color('primary'), fontSize: 10}}>
{this.props.day.format("MMM")}
</span>
) : null}
</div>
<div style={{height: 30, flexShrink: 0, display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
<span style={{color: color('primary', 'light'), fontWeight: 900}}>
{this.props.day.format("dd")[0]}
</span>
</div>
</div>
)
}
}
class Day extends Component {
render() {
return (
<div style={{width: column_width, display: 'inline-block', height: this.props.height}}>
<div style={{width: column_width, height: this.props.height, display: 'flex', flexDirection: 'column', alignItems: 'stretch', overflow: 'hidden'}}>
{this.props.hours.map(({hue: hour_hue, category, event, hour}, index) =>
<Hour
key={index}
hue={this.props.background_hue ? this.props.background_hue : hour_hue}
category={this.props.background_hue ? 'middle' : category}
on_click={() => this.props.on_click(hour, event)}
/>
)}
</div>
</div>
)
}
}
const DisplayedHour = (props) => {
switch (props.hour) {
case 0:
return (
<div style={{flex: 0.6, display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
</div>
)
case 24:
return (
<div style={{flex: 0.4, display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
</div>
)
default:
return (
<div style={{flex: 1, display: 'flex', justifyContent: 'center', alignItems: 'center', backgroundColor: props.hour % 2 ? color('important') : 'white'}}>
<span style={{textAlign: 'center'}}>
{props.hour}h
</span>
</div>
)
}
}
class Hour extends Component {
render() {
return (
<div style={{flex: 1, position: 'relative', display: 'flex', justifyContent: 'center', alignItems: 'center'}} onClick={this.props.on_click}>
<Sticker {...this.props} />
<div style={{backgroundColor: this.props.category == "none" ? color('primary', 'light') : 'white', width: 2, height: 2, borderRadius: 1, zIndex: 1}} />
</div>
)
}
}
class Sticker extends Component {
render() {
switch (this.props.category) {
case 'one':
return (
<div style={styles.sticker_container}>
<div style={{...styles.one_sticker, backgroundColor: this.props.hue}} />
</div>
)
case 'middle':
return (
<div style={styles.sticker_container}>
<div style={{...styles.middle_sticker, backgroundColor: this.props.hue}} />
</div>
)
case 'start':
return (
<div style={styles.sticker_container}>
<div style={{...styles.filler, justifyContent: 'flex-start'}}>
<div style={{...styles.half_sticker, backgroundColor: this.props.hue}} />
<div style={styles.half_sticker} />
</div>
<div style={{...styles.one_sticker, backgroundColor: this.props.hue}} />
</div>
)
case 'end':
return (
<div style={styles.sticker_container}>
<div style={{...styles.filler, justifyContent: 'flex-end'}}>
<div style={styles.half_sticker} />
<div style={{...styles.half_sticker, backgroundColor: this.props.hue}} />
</div>
<div style={{...styles.one_sticker, backgroundColor: this.props.hue}} />
</div>
)
default:
return <div />
}
}
}
const styles = {
sticker_container: {
position: 'absolute',
top: 0,
bottom: 0,
left: (column_width - diameter)/2,
right: (column_width - diameter)/2,
display: 'flex',
alignItems: 'stretch',
flexDirection: 'column',
justifyContent: 'center',
},
middle_sticker: {
flex: 1,
},
filler: {
display: 'flex',
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
flexDirection: 'column',
alignItems: 'stretch',
},
half_sticker: {
flex: 1,
},
one_sticker: {
borderRadius: diameter/2,
height: diameter,
width: diameter,
},
}
export default Week