mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-02 04:01:43 +02:00
web: add support for yearly reminders
This commit is contained in:
30
apps/web/package-lock.json
generated
30
apps/web/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -36,7 +36,7 @@ export type FieldProps = InputProps & {
|
||||
};
|
||||
action?: {
|
||||
testId?: string;
|
||||
onClick?: () => void;
|
||||
onClick?: React.MouseEventHandler<HTMLButtonElement>;
|
||||
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}
|
||||
>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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<string>();
|
||||
const [description, setDescription] = useState<string>();
|
||||
const [showCalendar, setShowCalendar] = useState(false);
|
||||
const refresh = useStore((state) => state.refresh);
|
||||
const isUserPremium = useIsUserPremium();
|
||||
const theme = useThemeStore((store) => store.colorScheme);
|
||||
const dateInputRef = useRef<HTMLInputElement>(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) {
|
||||
|
||||
<Flex sx={{ gap: 2, overflowX: "auto", mt: 2 }}>
|
||||
{mode === Modes.ONCE ? (
|
||||
<Field
|
||||
id="date"
|
||||
label="Date"
|
||||
required
|
||||
type="date"
|
||||
data-test-id="date-input"
|
||||
min={dayjs().format("YYYY-MM-DD")}
|
||||
defaultValue={date}
|
||||
value={date}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const date = dayjs(e.target.value);
|
||||
setDate(date.format("YYYY-MM-DD"));
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<Field
|
||||
id="date"
|
||||
label="Date"
|
||||
required
|
||||
inputRef={dateInputRef}
|
||||
data-test-id="date-input"
|
||||
defaultValue={getFormattedDate(date, "date")}
|
||||
readOnly
|
||||
action={{
|
||||
icon: Calendar,
|
||||
onClick() {
|
||||
setShowCalendar(true);
|
||||
}
|
||||
}}
|
||||
onClick={() => setShowCalendar(true)}
|
||||
/>
|
||||
<PopupPresenter
|
||||
isOpen={showCalendar}
|
||||
onClose={() => 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" }
|
||||
}}
|
||||
>
|
||||
<DayPicker
|
||||
mode="single"
|
||||
captionLayout="dropdown-buttons"
|
||||
fromYear={new Date().getFullYear()}
|
||||
toYear={new Date().getFullYear() + 99}
|
||||
modifiers={{
|
||||
disabled: { before: new Date() }
|
||||
}}
|
||||
selected={dayjs(date).toDate()}
|
||||
onSelect={(day) => {
|
||||
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
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</PopupPresenter>
|
||||
</>
|
||||
) : recurringMode === RecurringModes.YEAR ? (
|
||||
<>
|
||||
<LabeledSelect
|
||||
id="month"
|
||||
label="Month"
|
||||
value={`${dayjs(date).month()}`}
|
||||
options={MONTHS_FULL.map((month, index) => ({
|
||||
value: `${index}`,
|
||||
title: month
|
||||
}))}
|
||||
onSelectionChanged={(month) =>
|
||||
setDate(dayjs(date).month(parseInt(month)).format("YYYY-MM-DD"))
|
||||
}
|
||||
/>
|
||||
<LabeledSelect
|
||||
id="day"
|
||||
label="Day"
|
||||
value={`${dayjs(date).date()}`}
|
||||
options={new Array(dayjs(date).daysInMonth())
|
||||
.fill("0")
|
||||
.map((_, day) => ({
|
||||
value: `${day + 1}`,
|
||||
title: `${day + 1}`
|
||||
}))}
|
||||
onSelectionChanged={(day) =>
|
||||
setDate(dayjs(date).date(parseInt(day)).format("YYYY-MM-DD"))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
<Field
|
||||
id="time"
|
||||
@@ -473,3 +572,54 @@ function nth(n: number) {
|
||||
"th"
|
||||
);
|
||||
}
|
||||
|
||||
type LabeledSelectProps = {
|
||||
label: string;
|
||||
id: string;
|
||||
options: { value: string; title: string }[];
|
||||
value: string;
|
||||
onSelectionChanged: (value: string) => void;
|
||||
};
|
||||
function LabeledSelect(props: LabeledSelectProps) {
|
||||
const { id, label, options, value, onSelectionChanged } = props;
|
||||
return (
|
||||
<Label
|
||||
htmlFor={id}
|
||||
variant="text.body"
|
||||
sx={{
|
||||
m: "2px",
|
||||
mr: "2px",
|
||||
fontSize: "subtitle",
|
||||
fontWeight: "bold",
|
||||
fontFamily: "body",
|
||||
color: "paragraph",
|
||||
flexDirection: "column",
|
||||
width: "auto",
|
||||
"& > select": {
|
||||
mt: 1,
|
||||
backgroundColor: "background",
|
||||
// outline: "none",
|
||||
border: "none",
|
||||
outline: "1.5px solid var(--border)",
|
||||
borderRadius: "default",
|
||||
color: "paragraph",
|
||||
padding: 2,
|
||||
fontFamily: "body",
|
||||
fontSize: "input"
|
||||
}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => onSelectionChanged(e.target.value)}
|
||||
>
|
||||
{options.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<PopupPresenterProps>) {
|
||||
const {
|
||||
@@ -43,7 +43,8 @@ function _PopupPresenter(props: PropsWithChildren<PopupPresenterProps>) {
|
||||
focusOnRender = true,
|
||||
children,
|
||||
scope,
|
||||
isMobile
|
||||
isMobile,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const contentRef = useRef<HTMLDivElement>();
|
||||
@@ -215,7 +216,9 @@ function _PopupPresenter(props: PropsWithChildren<PopupPresenterProps>) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EmotionThemeProvider scope={scope}>{children}</EmotionThemeProvider>
|
||||
<EmotionThemeProvider scope={scope} {...restProps}>
|
||||
{children}
|
||||
</EmotionThemeProvider>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user