instantjob-recruiter-client
Version:
a set of tools for creating an instantjob recruiter react client
454 lines (414 loc) • 13 kB
JSX
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