UNPKG

instantjob-recruiter-client

Version:

a set of tools for creating an instantjob recruiter react client

454 lines (414 loc) 13 kB
import React, {Component} from 'react' import classNames from 'classnames/bind' import {DragSource, DropTarget} from 'react-dnd' import Hover from 'components/utils/hover' import {getEmptyImage} from 'react-dnd-html5-backend' import auto_bind from '../../common/auto_bind' import moment from '../../common/moment' import { range, exist, for_all, make_memoized, set_from_array, empty_array, map_hash, filter_hash, array_from_set, create_hash, group_by_batch, property_getter, } from '../../common/utilities' import {make_periods, intersecting_periods_interval} from '../../common/calendar' import {tolerant_selector} from '../../selectors/base' const cx = classNames.bind(require('../../styles/time_pickers.scss')) const is_between = (a, b, c) => (a >= b && a < c) || (a >= c && a < b) export const day_id = (day) => day.format("YYYY-MM-DD") const get_events_periods = (events) => make_periods(events) const get_day_periods = make_memoized((day) => tolerant_selector( [get_events_periods], (periods) => { let {lower, upper} = intersecting_periods_interval(periods, moment(day), moment(day).endOf('day')) return periods.slice(lower, upper) } )) const get_event_hours = make_memoized((day) => tolerant_selector( [get_day_periods(day)], (periods) => { return set_from_array(range(24).filter((hour) => { let start = moment(day).add(hour, 'hours') let end = moment(start).endOf('hour') let {lower, upper} = intersecting_periods_interval(periods, start, end) return lower != upper })) } )) export const chunks_count_in_hour = 4 export const chunk_length_in_minutes = 60 / chunks_count_in_hour export const chunks_count_in_day = 24 * chunks_count_in_hour export const get_periods = (chunk_set) => { let periods = [], period_started, start = null let add_period = (end) => periods.push({start, end}) range(chunks_count_in_day).forEach((chunk) => { if (chunk_set[chunk]) { if (!period_started) { start = chunk period_started = true } } else { if (period_started) { add_period(chunk - 1) period_started = false } } }) if (period_started) { add_period(chunks_count_in_day - 1) } return periods } export const get_start_end = (day, start_id, end_id) => { let start_of_day = moment(day) let start = start_id, end = end_id if (start > end) { start = end end = start_id } return { start: moment(start_of_day).add(start * chunk_length_in_minutes, 'minutes'), end: moment(start_of_day).add((end + 1) * chunk_length_in_minutes, 'minutes'), } } export const get_days_chunks = (events) => { const days = {} events.forEach((event) => { let current = moment(event.start) while (current.isBefore(event.end)) { let id = day_id(current) if (!days[id]) { days[id] = {} } if (!event.full_days) { days[id][(60 * current.hour() + current.minutes()) / chunk_length_in_minutes] = true } current.add(chunk_length_in_minutes, 'minutes') } }) return days } class TimePickers extends Component { constructor(props) { super(props) auto_bind(this) } render_displayed_hour(hour) { return ( <div key={hour} className={cx('time-pickers__header-hour')}> {hour % 2 == 0 ? `${hour}h` : null} </div> ) } render_day({date, chunks}) { return ( <div key={date.format()} className={cx('time-pickers__day')}> <div className={cx('time-pickers__day-label')}> {this.props.day_label ? date.format(`D MMMM${moment().isSame(date, 'year') ? '' : ' YYYY'}`) : ""} </div> <TimePicker color={this.props.full_day_color && for_all(range(chunks_count_in_day), (chunk) => chunks[chunk]) ? this.props.full_day_color : this.props.drag_color} on_drag_and_drop={this.props.on_drag_and_drop} chunks={chunks} date={date} events={this.props.events} flip={this.props.flip} /> </div> ) } render() { return ( <div className={cx('time-pickers')}> <div className={cx('time-pickers__header')}> {range(25).map(this.render_displayed_hour)} </div> <div className={cx('time-pickers__days')}> {this.props.days.map(this.render_day)} <div key={'bottom-filler'} className={cx('time-pickers__days-bottom-filler')}></div> </div> </div> ) } } class TimePicker extends Component { constructor(props) { super(props) this.state = { is_dragging: false, start: 0, end: 0, } auto_bind(this) } shouldComponentUpdate(props, state) { return !props.date.isSame(this.props.date) || props.color !== this.props.color || exist(range(chunks_count_in_day), (chunk) => props.chunks[chunk] !== this.props.chunks[chunk]) || state !== this.state } dragging_paint({start, end}) { if (start < end) { return !this.count(start + 1) } else if (start > end) { return !this.count(start - 1) } else { return this.count(start + 1) == this.count(start - 1) ? this.count(start) == 0 : this.count(start) > 0 } } offset({start, end}) { let offset = 0 if (start <= end) { offset = chunks_count_in_hour - 1 } let count = this.count(start) if (!this.props.chunks[start * chunks_count_in_hour]) { count = chunks_count_in_hour - count } return (count + offset) % chunks_count_in_hour - offset } count(hour) { return range(hour * chunks_count_in_hour, (hour + 1) * chunks_count_in_hour) .filter((chunk) => this.props.chunks[chunk]) .length } paint(hour) { return for_all( this.get_chunks(hour), property_getter('active'), ) } get_chunks(hour) { return this.get_active_chunks().slice(hour * chunks_count_in_hour, (hour + 1) * chunks_count_in_hour) } drag_start(hour) { this.setState({start: hour, end: hour, is_dragging: true}) } drag_hover(hour) { this.setState({end: hour}) } drag_end() { this.setState({is_dragging: false}) this.props.flip(this.props.date, this.get_dragging_chunks(), this.dragging_paint(this.state)) } get_dragging_chunks() { let {start, end} = this.state let offset = this.offset(this.state) if (start > end) { start = this.state.end end = this.state.start } return range(start * chunks_count_in_hour + offset, (end + 1) * chunks_count_in_hour + offset) } get_active_chunks() { let active_chunks = range(chunks_count_in_day).map((chunk) => this.props.chunks[chunk]) let paint = this.dragging_paint(this.state) if (this.state.is_dragging) { this.get_dragging_chunks().forEach((chunk) => active_chunks[chunk] = paint) } return active_chunks.map((active, index) => ({ active, start: active && (index == 0 || !active_chunks[index - 1]), end: active && (index + 1 == chunks_count_in_day || !active_chunks[index + 1]), })) } on_hour_click(hour) { this.props.flip(this.props.date, range(hour * chunks_count_in_hour, (hour + 1) * chunks_count_in_hour), !this.paint(hour)) } get_selectors() { return this.get_active_chunks().map(({start, end}, chunk) => { let select, offset if (start) { const chunk_base = chunks_count_in_hour * Math.floor(chunk / chunks_count_in_hour) select = (index) => { this.props.flip(this.props.date, range(chunk_base + index, chunk_base + chunks_count_in_hour), true) this.props.flip(this.props.date, range(chunk_base, chunk_base + index), false) } offset = -10 } else if (end) { chunk = chunk + 1 const chunk_base = chunks_count_in_hour * Math.floor(chunk / chunks_count_in_hour) select = (index) => { this.props.flip(this.props.date, range(chunk_base + index, chunk_base + chunks_count_in_hour), false) this.props.flip(this.props.date, range(chunk_base, chunk_base + index), true) } offset = +10 } if (select) { return {position: chunk * 20 / chunks_count_in_hour + offset, chunk, select} } }).filter(Boolean) } render_selector(selector, index) { return <Selector key={index} {...selector} /> } render_hour(hour) { let events_colors = array_from_set(filter_hash( map_hash(this.props.events, get_event_hours(day_id(this.props.date))), (active_hours) => active_hours[hour], )) return ( <DragDropHour key={hour} hour={hour} drag_start={this.drag_start} drag_hover={this.drag_hover} drag_end={this.drag_end} onClick={this.on_hour_click} chunks={this.get_chunks(hour)} event_color={events_colors[0]} active_color={this.props.color} /> ) } render() { return ( <div className={cx('time-pickers__day-hours')}> <div className={cx('time-pickers__day-hours-slider')}> {range(24).map(this.render_hour)} </div> <div className={cx('time-pickers__day-hours-selectors')}> {this.get_selectors().map(this.render_selector)} </div> </div> ) } } class Hour extends Component { constructor(props) { super(props) auto_bind(this) } componentDidMount() { this.props.connectDragPreview(getEmptyImage(), {captureDraggingState: true}) } onClick() { this.props.onClick(this.props.hour) } render_chunk({active, start, end}, index) { const {active_color} = this.props return ( <div key={index} style={{backgroundColor: active ? active_color : null}} className={cx('time-pickers__hour-box-chunk')}> {start ? ( <div style={{backgroundColor: active_color}} className={cx('time-pickers__hour-box-chunk-start')}> <div className={cx('time-pickers__hour-box-chunk-handle')}></div> </div> ) : null} {end ? ( <div style={{backgroundColor: active_color}} className={cx('time-pickers__hour-box-chunk-end')}> <div className={cx('time-pickers__hour-box-chunk-handle')}></div> </div> ) : null} </div> ) } render() { let {chunks, connectDropTarget, connectDragSource, event_color} = this.props return connectDropTarget(connectDragSource( <div className={cx('time-pickers__hour')}> <div style={{backgroundColor: event_color}} onClick={this.onClick} className={cx('time-pickers__hour-box')} > {chunks.map(this.render_chunk)} </div> </div> )) } } const DragDropHour = DropTarget( "time-pickers__hour", { hover(props, monitor, component) { let period = monitor.getItem() if (period) { if (period.end != props.hour) { props.drag_hover(props.hour) period.end = props.hour } } }, }, (connect, monitor) => ({ connectDropTarget: connect.dropTarget(), }) )(DragSource( "time-pickers__hour", { beginDrag(props) { props.drag_start(props.hour) return {start: props.hour, end: props.hour} }, endDrag(props) { props.drag_end() } }, (connect, monitor) => ({ connectDragSource: connect.dragSource(), connectDragPreview: connect.dragPreview(), isDragging: monitor.isDragging(), }) )(Hour)) class Selector extends Component { constructor(props) { super(props) auto_bind(this) } render_chunk(index) { const {chunk, select} = this.props return ( <div key={index} className={cx( 'time-pickers__selector-chunks-chunk', {'time-pickers__selector-chunks-chunk_selected': index == chunk % chunks_count_in_hour} )} onClick={() => select(index)} > {this.get_chunk(chunks_count_in_hour * Math.floor(chunk / chunks_count_in_hour) + index)} </div> ) } get_chunk(chunk) { let minutes = chunk_length_in_minutes * (chunk % chunks_count_in_hour) return `${Math.floor(chunk / chunks_count_in_hour)}h${minutes < 10 ? 0 : ""}${minutes}` } render() { const {position, chunk} = this.props return ( <div style={{left: position}} className={cx('time-pickers__selector')}> <div className={cx('time-pickers__selector-container')}> <Hover> {(hover) => hover ? ( <div className={cx('time-pickers__selector-chunks')}> {range(chunks_count_in_hour).map(this.render_chunk)} </div> ) : ( <div className={cx('time-pickers__selector-chunk')}> {this.get_chunk(chunk)} </div> )} </Hover> </div> </div> ) } } TimePickers.defaultProps = { day_label: true, days: [], on_drag_and_drop: (date, start, end, paint) => {}, full_day_color: '', drag_color: '', events: {}, } export default TimePickers