onejgordon/flow-dashboard

View on GitHub
src/js/components/Timeline.js

Summary

Maintainability
C
1 day
Test Coverage
var React = require('react');
var api = require('utils/api');
var UserActions = require('actions/UserActions');
import {Drawer, AppBar, IconButton, FlatButton, RaisedButton,
    List, ListItem, TextField, Dialog, DatePicker, Toggle, IconMenu,
    MenuItem, FontIcon} from 'material-ui';
import {clone} from 'lodash';
var ReactLifeTimeline = require('react-life-timeline');
import connectToStores from 'alt-utils/lib/connectToStores';
import {changeHandler} from 'utils/component-utils';
import { SwatchesPicker } from 'react-color';
var util = require('utils/util');

@connectToStores
@changeHandler
class Timeline extends React.Component {
    static defaultProps = {};
    constructor(props) {
        super(props);
        this.state = {
            events: [],
            event_list_open: false,
            editing_index: null,
            form: {},
            batch_dialog_open: false
        };
    }

    static getStores() {
        return [];
    }

    static getPropsFromStores() {
        return {};
    }

    componentDidMount() {
        this.fetch_events();
        util.set_title("Timeline");
    }

    save_birthday() {
        let {form} = this.state;
        if (form.birthday) UserActions.update({birthday: util.printDateObj(form.birthday)});
    }

    fetch_events() {
      api.get("/api/event", {}, (res) => {
        this.setState({events: res.events}, () => {
            if (this.refs.rlt) this.refs.rlt.got_events(res.events);
        });
      });
    }

    edit_event(e, i) {
        let form = clone(e);
        if (form.date_start) form.date_start = util.date_from_iso(form.date_start);
        if (form.date_end) form.date_end = util.date_from_iso(form.date_end);
        this.setState({editing_index: i, form: form});
    }

    color_change(color) {
        let {form} = this.state;
        form.color = color.hex;
        this.setState({form});
    }

    render_events() {
        let {events} = this.state;
        return events.map((e, i) => {
            let date_range = e.date_start;
            if (e.ongoing) date_range += ` (ongoing)`;
            else if (e.date_end && e.date_end != e.date_start) date_range += " - " + e.date_end;
            return <ListItem
                        key={i}
                        primaryText={e.title}
                        secondaryText={<span style={{color: e.color || "#CCC"}}>{date_range}</span>}
                        onClick={this.edit_event.bind(this, e, i)} />
        });
    }

    batch_toggle(open) {
        this.setState({batch_dialog_open: open})
    }

    render_edit_form() {
        let {editing_index, form} = this.state;
        if (editing_index != null) return (
            <div>
                <TextField floatingLabelText="Title" name="title" value={form.title||''} onChange={this.changeHandler.bind(this, 'form', 'title')} fullWidth />
                <TextField floatingLabelText="Details" name="details" value={form.details||''} onChange={this.changeHandler.bind(this, 'form', 'details')} fullWidth />
                <div className="row">
                    <div className="col-sm-6">
                        <label>Event Color (optional)</label>
                        <SwatchesPicker width="100%" height={200} display={true} color={form.color || ""} onChangeComplete={this.color_change.bind(this)} />
                    </div>
                    <div className="col-sm-6">
                        <DatePicker autoOk={true} floatingLabelText="Date Start" formatDate={util.printDateObj} value={form.date_start||null} onChange={this.changeHandlerNilVal.bind(this, 'form', 'date_start')} />
                        <DatePicker autoOk={true} floatingLabelText="Date End (optional)" formatDate={util.printDateObj} value={form.date_end||null} onChange={this.changeHandlerNilVal.bind(this, 'form', 'date_end')} />
                        <Toggle toggled={form.ongoing} onToggle={this.changeHandlerToggle.bind(this, 'form', 'ongoing')} label="Ongoing" labelPosition="right" />
                    </div>
                </div>
            </div>
            )
    }

    upload_events() {
        let {form} = this.state;
        let params = {events: form.events};
        api.post("/api/event/batch", params, (res) => {
            this.setState({form: {}}, () => {
                this.fetch_events()
            });
        });
    }

    save_event() {
        let params = clone(this.state.form);
        if (params.date_start) params.date_start = util.printDateObj(params.date_start);
        if (params.date_end) params.date_end = util.printDateObj(params.date_end);
        api.post("/api/event", params, (res) => {
            // Update events list
            let events = this.state.events;
            let {editing_index} = this.state;
            if (editing_index >= 0) events[editing_index] = res.event;
            else events.push(res.event);
            this.setState({editing_index: null, form: {}, events: events}, () => {
                this.refs.rlt.got_events(events);
            });
        });
    }

    delete_event() {
        let {editing_index, events} = this.state;
        let event = events[editing_index]
        let params = {
            id: event.id
        }
        api.post("/api/event/delete", params, () => {
            if (editing_index >= 0) events.splice(editing_index, 1);
            this.setState({events: events, editing_index: null}, () => {
                this.refs.rlt.got_events(events);
            })
        })
    }

    new_event() {
        this.setState({form: {}, editing_index: -1});
    }

    toggle_event_list(open) {
        this.setState({event_list_open: open})
    }

    render() {
        let {form, batch_dialog_open} = this.state;
        let {user} = this.props;
        if (!user) return <div></div>
        let DOB = this.props.user.birthday;
        let today = new Date();
        if (!DOB) return (
            <div>
                <div className="empty">
                    <p>Use the timeline as a birds eye view of important eras &amp; life events. <a href="https://waitbutwhy.com/2014/05/life-weeks.html" target="_blank">Read Tim Urban</a> on visuals like this.</p>

                    <p>To populate your timeline, first enter your birthday. This date picker UI is fairly counterintuitive: to change the year, click on it.</p>

                    <DatePicker autoOk={true}
                        floatingLabelText="Birthday"
                        formatDate={util.printDateObj}
                        maxDate={today}
                        value={form.birthday} onChange={this.changeHandlerNilVal.bind(this, 'form', 'birthday')}/>

                </div>
                <div className="text-center">
                    <RaisedButton primary={true} label="Save Birthday" onClick={this.save_birthday.bind(this)} disabled={form.birthday == null} />
                </div>
            </div>
        );
        let events = this.state.events;
        let dialog_actions = [
            <RaisedButton label="Save" onClick={this.save_event.bind(this)} primary={true} />,
            <FlatButton label="Delete" style={{color: 'red'}} onClick={this.delete_event.bind(this)} />,
            <FlatButton label="Cancel" onClick={this.setState.bind(this, {editing_index: null})} />
        ]
        return (
            <div>

                <Dialog title="Add / Edit Event"
                    open={this.state.editing_index != null}
                    actions={dialog_actions}
                    autoDetectWindowHeight={true} autoScrollBodyContent={true}
                    onRequestClose={this.setState.bind(this, {editing_index: null})} >
                    { this.render_edit_form() }
                </Dialog>


                <Dialog title="Upload Batch of Events"
                    open={batch_dialog_open}
                    actions={[<RaisedButton label="Batch Upload from JSON" onClick={this.upload_events.bind(this)} primary />]}
                    autoDetectWindowHeight={true} autoScrollBodyContent={true}
                    onRequestClose={this.batch_toggle.bind(this, false)} >
                    <label>Batch Upload from JSON array</label>
                    <p>Each element should be a JSON object that includes properties: <code>title</code> (str), <code>date_start</code> (str, YYYY-MM-DD), <code>date_end</code> (str, optional, YYYY-MM-DD), <code>details</code> (str, optional), <code>color</code> (str, e.g. #FF0000, optional).</p>
                    <TextField placeholder="Events (JSON array)" name="events" value={form.events || ""} onChange={this.changeHandler.bind(this, 'form', 'events')} multiLine={true} fullWidth />
                </Dialog>


                <Drawer docked={false} width={300} open={this.state.event_list_open} onRequestChange={this.setState.bind(this, {event_list_open: false})} openSecondary={true} >
                  <AppBar
                    title="Events"
                    zDepth={0}
                    iconElementRight={<IconButton iconClassName="material-icons">close</IconButton>}
                    onRightIconButtonTouchTap={this.toggle_event_list.bind(this, false)} />
                    <List>
                      { this.render_events() }
                    </List>
                </Drawer>

                <div className="pull-right">
                    <FlatButton label="Show Event List" onTouchTap={this.toggle_event_list.bind(this, true)} />
                    <RaisedButton label="New Event" onTouchTap={this.new_event.bind(this)} primary={true} />
                    <IconMenu className="pull-right" iconButtonElement={<IconButton iconClassName="material-icons">more_vert</IconButton>}>
                        <MenuItem key="batch" primaryText="Upload Batch" onClick={this.batch_toggle.bind(this, true)} leftIcon={<FontIcon className="material-icons">file_upload</FontIcon>} />
                    </IconMenu>
                </div>

                <h2>Timeline</h2>

                <h4>Life in Weeks</h4>

                <ReactLifeTimeline
                    ref="rlt"
                    events={events}
                    birthday={util.date_from_iso(DOB)} />

            </div>
        );
    }
}

export default Timeline;