From 7bca839f6dfe68d2a5cbfef71602696d00915c4b Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Thu, 21 Dec 2023 08:57:46 +0500 Subject: [PATCH] web: add support for yearly reminders --- apps/web/package-lock.json | 30 +++ apps/web/package.json | 2 + apps/web/src/components/field/index.tsx | 9 +- apps/web/src/components/icons/index.tsx | 4 +- apps/web/src/components/reminder/index.tsx | 3 +- apps/web/src/dialogs/add-reminder-dialog.tsx | 182 ++++++++++++++++-- .../src/dialogs/reminder-preview-dialog.tsx | 3 +- apps/web/src/stores/reminder-store.ts | 7 +- .../src/components/popup-presenter/index.tsx | 11 +- 9 files changed, 223 insertions(+), 28 deletions(-) diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index e8fb327d8..3d7c0ba6b 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -47,6 +47,7 @@ "clipboard-polyfill": "4.0.0", "comlink": "^4.3.1", "cronosjs": "^1.7.1", + "date-fns": "^2.30.0", "dayjs": "1.11.9", "electron-trpc": "0.5.2", "event-source-polyfill": "^1.0.25", @@ -66,6 +67,7 @@ "qclone": "^1.2.0", "react": "18.2.0", "react-complex-tree": "^2.2.4", + "react-day-picker": "^8.9.1", "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-hot-toast": "^2.4.1", @@ -44074,6 +44076,21 @@ "version": "3.1.2", "license": "MIT" }, + "node_modules/date-fns": { + "version": "2.30.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", + "integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==", + "dependencies": { + "@babel/runtime": "^7.21.0" + }, + "engines": { + "node": ">=0.11" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/date-fns" + } + }, "node_modules/dayjs": { "version": "1.11.9", "license": "MIT" @@ -48057,6 +48074,19 @@ "react": ">=16.0.0" } }, + "node_modules/react-day-picker": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.0.tgz", + "integrity": "sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/gpbl" + }, + "peerDependencies": { + "date-fns": "^2.28.0 || ^3.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "license": "MIT", diff --git a/apps/web/package.json b/apps/web/package.json index e1680084f..c5792cd41 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -45,6 +45,7 @@ "clipboard-polyfill": "4.0.0", "comlink": "^4.3.1", "cronosjs": "^1.7.1", + "date-fns": "^2.30.0", "dayjs": "1.11.9", "electron-trpc": "0.5.2", "event-source-polyfill": "^1.0.25", @@ -64,6 +65,7 @@ "qclone": "^1.2.0", "react": "18.2.0", "react-complex-tree": "^2.2.4", + "react-day-picker": "^8.9.1", "react-dom": "18.2.0", "react-dropzone": "^14.2.3", "react-hot-toast": "^2.4.1", diff --git a/apps/web/src/components/field/index.tsx b/apps/web/src/components/field/index.tsx index b41710bdf..f56f0bc8d 100644 --- a/apps/web/src/components/field/index.tsx +++ b/apps/web/src/components/field/index.tsx @@ -36,7 +36,7 @@ export type FieldProps = InputProps & { }; action?: { testId?: string; - onClick?: () => void; + onClick?: React.MouseEventHandler; disabled?: boolean; icon?: Icon; component?: JSX.Element; @@ -134,12 +134,13 @@ function Field(props: FieldProps) { data-test-id={action.testId} onClick={action.onClick} sx={{ + bg: "transparent", position: "absolute", - right: "2px", + right: "4px", + top: "2px", px: 1, borderRadius: "default", - ":hover": { bg: "border" }, - height: "100%" + ":hover": { bg: "border" } }} disabled={action.disabled} > diff --git a/apps/web/src/components/icons/index.tsx b/apps/web/src/components/icons/index.tsx index be4f2bfa4..8b024d5e0 100644 --- a/apps/web/src/components/icons/index.tsx +++ b/apps/web/src/components/icons/index.tsx @@ -208,7 +208,8 @@ import { mdiGavel, mdiDesktopClassic, mdiBellBadgeOutline, - mdiDotsHorizontal + mdiDotsHorizontal, + mdiCalendarBlank } from "@mdi/js"; import { useTheme } from "@emotion/react"; import { Theme } from "@notesnook/theme"; @@ -533,3 +534,4 @@ export const Documentation = createIcon(mdiFileDocumentOutline); export const Legal = createIcon(mdiGavel); export const Desktop = createIcon(mdiDesktopClassic); export const Notification = createIcon(mdiBellBadgeOutline); +export const Calendar = createIcon(mdiCalendarBlank); diff --git a/apps/web/src/components/reminder/index.tsx b/apps/web/src/components/reminder/index.tsx index d035f31f4..3b0696202 100644 --- a/apps/web/src/components/reminder/index.tsx +++ b/apps/web/src/components/reminder/index.tsx @@ -49,7 +49,8 @@ import { Reminder as ReminderType } from "@notesnook/core/dist/types"; const RECURRING_MODE_MAP = { week: "Weekly", day: "Daily", - month: "Monthly" + month: "Monthly", + year: "Yearly" } as const; const PRIORITY_ICON_MAP = { diff --git a/apps/web/src/dialogs/add-reminder-dialog.tsx b/apps/web/src/dialogs/add-reminder-dialog.tsx index 3fc2ccc17..65e06fd2f 100644 --- a/apps/web/src/dialogs/add-reminder-dialog.tsx +++ b/apps/web/src/dialogs/add-reminder-dialog.tsx @@ -22,13 +22,19 @@ import Dialog from "../components/dialog"; import Field from "../components/field"; import { Box, Button, Flex, Label, Radio, Text } from "@theme-ui/components"; import dayjs from "dayjs"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { db } from "../common/db"; import { useStore } from "../stores/reminder-store"; import { showToast } from "../utils/toast"; import { useIsUserPremium } from "../hooks/use-is-user-premium"; -import { Pro } from "../components/icons"; +import { Calendar, Pro } from "../components/icons"; import { usePersistentState } from "../hooks/use-persistent-state"; +import { DayPicker } from "react-day-picker"; +import "react-day-picker/dist/style.css"; +import { PopupPresenter } from "@notesnook/ui"; +import { useStore as useThemeStore } from "../stores/theme-store"; +import { getFormattedDate } from "@notesnook/common"; +import { MONTHS_FULL } from "@notesnook/core/dist/utils/date"; export type AddReminderDialogProps = { onClose: Perform; @@ -53,6 +59,7 @@ const Priorities = { const RecurringModes = { WEEK: "week", MONTH: "month", + YEAR: "year", DAY: "day" } as const; @@ -97,6 +104,11 @@ const recurringModes = [ id: RecurringModes.MONTH, title: "Monthly", options: new Array(31).fill(0).map((_, i) => i + 1) + }, + { + id: RecurringModes.YEAR, + title: "Yearly", + options: [] } ]; @@ -115,8 +127,11 @@ export default function AddReminderDialog(props: AddReminderDialogProps) { const [time, setTime] = useState(dayjs().format("HH:mm")); const [title, setTitle] = useState(); const [description, setDescription] = useState(); + const [showCalendar, setShowCalendar] = useState(false); const refresh = useStore((state) => state.refresh); const isUserPremium = useIsUserPremium(); + const theme = useThemeStore((store) => store.colorScheme); + const dateInputRef = useRef(null); useEffect(() => { if (!reminderId) return; @@ -154,12 +169,14 @@ export default function AddReminderDialog(props: AddReminderDialogProps) { title={reminderId ? "Edit reminder" : "Add a reminder"} testId="add-reminder-dialog" onClose={() => props.onClose(false)} + sx={{ fontFamily: "body" }} positiveButton={{ text: reminderId ? "Save" : "Add", disabled: !title || (mode !== Modes.ONCE && recurringMode !== RecurringModes.DAY && + recurringMode !== RecurringModes.YEAR && !selectedDays.length), onClick: async () => { if (!("Notification" in window)) @@ -366,20 +383,102 @@ export default function AddReminderDialog(props: AddReminderDialogProps) { {mode === Modes.ONCE ? ( - ) => { - const date = dayjs(e.target.value); - setDate(date.format("YYYY-MM-DD")); - }} - /> + <> + setShowCalendar(true)} + /> + setShowCalendar(false)} + position={{ + isTargetAbsolute: true, + target: dateInputRef.current, + location: "below" + }} + sx={{ + ".rdp": { + bg: "background", + p: 1, + boxShadow: `0px 0px 25px 5px ${ + theme === "dark" ? "#000000aa" : "#0000004e" + }`, + borderRadius: "dialog", + fontFamily: "body", + "--rdp-accent-color": "var(--accent)", + "--rdp-background-color": "var(--background)", + "--rdp-background-color-dark": "var(--background)", + "--rdp-selected-color": "var(--paragraph-selected)", + color: "paragraph" + }, + ".rdp-caption_label": { fontWeight: "heading" } + }} + > + { + const date = dayjs(day).format("YYYY-MM-DD"); + setDate(date); + if (dateInputRef.current) + dateInputRef.current.value = getFormattedDate(date, "date"); + }} + styles={{ + day: { + fontFamily: "inherit", + fontSize: 14 + } + }} + /> + + + ) : recurringMode === RecurringModes.YEAR ? ( + <> + ({ + value: `${index}`, + title: month + }))} + onSelectionChanged={(month) => + setDate(dayjs(date).month(parseInt(month)).format("YYYY-MM-DD")) + } + /> + ({ + value: `${day + 1}`, + title: `${day + 1}` + }))} + onSelectionChanged={(day) => + setDate(dayjs(date).date(parseInt(day)).format("YYYY-MM-DD")) + } + /> + ) : null} void; +}; +function LabeledSelect(props: LabeledSelectProps) { + const { id, label, options, value, onSelectionChanged } = props; + return ( + + ); +} diff --git a/apps/web/src/dialogs/reminder-preview-dialog.tsx b/apps/web/src/dialogs/reminder-preview-dialog.tsx index 8be7a3369..6b514b647 100644 --- a/apps/web/src/dialogs/reminder-preview-dialog.tsx +++ b/apps/web/src/dialogs/reminder-preview-dialog.tsx @@ -35,7 +35,8 @@ export type ReminderPreviewDialogProps = { const RECURRING_MODE_MAP = { week: "Weekly", day: "Daily", - month: "Monthly" + month: "Monthly", + year: "Yearly" } as const; const SNOOZE_TIMES = [ diff --git a/apps/web/src/stores/reminder-store.ts b/apps/web/src/stores/reminder-store.ts index 57224196d..0d5bf319b 100644 --- a/apps/web/src/stores/reminder-store.ts +++ b/apps/web/src/stores/reminder-store.ts @@ -143,7 +143,12 @@ function reminderToCronExpression(reminder: Reminder) { return dateTime.format("ss mm HH DD MM * YYYY"); } else { const cron = dateTime.format("ss mm HH").split(" "); - if (recurringMode === "week") { + if (recurringMode === "year") { + cron.push(`${dateTime.date()}`); // day of month + cron.push(`${dateTime.month()}`); // month + cron.push("*"); // day of week + cron.push("*"); // year + } else if (recurringMode === "week") { cron.push("*"); // day of month cron.push("*"); // month cron.push(selectedDays.sort((a, b) => a - b).join(",")); // day of week diff --git a/packages/ui/src/components/popup-presenter/index.tsx b/packages/ui/src/components/popup-presenter/index.tsx index 1f3527b4d..a34923607 100644 --- a/packages/ui/src/components/popup-presenter/index.tsx +++ b/packages/ui/src/components/popup-presenter/index.tsx @@ -18,7 +18,7 @@ along with this program. If not, see . */ import { useCallback, useRef, useEffect, PropsWithChildren } from "react"; -import { Box } from "@theme-ui/components"; +import { Box, BoxProps } from "@theme-ui/components"; import { getPosition, PositionOptions } from "../../utils/position"; import Modal from "react-modal"; import { EmotionThemeProvider, ThemeScopes } from "@notesnook/theme"; @@ -32,7 +32,7 @@ export type PopupPresenterProps = { movable?: boolean; scope?: keyof ThemeScopes; isMobile?: boolean; -}; +} & BoxProps; function _PopupPresenter(props: PropsWithChildren) { const { @@ -43,7 +43,8 @@ function _PopupPresenter(props: PropsWithChildren) { focusOnRender = true, children, scope, - isMobile + isMobile, + ...restProps } = props; const contentRef = useRef(); @@ -215,7 +216,9 @@ function _PopupPresenter(props: PropsWithChildren) { } }} > - {children} + + {children} + ); }