instantjob-recruiter-client
Version:
a set of tools for creating an instantjob recruiter react client
446 lines (412 loc) • 13 kB
JSX
import React, {Component} from 'react'
import classNames from 'classnames/bind'
import InfiniteList from './infinite_list'
import {DragSource, DropTarget} from 'react-dnd'
import {getEmptyImage} from 'react-dnd-html5-backend'
import {MdSubdirectoryArrowRight} from 'react-icons/lib/md'
import {color} from '../common/constants'
import {range, capitalize_first_letter, array_contains, make_memoized} from '../common/utilities'
import moment from '../common/moment'
import auto_bind from '../common/auto_bind'
import {event_intersects} from '../common/calendar'
import {tolerant_equal, tolerant_selector} from '../selectors/base'
const cx = classNames.bind(require('../styles/input_calendar.scss'))
let get_start_end = (start_id, end_id) => {
let start_of_week = moment().startOf('week')
let start = start_id, end = end_id
if (start > end) {
start = end
end = start_id
}
return {
start: moment(start_of_week).add(start - 1, 'days'),
end: moment(start_of_week).add(end - 1, 'days').endOf('day'),
}
}
let get_events = (props, state) => {
if (state.is_dragging && props.drag_color) {
if (state.paint) {
return {...props.events, [props.drag_color]: [...(props.events[props.drag_color] || []), {
id: 'temp',
...get_start_end(state.start, state.end),
full_days: true
}]}
} else {
let other_events = {...props.events}
if (other_events[props.drag_color]) {
delete other_events[props.drag_color]
}
return {...other_events, [""]: [{
id: 'temp',
...get_start_end(state.start, state.end),
full_days: true,
}], [props.drag_color]: props.events[props.drag_color] || []}
}
} else {
return props.events
}
}
let make_get_week_events = make_memoized((week_number) => tolerant_selector(
[get_events],
(events) => {
let week = moment().startOf('week').add(week_number, 'weeks')
let hues = Object.keys(events)
return hues.reduce((other_events, hue) => ({
...other_events,
[hue]: events[hue].filter((e) => event_intersects(e, moment(week).subtract(1, 'day'), 9, 'days'))
}), {})
}
))
let make_get_week_days = make_memoized((week_number) => tolerant_selector(
[make_get_week_events(week_number)],
(events) => {
let week = moment().startOf('week').add(week_number, 'weeks')
let days = range(9).map((day) => {
let date = moment(week).add(day - 1, 'days')
let hued_event = Object.keys(events).reduce((hued_event, hue) => {
if (hued_event) {
return hued_event
} else {
return events[hue].reduce((hued_event, e) => {
if (hued_event) {
return hued_event
} else {
if (event_intersects(e, date, 1, 'day')) {
return {event: e, hue}
} else {
return null
}
}
}, null)
}
}, null)
return {date, ...hued_event}
})
const days_event_are_continuous = (i) => days[i].hue == days[i+1].hue
for (let i = 0; i < 7; i ++) {
let category
if (days[i+1].event) {
if (days_event_are_continuous(i)) {
if (days_event_are_continuous(i+1)) {
category = "middle"
} else {
category = "end"
}
} else {
if (days_event_are_continuous(i+1)) {
category = "start"
} else {
category = "one"
}
}
} else {
category = "none"
}
days[i+1].category = category
}
delete days[0]
delete days[8]
return days
}
))
class Calendar extends Component {
constructor(props) {
super(props)
auto_bind(this)
}
componentDidMount() {
this.update_month(moment())
}
scroll_to_week(week) {
this.weeks.scroll_to_week(week)
}
render() {
return (
<div className={cx('input-calendar')} style={{display: "flex", flexDirection: 'column', alignItems: 'stretch', flex: 1}}>
<div style={{flexShrink: 0, height: 30, display: "flex", flexDirection: "column", justifyContent: 'center', alignItems: "center"}}>
<div ref={(month_label) => this.month_label = month_label} style={{textAlign: 'center', color: color('primary', 'light'), fontWeight: 900}}>
</div>
</div>
<div style={{marginLeft: this.props.hide_navigation ? 0 : 50, flexShrink: 0, height: 30, display: "flex", flexDirection: "row", alignItems: "center", justifyContent: "space-around", borderBottomStyle: "solid", borderBottomWidth: 2, borderBottomColor: color('primary', 'light')}}>
{["L", "M", "M", "J", "V", "S", "D"].map((day, index) => <WeekDay day={day} key={index} />)}
</div>
<div style={{flex: 1, cursor: this.props.drag_color ? 'ew-resize' : 'initial', display: 'flex', alignItems: 'stretch'}}>
<Weeks
ref={(weeks) => this.weeks = weeks}
update_month={this.update_month}
events={this.props.events}
on_click={this.props.on_click}
drag_color={this.props.drag_color}
on_drag_and_drop={this.props.on_drag_and_drop}
on_navigate_to_week={this.props.on_navigate_to_week}
hide_navigation={this.props.hide_navigation}
/>
</div>
</div>
)
}
update_month(month) {
$(this.month_label).text(capitalize_first_letter(month.isSame(moment(), 'year') ? month.format("MMMM") : month.format("MMMM YYYY")))
}
}
class Weeks extends Component {
constructor(props) {
super(props)
this.state = {
is_dragging: false,
start: 0,
end: 0,
paint: true,
}
auto_bind(this)
this.current_month = moment().startOf('month')
}
is_week_loaded(week) {
return week < this.state.week_count - this.state.week_offset
}
on_week_click(date, event) {
this.props.on_click(date, event)
}
drag_start(day, paint) {
this.setState({start: day, end: day, is_dragging: true, paint})
}
drag_hover(day) {
this.setState({end: day})
}
drag_end(day) {
this.setState({is_dragging: false})
let {start, end} = get_start_end(this.state.start, this.state.end)
this.props.on_drag_and_drop(start, end, this.state.paint)
}
scroll_to_week(week) {
this.list.scroll_to_item(week)
}
handle_scroll(scrollTop) {
let month = moment().startOf('week').add(scrollTop / row_height, 'weeks')
if (!month.isSame(this.current_month, 'month')) {
this.current_month = month
this.props.update_month(month)
}
}
render_week(index) {
return (
<Week
key={index}
days={make_get_week_days(index)(this.props, this.state)}
on_click={this.on_week_click}
week={index}
drag_start={this.drag_start}
drag_hover={this.drag_hover}
drag_end={this.drag_end}
on_navigate_to_week={this.props.on_navigate_to_week}
hide_navigation={this.props.hide_navigation}
/>
)
}
render() {
return (
<InfiniteList
item_height={row_height}
handle_scroll={this.handle_scroll}
ref={(list) => this.list = list}
>
{this.render_week}
</InfiniteList>
)
}
}
class WeekDay extends Component {
render() {
return (
<span style={{color: color('primary', 'light'), fontWeight: 900}}>
{this.props.day}
</span>
)
}
}
class Week extends Component {
constructor(props) {
super(props)
auto_bind(this)
}
on_navigate_to_week() {
this.props.on_navigate_to_week(this.props.week)
}
render() {
return (
<div className={cx('input-calendar__week')} style={{height: 50, width: this.props.width, display: 'flex', flexDirection: 'row', alignItems: 'stretch', justifyContent: 'space-around'}}>
{this.props.hide_navigation ? null : (
<div onClick={this.on_navigate_to_week} className={cx('input-calendar__week-link')}>
<MdSubdirectoryArrowRight />
</div>
)}
{this.props.days.map((day, index) => (
<DragDropDay
drag_start={this.props.drag_start}
drag_hover={this.props.drag_hover}
drag_end={this.props.drag_end}
day={this.props.week * 7 + index}
event={day.event}
hue={day.hue}
category={day.category}
on_click={this.props.on_click}
date={day.date}
key={index}
/>
))}
</div>
)
}
}
class Day extends Component {
shouldComponentUpdate(props) {
return props.day !== this.props.day ||
props.event !== this.props.event ||
props.hue !== this.props.hue ||
props.category !== this.props.category ||
!props.date.isSame(this.props.date)
}
componentDidMount() {
this.props.connectDragPreview(getEmptyImage(), {captureDraggingState: true})
}
render() {
let hue
switch (this.props.category) {
case "none":
hue = color('primary')
break
default:
hue = this.props.hue ? 'white' : color('primary')
}
return this.props.connectDropTarget(this.props.connectDragSource(
<div onClick={() => this.props.on_click(this.props.date, this.props.event)} style={{flex: 1, position: 'relative', display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', backgroundColor: this.props.date.month() % 2 == 0 ? 'white' : color('black', 'pale')}}>
<Sticker category={this.props.category} hue={this.props.hue} />
<div style={{color: hue, zIndex: 1}}>
{this.props.date.format("D")}
</div>
{this.props.date.date() == 1 ? (
<div style={{color: this.props.hue ? this.props.hue : hue, zIndex: 1, fontSize: 10, position: 'absolute', bottom: 1, left: 0, right: 0, textAlign: 'center'}}>
{this.props.date.format("MMM")}
</div>
) : null}
</div>
))
}
}
const DragDropDay = DropTarget(
"generic-calendar__day",
{
hover(props, monitor, component) {
let period = monitor.getItem()
if (period) {
if (period.end != props.day) {
props.drag_hover(props.day)
period.end = props.day
}
}
},
},
(connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
})
)(DragSource(
"generic-calendar__day",
{
beginDrag(props) {
props.drag_start(props.day, !props.event)
return {start: props.day, end: props.day}
},
endDrag(props) {
props.drag_end()
}
},
(connect, monitor) => ({
connectDragSource: connect.dragSource(),
connectDragPreview: connect.dragPreview(),
isDragging: monitor.isDragging(),
})
)(Day))
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 'end':
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 'start':
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 row_height = 50
const diameter = 24
const styles = {
sticker_container: {
position: 'absolute',
left: 0,
right: 0,
top: (row_height - diameter)/2,
bottom: (row_height - diameter)/2,
display: 'flex',
alignItems: 'stretch',
flexDirection: 'row',
justifyContent: 'center',
},
middle_sticker: {
flex: 1,
},
filler: {
display: 'flex',
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
flexDirection: 'row',
alignItems: 'stretch',
},
half_sticker: {
flex: 1,
},
one_sticker: {
borderRadius: diameter/2,
height: diameter,
width: diameter,
},
}
Calendar.defaultProps = {
events: {},
on_click: () => {},
drag_color: '',
on_drag_and_drop: (start, end, paint) => {},
hide_navigation: false,
}
export default Calendar