Skip to content
This repository has been archived by the owner on Jul 12, 2021. It is now read-only.

Commit

Permalink
Handle all dates with Moment.js UTC mode
Browse files Browse the repository at this point in the history
This should help prevent unwanted behavior due to implicit timezone conversions.

Fixes #69
  • Loading branch information
samuelmeuli committed Apr 7, 2020
1 parent a1ff705 commit 3f7ffba
Show file tree
Hide file tree
Showing 29 changed files with 167 additions and 136 deletions.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"website:build:css": "cp ./node_modules/minireset.css/minireset.min.css ./website/styles/",
"website:build:ts": "tsc --project ./website",
"test": "run-s test:*",
"test:jest": "cross-env TZ=GMT jest",
"test:jest": "jest",
"test:types": "tsc --noEmit && tsc --project ./website --noEmit",
"lint:css": "stylelint --fix --ignore-path ./.gitignore --max-warnings 0 '**/*.{css,sass,scss}'",
"lint:ts": "eslint --ext .ts,.tsx --fix --ignore-path ./.gitignore --max-warnings 0 '**/*.{ts,tsx}'",
Expand Down Expand Up @@ -98,7 +98,6 @@
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"babel-loader": "^8.1.0",
"cross-env": "^7.0.2",
"css-loader": "^3.4.0",
"electron": "^8.2.0",
"electron-builder": "^22.4.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Moment } from "moment";
import React, { ReactElement } from "react";
import countWords from "word-count";

import { Entries } from "../../../../../types";
import { toIndexDate } from "../../../../../utils/dateFormat";

export interface StateProps {
dateSelected: Date;
dateSelected: Moment;
entries: Entries;
}

Expand Down
7 changes: 4 additions & 3 deletions src/renderer/components/elements/editor/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import createListPlugin from "draft-js-list-plugin";
import PluginEditor from "draft-js-plugins-editor";
import debounce from "lodash.debounce";
import { draftToMarkdown, markdownToDraft } from "markdown-draft-js";
import { Moment } from "moment";
import React, { KeyboardEvent, PureComponent, ReactNode } from "react";

import { Entries, IndexDate } from "../../../../types";
Expand All @@ -30,7 +31,7 @@ const listPlugin = createListPlugin();
const plugins = [listPlugin];

export interface StateProps {
dateSelected: Date;
dateSelected: Moment;
entries: Entries;
}

Expand All @@ -41,7 +42,7 @@ export interface DispatchProps {
type Props = StateProps & DispatchProps;

interface State {
dateSelected: Date;
dateSelected: Moment;
textEditorState: EditorState;
titleEditorState: EditorState;
}
Expand All @@ -63,7 +64,7 @@ export default class Editor extends PureComponent<Props, State> {

static getStateFromEntry(
entries: Entries,
date: Date,
date: Moment,
): { textEditorState: EditorState; titleEditorState: EditorState } {
const indexDate = toIndexDate(date);
const entry = entries[indexDate];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import PrevIcon from "feather-icons/dist/icons/chevron-left.svg";
import NextIcon from "feather-icons/dist/icons/chevron-right.svg";
import moment from "moment";
import { Moment } from "moment";
import React, { ReactElement } from "react";

import { MAX_DATE, MIN_DATE } from "../../../../constants";
import { toMonthYear } from "../../../../utils/dateFormat";
import { createDate, parseDate, toMonthYear } from "../../../../utils/dateFormat";
import { translations } from "../../../../utils/i18n";
import { iconProps } from "../../../../utils/icons";

export interface StateProps {
allowFutureEntries: boolean;
monthSelected: Date;
monthSelected: Moment;
}

export interface DispatchProps {
Expand All @@ -28,11 +28,11 @@ export default function CalendarNav(props: Props): ReactElement {
setMonthSelectedPrevious,
} = props;

const today = moment();
const today = createDate();

// Check if buttons for switching to previous/next month should be enabled. Determined based on
// the min/max dates and whether future diary entries are allowed
const month = moment(monthSelected);
const month = parseDate(monthSelected);
const canClickPrev = month.isAfter(MIN_DATE, "month");
const canClickNext =
month.isBefore(MAX_DATE, "month") && (allowFutureEntries || month.isBefore(today, "month"));
Expand Down
31 changes: 17 additions & 14 deletions src/renderer/components/elements/sidebar/calendar/Calendar.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import moment from "moment";
import { Moment } from "moment";
import React, { PureComponent, ReactNode } from "react";
import DayPicker from "react-day-picker";
import MomentLocaleUtils from "react-day-picker/moment";

import { Entries, Weekday } from "../../../../types";
import { toIndexDate } from "../../../../utils/dateFormat";
import { createDate, parseDate, toIndexDate } from "../../../../utils/dateFormat";
import { lang } from "../../../../utils/i18n";
import CalendarNavContainer from "../calendar-nav/CalendarNavContainer";

export interface StateProps {
allowFutureEntries: boolean;
dateSelected: Date;
dateSelected: Moment;
entries: Entries;
firstDayOfWeek: Weekday | null;
monthSelected: Date;
monthSelected: Moment;
}

export interface DispatchProps {
setDateSelected: (date: Date) => void;
setDateSelected: (date: Moment) => void;
}

type Props = StateProps & DispatchProps;
Expand All @@ -32,28 +32,31 @@ export default class Calendar extends PureComponent<Props, {}> {

onDateSelection(date: Date): void {
const { allowFutureEntries, setDateSelected } = this.props;
const parsedDate = parseDate(date);
const today = createDate();

if (allowFutureEntries || moment(date).isSameOrBefore(moment(), "day")) {
setDateSelected(date);
if (allowFutureEntries || parseDate(date).isSameOrBefore(today, "day")) {
setDateSelected(parsedDate);
}
}

render(): ReactNode {
const { allowFutureEntries, dateSelected, entries, firstDayOfWeek, monthSelected } = this.props;

const today = new Date();
const today = createDate();
const daysWithEntries = Object.keys(entries);
const hasEntry = (day: Date): boolean => {
const indexDate = toIndexDate(day);

const hasEntry = (date: Date): boolean => {
const indexDate = toIndexDate(parseDate(date));
return daysWithEntries.includes(indexDate);
};

return (
<DayPicker
selectedDays={dateSelected}
disabledDays={allowFutureEntries ? null : { after: today }}
month={monthSelected}
toMonth={today}
selectedDays={dateSelected.toDate()}
disabledDays={allowFutureEntries ? null : { after: today.toDate() }}
month={monthSelected.toDate()}
toMonth={today.toDate()}
captionElement={(): null => null}
modifiers={{ hasEntry }}
firstDayOfWeek={firstDayOfWeek ?? undefined}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Moment } from "moment";
import { connect } from "react-redux";

import { setDateSelected } from "../../../../store/diary/actionCreators";
Expand All @@ -14,7 +15,7 @@ const mapStateToProps = (state: RootState): StateProps => ({
});

const mapDispatchToProps = (dispatch: ThunkDispatchT): DispatchProps => ({
setDateSelected: (date: Date): SetDateSelectedAction => dispatch(setDateSelected(date)),
setDateSelected: (date: Moment): SetDateSelectedAction => dispatch(setDateSelected(date)),
});

export default connect(mapStateToProps, mapDispatchToProps)(Calendar);
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import ClearIcon from "feather-icons/dist/icons/x.svg";
import debounce from "lodash.debounce";
import moment from "moment";
import { Moment } from "moment";
import React, { ChangeEvent, PureComponent, ReactNode } from "react";

import TodayIcon from "../../../../assets/icons/today.svg";
import { createDate, parseDate } from "../../../../utils/dateFormat";
import { translations } from "../../../../utils/i18n";
import { iconProps } from "../../../../utils/icons";

export interface StateProps {
dateSelected: Date;
monthSelected: Date;
dateSelected: Moment;
monthSelected: Moment;
searchKey: string;
}

export interface DispatchProps {
search: (searchKey: string) => void;
setDateSelected: (date: Date) => void;
setDateSelected: (date: Moment) => void;
}

type Props = StateProps & DispatchProps;
Expand Down Expand Up @@ -56,7 +57,7 @@ export default class SearchBar extends PureComponent<Props, State> {
onTodaySelection(): void {
const { setDateSelected } = this.props;

const today = new Date();
const today = createDate();
setDateSelected(today);
}

Expand All @@ -77,9 +78,9 @@ export default class SearchBar extends PureComponent<Props, State> {
const { dateSelected, monthSelected } = this.props;
const { newSearchKey } = this.state;

const today = moment();
const isToday = moment(dateSelected).isSame(today, "day");
const isCurrentMonth = moment(monthSelected).isSame(today, "month");
const today = createDate();
const isToday = parseDate(dateSelected).isSame(today, "day");
const isCurrentMonth = parseDate(monthSelected).isSame(today, "month");

return (
<div className="view-selector">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Moment } from "moment";
import { connect } from "react-redux";

import { search, setDateSelected } from "../../../../store/diary/actionCreators";
Expand All @@ -13,7 +14,7 @@ const mapStateToProps = (state: RootState): StateProps => ({

const mapDispatchToProps = (dispatch: ThunkDispatchT): DispatchProps => ({
search: (searchKey: string): void => dispatch(search(searchKey)),
setDateSelected: (date: Date): SetDateSelectedAction => dispatch(setDateSelected(date)),
setDateSelected: (date: Moment): SetDateSelectedAction => dispatch(setDateSelected(date)),
});

export default connect(mapStateToProps, mapDispatchToProps)(SearchBar);
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Moment } from "moment";
import React, { PureComponent, ReactNode } from "react";

import { Entries } from "../../../../types";
import { momentIndex, toDateString } from "../../../../utils/dateFormat";
import { fromIndexDate, toDateString } from "../../../../utils/dateFormat";
import { translations } from "../../../../utils/i18n";
import Banner from "../../general/banner/Banner";

export interface StateProps {
dateSelected: Date;
dateSelected: Moment;
entries: Entries;
searchResults: string[];
}

export interface DispatchProps {
setDateSelected: (date: Date) => void;
setDateSelected: (date: Moment) => void;
}

type Props = StateProps & DispatchProps;
Expand All @@ -36,15 +37,15 @@ export default class SearchResults extends PureComponent<Props, {}> {
// Create search result element if a corresponding diary entry exists
// (When deleting a diary entry after a search, it is still part of the search results
// until a new search is performed. That's why it needs to be filtered out here)
const date = momentIndex(searchResult);
const date = fromIndexDate(searchResult);
const { title } = entries[searchResult];
const isSelected = date.isSame(dateSelected, "day");
r.push(
<li key={searchResult} className="search-result">
<button
type="button"
className={`button ${isSelected ? "button-main" : ""}`}
onClick={(): void => setDateSelected(date.toDate())}
onClick={(): void => setDateSelected(date)}
>
<p className="search-date text-faded">{toDateString(date)}</p>
<p className={`search-title ${!title ? "text-faded" : ""}`}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Moment } from "moment";
import { connect } from "react-redux";

import { setDateSelected } from "../../../../store/diary/actionCreators";
Expand All @@ -12,7 +13,7 @@ const mapStateToProps = (state: RootState): StateProps => ({
});

const mapDispatchToProps = (dispatch: ThunkDispatchT): DispatchProps => ({
setDateSelected: (date: Date): SetDateSelectedAction => dispatch(setDateSelected(date)),
setDateSelected: (date: Moment): SetDateSelectedAction => dispatch(setDateSelected(date)),
});

export default connect(mapStateToProps, mapDispatchToProps)(SearchResults);
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import logger from "electron-log";
import { Moment } from "moment";
import React, { ChangeEvent, FormEvent, ReactElement, useState } from "react";

import { momentIndex, toIndexDate } from "../../../utils/dateFormat";
import { createDate, fromIndexDate, toIndexDate } from "../../../utils/dateFormat";
import { translations } from "../../../utils/i18n";
import OverlayContainer from "../overlay-hoc/OverlayContainer";

export interface StateProps {
allowFutureEntries: boolean;
dateSelected: Date;
dateSelected: Moment;
}

export interface DispatchProps {
closeOverlay: () => void;
setDateSelected: (date: Date) => void;
setDateSelected: (date: Moment) => void;
}

type Props = StateProps & DispatchProps;
Expand All @@ -23,7 +24,7 @@ type Props = StateProps & DispatchProps;
export default function GoToDateOverlay(props: Props): ReactElement {
const { allowFutureEntries, closeOverlay, dateSelected, setDateSelected } = props;

const todayFormatted = toIndexDate(new Date());
const todayFormatted = toIndexDate(createDate());
const dateSelectedFormatted = toIndexDate(dateSelected);

// `date` can become `undefined` when the user's date input is incomplete (e.g. year not filled in
Expand All @@ -37,7 +38,7 @@ export default function GoToDateOverlay(props: Props): ReactElement {
if (!date) {
logger.error("Cannot go to date: Date is not defined");
} else {
setDateSelected(momentIndex(date).toDate());
setDateSelected(fromIndexDate(date));
closeOverlay();
}
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Moment } from "moment";
import { connect } from "react-redux";

import { closeOverlay } from "../../../store/app/actionCreators";
Expand All @@ -14,7 +15,7 @@ const mapStateToProps = (state: RootState): StateProps => ({

const mapDispatchToProps = (dispatch: ThunkDispatchT): DispatchProps => ({
closeOverlay: (): SetOverlayAction => dispatch(closeOverlay()),
setDateSelected: (date: Date): SetDateSelectedAction => dispatch(setDateSelected(date)),
setDateSelected: (date: Moment): SetDateSelectedAction => dispatch(setDateSelected(date)),
});

export default connect(mapStateToProps, mapDispatchToProps)(GoToDateOverlay);
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import moment from "moment";
import React, { PureComponent, ReactNode } from "react";
import countWords from "word-count";

import { getLang } from "../../../electron/ipcRenderer/senders";
import { Entries } from "../../../types";
import { momentIndex } from "../../../utils/dateFormat";
import { createDate, fromIndexDate } from "../../../utils/dateFormat";
import { translations } from "../../../utils/i18n";
import OverlayContainer from "../overlay-hoc/OverlayContainer";

Expand Down Expand Up @@ -40,19 +39,19 @@ export default class StatsOverlay extends PureComponent<Props, {}> {
static calcStats(entries: Entries): Stats {
const indexDatesSorted = Object.keys(entries).sort();
const nrEntries = indexDatesSorted.length;
const today = moment();
const today = createDate();

let currentStreak = 1;
let longestStreak = 0;
let nrWords = 0;
let prevDate = momentIndex("1970/01/01");
let prevDate = fromIndexDate("1970/01/01");

// Loop over sorted indices and find corresponding diary entry. Update stats variables using its
// content
indexDatesSorted.forEach((indexDate): void => {
const entry = entries[indexDate];
const { title, text } = entry;
const date = momentIndex(indexDate);
const date = fromIndexDate(indexDate);

// Check if the current date is one day after the previous one. If so, increase
// `currentStreak`. If not, update `longestStreak` if necessary and reset `currentStreak`
Expand Down
Loading

0 comments on commit 3f7ffba

Please sign in to comment.