UNPKG

instantjob-recruiter-client

Version:

a set of tools for creating an instantjob recruiter react client

370 lines (346 loc) 12.9 kB
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