UNPKG

thinkful-ui

Version:

Shared navigation and UI resources for Thinkful.

343 lines (292 loc) 9.5 kB
require('./availability_grid.less'); const React = require('react'); const classNames = require('classnames'); const moment = require('moment'); const chunk = require('lodash/array/chunk'); const difference = require('lodash/array/difference'); const fill = require('lodash/array/fill'); const log = require('debug')('ui:AvailabilityGrid'); const AvailabilityGridSlot = React.createClass({ propTypes: { dayIndex: React.PropTypes.number, mouseDown: React.PropTypes.number, selectionMode: React.PropTypes.string, data: React.PropTypes.object, onSelectionModeChanged: React.PropTypes.func, onSlotUnselected: React.PropTypes.func, onSlotSelected: React.PropTypes.func }, handleMouseDown() { const {data, dayIndex, onSelectionModeChanged, onSlotUnselected, onSlotSelected} = this.props; if (this.props.data.selected) { onSelectionModeChanged('unselecting', dayIndex, data.index); onSlotUnselected(dayIndex, data.index); } else { onSelectionModeChanged('selecting', dayIndex, data.index); onSlotSelected(dayIndex, data.index); } }, handleMouseUp() { this.props.onSelectionModeChanged('neutral'); }, handleMouseEnter() { const {data, mouseDown, dayIndex, selectionMode, onSlotSelected, onSlotUnselected} = this.props; if (mouseDown === 1) { if (selectionMode === 'selecting') { onSlotSelected(dayIndex, data.index); } else if (selectionMode === 'unselecting') { onSlotUnselected(dayIndex, data.index); } } }, render() { let classes = classNames( 'availability-grid-slot', {'selected': this.props.data.selected} ); return ( <div className={classes} onMouseEnter={this.handleMouseEnter} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} > </div> ); } }) const AvailabilityGridDay = React.createClass({ propTypes: { minSlot: React.PropTypes.number, maxSlot: React.PropTypes.number, data: React.PropTypes.object }, render() { let slotNodes = this.props.data.slots.map((slotData) => { const {data, ...other} = this.props; return ( <AvailabilityGridSlot data={slotData} dayIndex={data.index} {...other} /> ); }) return ( <div className="availability-grid-day"> {this.props.data.name} {slotNodes.slice(this.props.minSlot, this.props.maxSlot)} </div> ); } }) const AvailabilityGrid = React.createClass({ propTypes: { slotsHour: React.PropTypes.number, minHour: React.PropTypes.number, maxHour: React.PropTypes.number, onPost: React.PropTypes.func, disabled: React.PropTypes.bool }, getInitialState() { const days = ['Mon', 'Tues', 'Wed', 'Thurs', 'Fri', 'Sat', 'Sun']; const HOURS_DAY = 24; const MINUTES_HOUR = 60; const MINUTES_SLOT = MINUTES_HOUR / this.props.slotsHour; const SLOTS_DAY = HOURS_DAY * this.props.slotsHour; let daysData = []; let slotNames = []; let selectionMode = 'neutral'; // precalculate the slot names one time let formatString = 'h a'; if (this.props.slotsHour > 1) { formatString = 'h:mma'; } let currentSlot = moment().startOf('day'); for (let i = 0; i < SLOTS_DAY; i++) { slotNames.push(currentSlot.format(formatString)); currentSlot.add(MINUTES_SLOT, 'm'); } daysData = days.map((name, index) => { let slots = slotNames.map((name, index) => ({name, index, selected: false})); return {index, name, slots}; }); return { days: daysData, slotNames: slotNames, selectionMode: selectionMode, selectionStartDays: _.cloneDeep(daysData), selectionStartDay: 0, selectionStartSlot: 0, mouseDown: 0 }; }, onMouseDown(e) { this.setState({mouseDown: this.state.mouseDown + 1}); }, onMouseUp(e) { this.setState({mouseDown: this.state.mouseDown - 1}); }, componentWillReceiveProps(nextProps) { this._digestBitmap(nextProps.bitmap); }, componentDidMount() { // If the bitmap is present when the component is mounted, render it. if (this.props.bitmap && this.props.bitmap != '') { this._digestBitmap(this.props.bitmap); } }, /** * Translates bitmapstring to internal data structures and stores it on * the state. * * Bitmap for availability is a string of 1s and 0s, with 1 representing * an avalible time block and 0 representing an unavailible time block. * * Totally free: 11111111111111111111 * Always busy: 00000000000000000000 * * If there are 4 slots in every hour (15 minutes per time block), then * the availability bitmap of someone busy for the first 30m of each hour * would look like this: 00110011001100110011 * * This function splits the bitstring into the days variable we store on * state. * */ _digestBitmap(bitmap='') { const MAX_SLOTS_HOUR = 4; const HOURS_DAY = 24; const DAYS_SLOT = MAX_SLOTS_HOUR * HOURS_DAY; const BITS_SLOT = MAX_SLOTS_HOUR / this.props.slotsHour; const AVAILABLE_BITMAP = fill(Array(BITS_SLOT), '1') bitmap = bitmap.split(''); chunk(bitmap, DAYS_SLOT). map((day, dayIndex) => chunk(day, BITS_SLOT). map((slot, slotIndex) => { this.state.days[dayIndex].slots[slotIndex].selected = difference(slot, AVAILABLE_BITMAP).length === 0; }) ) ; this.setState({days: this.state.days}); }, getBitmap() { // translate state to bitmap and return return this.state.days.map((dayData) => { return dayData.slots.map((slot) => { let value = slot.selected ? '1' : '0' return new Array(4 / this.props.slotsHour + 1).join(value); }).join(''); }).join(''); }, getSlotsAvailable() { return _.sum(this.state.days.map(dayData => { return _.sum(dayData.slots.map(slot => { return slot.selected ? 1 : 0 })); })); }, handleMouseEnter() { if (this.state.mouseDown === 0) { this.handleSelectionModeChanged('neutral'); } }, handlePost() { this.props.onPost(this.getBitmap()); }, handleSelectionModeChanged(newMode, startDay, startSlot) { if (!this.props.disabled) { if (newMode == 'selecting' || newMode == 'unselecting') { let days = this.state.days; days[startDay].slots[startSlot].selected = (newMode === 'selecting') this.setState({ days: days, selectionStartDays: _.cloneDeep(days), selectionMode: newMode, selectionStartDay: startDay, selectionStartSlot: startSlot }); } else { this.setState({ selectionMode: newMode }); } } }, handleSlotSelected(dayIndex, slotIndex) { this.handleSlot(dayIndex, slotIndex, true); }, handleSlotUnselected(dayIndex, slotIndex) { this.handleSlot(dayIndex, slotIndex, false); }, handleSlot(dayIndex, slotIndex, value) { if (this.state.selectionMode === 'selecting' || this.state.selectionMode === 'unselecting') { let days = this.state.days; let startDay = Math.min(this.state.selectionStartDay, dayIndex) let endDay = Math.max(this.state.selectionStartDay, dayIndex) let startSlot = Math.min(this.state.selectionStartSlot, slotIndex) let endSlot = Math.max(this.state.selectionStartSlot, slotIndex) days.map((day, i) => { day.slots.map((slot, j) => { if (i >= startDay && i <= endDay && j >= startSlot && j <= endSlot) { days[i].slots[j].selected = value; } else { days[i].slots[j].selected = this.state.selectionStartDays[i].slots[j].selected; } }) }); this.setState({days: days}); } }, render() { let slotNames = this.state.slotNames.map((slotName) => { return ( <div className="availability-grid-slot-name"> {slotName} </div> ); }) let minSlot = this.props.minHour * this.props.slotsHour; let maxSlot = this.props.maxHour * this.props.slotsHour; let dayNodes = this.state.days.map((dayData) => { return ( <AvailabilityGridDay data={dayData} selectionMode={this.state.selectionMode} onSelectionModeChanged={this.handleSelectionModeChanged} onSlotSelected={this.handleSlotSelected} onSlotUnselected={this.handleSlotUnselected} minSlot={minSlot} maxSlot={maxSlot} mouseDown={this.state.mouseDown} /> ); }); let classes = classNames( 'availability-grid', {'availability-grid__disabled': this.props.disabled} ); return ( <div className={classes} onMouseEnter={this.handleMouseEnter} onMouseDown={this.onMouseDown} onMouseUp={this.onMouseUp} > <div className="availability-grid-slot-names"> {slotNames.slice(minSlot, maxSlot)} </div> <div className="availability-grid-days"> {dayNodes} </div> {this.props.onPost && ( <input className="button" type="submit" onClick={this.handlePost} value="Update Availability"/> )} </div> ); } }) module.exports = AvailabilityGrid;