thinkful-ui
Version:
Shared navigation and UI resources for Thinkful.
343 lines (292 loc) • 9.5 kB
JSX
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;