src/js/components/MiniJournalWidget.js
var React = require('react');
import PropTypes from 'prop-types';
import {Link} from 'react-router';
var AppConstants = require('constants/AppConstants');
var MobileDialog = require('components/common/MobileDialog');
import { TextField, IconMenu,
FlatButton, RaisedButton, IconButton, FontIcon,
DropDownMenu,
MenuItem } from 'material-ui';
var util = require('utils/util');
import {changeHandler} from 'utils/component-utils';
var JournalEditor = require('components/JournalEditor');
var api = require('utils/api');
var toastr = require('toastr');
import {clone, merge, without} from 'lodash';
@changeHandler
export default class MiniJournalWidget extends React.Component {
static propTypes = {
include_location: PropTypes.bool,
tomorrow_top_tasks: PropTypes.bool,
questions: PropTypes.array,
window_start_hr: PropTypes.number,
window_end_hr: PropTypes.number,
journal_notification: PropTypes.bool
}
static defaultProps = {
questions: [],
include_location: true,
tomorrow_top_tasks: true,
journal_notification: false
}
constructor(props) {
super(props);
this.state = {
today_data: {},
form: this.initial_form_state(),
tasks: [""],
open: false,
historical: false,
historical_date: null,
historical_incomplete_dates: [],
position: null, // {lat, lon}
submitted_date: null // ISO date (str)
};
this.MAX_TASKS = 3;
this.NOTIFY_CHECK_MINS = 5;
this.notify_checker_id = null; // For interval
}
componentDidMount() {
this.maybe_check_if_not_submitted();
this.notify_checker_id = setInterval(() => {
this.notify_check();
}, this.NOTIFY_CHECK_MINS*60*1000);
}
componentWillUnmount() {
if (this.notify_checker_id) clearInterval(this.notify_checker_id);
}
initial_form_state() {
let form = {}
this.props.questions.forEach((q) => {
if (q.response_type == 'slider' || q.response_type == 'number') form[q.name] = 5;
});
return form;
}
journal_form_change(form_data) {
this.setState({form: form_data});
}
notify_check() {
let {submitted_date, open} = this.state
if (this.should_notify()) {
util.notify("Flow Reminder", "Submit your daily journal", "jrnl_remind");
this.open_journal_dialog();
} else if (submitted_date != util.iso_from_date(this.current_submission_date())) {
if (!open) this.setState({today_data: {}}) // Clear prior journal's form data (if dialog not open)
}
}
change_task(i, event) {
let {tasks} = this.state;
tasks[i] = event.target.value;
this.setState({tasks});
}
remove_task() {
let {tasks} = this.state;
tasks.splice(tasks.length - 1, 1);
this.setState({tasks});
}
add_task() {
let {tasks} = this.state;
tasks.push("");
this.setState({tasks});
}
in_journal_window() {
// End of day
let d = new Date();
let hrs = d.getHours();
return hrs >= this.props.window_start_hr || hrs <= this.props.window_end_hr;
}
should_notify() {
let {open} = this.state
let d = new Date();
let notification_enabled = this.props.journal_notification
return (notification_enabled && !open && !this.submitted() &&
d.getMinutes() <= this.NOTIFY_CHECK_MINS && this.in_journal_window())
}
maybe_check_if_not_submitted() {
let {open} = this.state
if (this.in_journal_window() && !open) {
this.check_if_not_submitted();
}
}
check_if_not_submitted() {
// If not yet submitted for day, show dialog
api.get("/api/journal/today", {}, (res) => {
let not_submitted = !res.journal || (res.journal && !res.journal.data);
let st = {submitted_date: not_submitted ? null : res.journal.iso_date}
if (res.journal != null) st.today_data = res.journal.data
this.setState(st, () => {
if (not_submitted) this.open_journal_dialog();
});
});
}
open_journal_dialog() {
let {include_location} = this.props;
let {today_data} = this.state
if (include_location) {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(this.got_location.bind(this), (failure) => {
// Failure
if(failure.message.indexOf("Only secure origins are allowed") == 0) {
// Secure Origin issue
toastr.error(`Geolocation disabled? Try secure domain: ${AppConstants.SECURE_BASE}.`);
} else console.error(failure);
});
} else toastr.error(`Browser doesn't support geolocation`);
}
let no_today_data = Object.keys(today_data).length == 0
this.setState({open: true, form: no_today_data ? this.initial_form_state() : clone(today_data)});
}
got_location(position) {
let lat = position.coords.latitude;
let lon = position.coords.longitude;
let pos = {lat: lat, lon: lon};
this.setState({position: pos});
}
submit() {
let {position, tasks, historical, historical_date, historical_incomplete_dates} = this.state;
let _form = this.state.form;
let form = clone(_form);
let params = this.refs.je.get_params();
merge(params, form);
if (position) {
params.lat = position.lat;
params.lon = position.lon;
}
if (historical && historical_date != null) params.date = historical_date;
if (tasks) {
params.tasks = JSON.stringify(tasks)
}
api.post("/api/journal/submit", params, (res) => {
let st = {open: false, historical: false, historical_date: null}
if (!this.submitted() && res.journal != null) st.submitted_date = res.journal.iso_date
if (historical && historical_date != null) {
st.form = this.initial_form_state()
let incomplete_dates = without(historical_incomplete_dates, historical_date)
st.historical_incomplete_dates = incomplete_dates
} else {
st.today_data = res.journal.data
}
this.setState(st)
});
}
dismiss() {
this.setState({open: false, historical: false, historical_date: null});
}
render_location() {
let {include_location} = this.props;
let {position} = this.state;
if (!include_location) return null;
else {
let position_text = "--";
if (position) position_text = position.lat + ", " + position.lon;
return (
<div className="vpad pull-right">
<div style={{fontSize: ".7em"}}><b>Location:</b> { position_text }</div>
</div>
);
}
}
toggle_historical() {
let N_DAYS = 7;
let {historical_incomplete_dates} = this.state;
let setting_historical = !this.state.historical;
this.setState({historical: !this.state.historical}, () => {
if (setting_historical && historical_incomplete_dates.length == 0) {
api.get("/api/journal", {days: N_DAYS}, (res) => {
let cursor = new Date();
let possible_dates = [];
for (let i = 0; i < N_DAYS - 1; i++) {
cursor.setDate(cursor.getDate() - 1);
possible_dates.push(util.printDateObj(cursor));
}
if (res.journals) {
res.journals.forEach((jrnl) => {
let idx = possible_dates.indexOf(jrnl.iso_date);
if (idx > -1) possible_dates.splice(idx, 1);
});
}
this.setState({historical_incomplete_dates: possible_dates});
});
}
});
}
submitted() {
let {submitted_date} = this.state
return submitted_date != null && submitted_date == util.iso_from_date(this.current_submission_date())
}
current_submission_date() {
// Submission date for non-historical journals submitted now
let d = new Date()
d.setHours(d.getHours() - AppConstants.JOURNAL_HOURS_BACK)
return d
}
get_journal_date() {
let {historical, historical_date} = this.state;
let d
if (historical && historical_date != null) d = util.date_from_iso(historical_date)
else d = this.current_submission_date()
return d
}
render_history_section() {
let {historical, historical_date, historical_incomplete_dates, today} = this.state;
let _selector;
if (historical) {
let today = util.printDateObj(this.current_submission_date());
let opts = [<MenuItem key='today' value={today} primaryText={`Today (${today})`} />];
historical_incomplete_dates = without(historical_incomplete_dates, today)
opts = opts.concat(historical_incomplete_dates.map((iso) => {
return <MenuItem key={iso} value={iso} primaryText={iso} />
}));
_selector = (
<DropDownMenu value={historical_date || today} onChange={this.changeHandlerDropDown.bind(this, null, 'historical_date')}>
{ opts }
</DropDownMenu>
);
}
return (
<div>
<div className="pull-right">
<span hidden={historical}>
<IconButton iconClassName="material-icons" onClick={this.toggle_historical.bind(this)}>history</IconButton><br/>
</span>
{ _selector }
</div>
<div className="clearfix"></div>
</div>
)
}
render_tasks() {
let {tomorrow_top_tasks} = this.props;
let {tasks} = this.state;
if (!tomorrow_top_tasks) return null;
else {
let _tasks = tasks.map((task, i) => {
let task_key = 'task' + (i+1);
return <TextField key={task_key} placeholder={`Task ${i+1}`} name={task_key} value={task} onChange={this.change_task.bind(this, i)} fullWidth />
})
return (
<div>
<p className="lead">What are your top couple of tasks for tomorrow?</p>
{ _tasks }
<span hidden={tasks.length >= this.MAX_TASKS}>
<RaisedButton label="Add Task" onClick={this.add_task.bind(this)} />
</span>
<span hidden={tasks.length == 0}>
<FlatButton label="Remove Task" onClick={this.remove_task.bind(this)} />
</span>
</div>
)
}
}
render() {
let {form, open, submitted_date} = this.state;
let {questions} = this.props;
let in_window = this.in_journal_window();
let actions = [
<RaisedButton label="Save Journal" primary={true} onClick={this.submit.bind(this)} />,
<FlatButton label="Dismiss" onClick={this.dismiss.bind(this)} />
]
let submitted = this.submitted()
let _cta = in_window ? <small><div><a href="javascript:void(0)" onClick={this.open_journal_dialog.bind(this)}>{ submitted ? "Update journal" : "Fill journal" }</a></div></small> : <small><div>You can submit at {this.props.window_start_hr}:00. <Link to="/app/settings#journals">Configure journal timing</Link>.</div></small>;
let _status = (
<p className="lead">{ submitted ? `Journal submitted for ${submitted_date}, but you can still make edits` : "Journal not yet submitted" }. { _cta }</p>
)
let journal_date = util.iso_from_date(this.get_journal_date())
let title = `Submit Daily Journal (${journal_date})`
return (
<div>
<MobileDialog title={title}
open={open} onRequestClose={this.dismiss.bind(this)}
autoDetectWindowHeight={true} autoScrollBodyContent={true}
actions={actions}>
<div style={{padding: "10px"}}>
{ this.render_history_section() }
<JournalEditor ref="je" form={form} onChange={this.journal_form_change.bind(this)} questions={questions} />
{ this.render_tasks() }
{ this.render_location() }
</div>
</MobileDialog>
<div>
<div className="row">
<div className="col-sm-6">
<h3>Daily Journal</h3>
</div>
<div className="col-sm-6">
<IconMenu className="pull-right" iconButtonElement={<IconButton iconClassName="material-icons">more_vert</IconButton>}>
<Link to="/app/journal/history"><MenuItem key="hist" primaryText="Journal History" leftIcon={<FontIcon className="material-icons">list</FontIcon>} /></Link>
<MenuItem key="refresh" primaryText="Refresh" onClick={this.maybe_check_if_not_submitted.bind(this)} leftIcon={<FontIcon className="material-icons">refresh</FontIcon>} />
</IconMenu>
</div>
</div>
{ _status }
</div>
</div>
)
}
}