diff --git a/.eslintrc.json b/.eslintrc.json index 8566e25..15e8e01 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -110,7 +110,7 @@ "ignoreEnums": true, "enforceConst": true, "ignoreReadonlyClassProperties": true, - "ignore": [0, 1, 2] + "ignore": [0, 1, 2, 1000] } ], "@typescript-eslint/brace-style": ["error", "1tbs"], diff --git a/package.json b/package.json index df98d54..c6bc4dc 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,17 @@ { "name": "amber_web", - "version": "0.0.8", + "version": "0.1.0", "private": true, "dependencies": { "@types/jest": "^24.X", - "@types/node": "^12.12.21", - "@types/react": "^16.9.17", - "@types/react-dom": "^16.9.4", - "@types/react-redux": "^7.1.5", - "@types/react-router-dom": "^5.1.3", - "axios": "^0.19.0", + "@types/node": "^13.X", + "@types/react": "^16.9.X", + "@types/react-dom": "^16.9.X", + "@types/react-redux": "^7.1.X", + "@types/react-router-dom": "^5.1.X", + "axios": "^0.19.1", "bulma": "^0.8.0", + "date-fns": "^2.9.0", "node-sass": "^4.13.0", "react": "^16.12.0", "react-dom": "^16.12.0", @@ -18,7 +19,7 @@ "react-redux": "^7.1.3", "react-router-dom": "^5.1.2", "react-scripts": "3.3.0", - "redux": "^4.0.4", + "redux": "^4.0.5", "redux-devtools-extension": "^2.13.8", "redux-persist": "^6.0.0", "redux-thunk": "^2.3.0", diff --git a/src/const.ts b/src/const.ts index 891ffd7..beb2044 100644 --- a/src/const.ts +++ b/src/const.ts @@ -1,7 +1,7 @@ export const baseURI: string = process.env.REACT_APP_APIURI || "https://amber.h.tdem.in/api/v0"; -export const appVersion: string = "0.0.8"; +export const appVersion: string = "0.1.0"; export const appFullName: string = "Amber Web"; export const appName: string = "amber_web"; export const appAuthor: string = "Timur Demin"; diff --git a/src/helpers/datetime.ts b/src/helpers/datetime.ts new file mode 100644 index 0000000..19644c7 --- /dev/null +++ b/src/helpers/datetime.ts @@ -0,0 +1,8 @@ +export const DateFormat = "yyyy-MM-dd"; +export const TimeFormat = "HH:mm"; + +/** + * Converts a JS date to epoch timestamp. + * @param date Date in JS `Date` class + */ +export const dateTimeToUnixTime = (date: Date) => date.getTime(); diff --git a/src/helpers/tasks.ts b/src/helpers/tasks.ts index 38782b9..3dcbd77 100644 --- a/src/helpers/tasks.ts +++ b/src/helpers/tasks.ts @@ -11,6 +11,8 @@ export const taskFromRecord = (task: TaskRecord): Task => { newTask.Text = "text" in task ? (task.text as string) : ""; newTask.Completed = task.status !== 0; newTask.LastMod = task.last_mod as number; + newTask.Deadline = "deadline" in task ? (task.deadline as number) : 0; + newTask.Reminder = "reminder" in task ? (task.reminder as number) : 0; return newTask; }; @@ -24,6 +26,8 @@ export const taskToRecord = (task: Task): TaskRecord => { parent_id: task.PID, text: task.Text, status: task.Completed ? 1 : 0, + deadline: task.Deadline, + reminder: task.Reminder, } as TaskRecord; return record; }; diff --git a/src/reducers/tasks.ts b/src/reducers/tasks.ts index 2656c23..6aa61fb 100644 --- a/src/reducers/tasks.ts +++ b/src/reducers/tasks.ts @@ -2,8 +2,22 @@ import Actions from "../actions/list"; import { TaskAction } from "../typings/actions"; import { Task } from "../typings/tasks"; -/** Sorts a task array in ascending order. Returns a new sorted array. */ -const sort = (tasks: Task[]) => [...tasks.sort((a, b) => a.ID - b.ID)]; +/** Sorts a task array in ascending order by: + * 1. Reminder date. + * 2. Deadline date. + * 3. ID. + * Returns a new sorted array. */ +const sort = (tasks: Task[]) => [ + ...tasks.sort((a, b) => { + if (a.Reminder !== b.Reminder) { + return b.Reminder - a.Reminder; + } + if (a.Deadline !== b.Deadline) { + return b.Deadline - a.Deadline; + } + return a.ID - b.ID; + }), +]; export interface TaskState { tasks: Task[]; diff --git a/src/typings/tasks.ts b/src/typings/tasks.ts index b809125..3e85034 100644 --- a/src/typings/tasks.ts +++ b/src/typings/tasks.ts @@ -4,9 +4,7 @@ export interface TaskRecord { text?: string; status: number; last_mod?: number; - /** TODO: Not implemented yet. */ deadline?: number; - /** TODO: Not implemented yet. */ reminder?: number; } @@ -16,6 +14,8 @@ export class Task { Text: string; Completed: boolean; LastMod: number; + Deadline: number; + Reminder: number; /** Informational field for tasks that have been created offline, those are * to be pushed to the server at the next sync. */ ToSync: boolean; @@ -31,5 +31,7 @@ export class Task { this.LastMod = Date.now(); this.ToSync = false; this.ToRemove = false; + this.Deadline = 0; + this.Reminder = 0; } } diff --git a/src/views/assets/locales.ts b/src/views/assets/locales.ts index 3a12260..abd5276 100644 --- a/src/views/assets/locales.ts +++ b/src/views/assets/locales.ts @@ -34,6 +34,8 @@ export default new LocalizedStrings({ editor_textTp: "Text:", editor_parentTp: "Parent:", editor_parentNoParentVal: "No parent", + editor_deadline: "Deadline:", + editor_reminder: "Reminder:", task_toggleBtnCompleted: "Completed", task_toggleBtnPending: "Pending", app_versionString: `${appFullName} v${appVersion} by ${appAuthor}`, diff --git a/src/views/components/dateTimePicker.tsx b/src/views/components/dateTimePicker.tsx new file mode 100644 index 0000000..826b9fc --- /dev/null +++ b/src/views/components/dateTimePicker.tsx @@ -0,0 +1,68 @@ +import React from "react"; +import parse from "date-fns/parse"; +import format from "date-fns/format"; + +import { + dateTimeToUnixTime, + DateFormat, + TimeFormat, +} from "../../helpers/datetime"; + +interface Props { + dateRequired?: boolean; + timeRequired?: boolean; + initialValue?: number; + onChange?: (date: number) => void; +} +interface State { + date: Date; +} +export class DateTimePicker extends React.Component { + state = { + date: new Date(this.props.initialValue || 0), + }; + componentDidUpdate = (_p: Props, prevState: State) => { + if (this.props.onChange) { + if (prevState.date !== this.state.date) { + this.props.onChange(dateTimeToUnixTime(this.state.date)); + } + } + }; + updateDate = (e: React.FormEvent) => { + // event input is a string of format like "2020-01-03" + // eslint-disable-next-line react/no-access-state-in-setstate + const date = parse(e.currentTarget.value, DateFormat, this.state.date); + this.setState({ date }); + }; + updateTime = (e: React.FormEvent) => { + // input is a string of format like "13:45" + // eslint-disable-next-line react/no-access-state-in-setstate + const date = parse(e.currentTarget.value, TimeFormat, this.state.date); + this.setState({ date }); + }; + inputInitValue = (date: Date, fmt: string): string => { + if (date.getTime()) { + return format(date, fmt); + } + return ""; + }; + // TODO: add an "Unset" button + render = () => ( +
+ + +
+ ); +} + +export default DateTimePicker; diff --git a/src/views/components/taskLine.tsx b/src/views/components/taskLine.tsx index a0a712f..e0eec3e 100644 --- a/src/views/components/taskLine.tsx +++ b/src/views/components/taskLine.tsx @@ -1,22 +1,25 @@ import React from "react"; import { connect } from "react-redux"; -import { ThunkDispatch } from "redux-thunk"; +import { withRouter, RouteComponentProps as RCP } from "react-router"; +import format from "date-fns/format"; import Level from "../components/bulma/level"; import Button from "../components/bulma/button"; -import Link from "../components/link"; -import { TaskAction } from "../../typings/actions"; import { Task } from "../../typings/tasks"; +import { Dispatch } from "../../typings/react"; import { updateTask } from "../../actions/tasks"; import strings from "../assets/locales"; -interface Props { +const mapDispatchToProps = (dispatch: Dispatch) => ({ + update: (task: Task) => dispatch(updateTask(task)), +}); + +interface Props extends ReturnType, RCP { level: number; task: Task; - dispatch: ThunkDispatch; } interface State { task: Task; @@ -35,22 +38,53 @@ class TaskLine extends React.Component { toggleTask = (): void => { const task: Task = { ...this.state.task }; task.Completed = !task.Completed; - this.props.dispatch(updateTask(task)); + this.props.update(task); }; + // Pp expands to a localized date string that looks like this: + // "05/29/1453, 12:00 AM" + fmtDate = (date: number) => format(new Date(date), "Pp"); + gotoEditor = () => this.props.history.push(`/task/${this.state.task.ID}`); render = () => { - const { ID, Completed, ToRemove, Text } = this.state.task; + const { + ID, + Completed, + ToRemove, + Text, + Deadline, + Reminder, + } = this.state.task; let classNames: string[] = ["taskLine"]; Completed && classNames.push("taskCompleted"); ToRemove && classNames.push("taskToRemove"); + Deadline && classNames.push("taskHasDeadline"); + Reminder && classNames.push("taskHasReminder"); return ( - - - {`#${ID} - `} + + + {`#${ID}`} + {/* eslint-disable react/jsx-no-literals */} +   {Text} - + + + + {`(D: ${this.fmtDate(Deadline)})`} + {/* eslint-disable react/jsx-no-literals */} +   + + + {`(R: ${this.fmtDate(Reminder)})`} + + - +