diff --git a/package.json b/package.json
index 0419a999a..6988c6d50 100644
--- a/package.json
+++ b/package.json
@@ -27,8 +27,7 @@
"@trpc/server": "^9.23.2",
"axios": "^0.24.0",
"clsx": "^1.1.1",
- "date-fns": "^2.28.0",
- "date-fns-tz": "^1.2.2",
+ "dayjs": "^1.11.3",
"eta": "^1.12.3",
"framer-motion": "^6.3.11",
"iron-session": "^6.1.3",
@@ -52,7 +51,7 @@
"react-query": "^3.34.12",
"react-use": "^17.3.2",
"smoothscroll-polyfill": "^0.4.4",
- "spacetime": "^7.1.2",
+ "spacetime": "^7.1.4",
"superjson": "^1.9.1",
"timezone-soft": "^1.3.1",
"typescript": "^4.5.2",
diff --git a/src/components/discussion/discussion.tsx b/src/components/discussion/discussion.tsx
index 1fcaaed16..c8885c21d 100644
--- a/src/components/discussion/discussion.tsx
+++ b/src/components/discussion/discussion.tsx
@@ -1,5 +1,5 @@
import clsx from "clsx";
-import { formatRelative } from "date-fns";
+import dayjs from "dayjs";
import { AnimatePresence, motion } from "framer-motion";
import { usePlausible } from "next-plausible";
import * as React from "react";
@@ -16,7 +16,6 @@ import NameInput from "../name-input";
import TruncatedLinkify from "../poll/truncated-linkify";
import UserAvatar from "../poll/user-avatar";
import { usePoll } from "../poll-context";
-import { usePreferences } from "../preferences/use-preferences";
import { isUnclaimed, useSession } from "../session";
interface CommentForm {
@@ -25,7 +24,6 @@ interface CommentForm {
}
const Discussion: React.VoidFunctionComponent = () => {
- const { locale } = usePreferences();
const queryClient = trpc.useContext();
const { poll } = usePoll();
@@ -122,13 +120,7 @@ const Discussion: React.VoidFunctionComponent = () => {
•
- {formatRelative(
- new Date(comment.createdAt),
- Date.now(),
- {
- locale,
- },
- )}
+ {dayjs(new Date(comment.createdAt)).fromNow()}
+ local.format(start, "MMMM DD", culture) +
+ " – " +
+ local.format(end, local.eq(start, end, "month") ? "DD" : "MMMM DD", culture);
+
+const dateRangeFormat = ({ start, end }, culture, local) =>
+ local.format(start, "L", culture) + " – " + local.format(end, "L", culture);
+
+const timeRangeFormat = ({ start, end }, culture, local) =>
+ local.format(start, "LT", culture) + " – " + local.format(end, "LT", culture);
+
+const timeRangeStartFormat = ({ start }, culture, local) =>
+ local.format(start, "LT", culture) + " – ";
+
+const timeRangeEndFormat = ({ end }, culture, local) =>
+ " – " + local.format(end, "LT", culture);
+
+export const formats = {
+ dateFormat: "DD",
+ dayFormat: "DD ddd",
+ weekdayFormat: "ddd",
+
+ selectRangeFormat: timeRangeFormat,
+ eventTimeRangeFormat: timeRangeFormat,
+ eventTimeRangeStartFormat: timeRangeStartFormat,
+ eventTimeRangeEndFormat: timeRangeEndFormat,
+
+ timeGutterFormat: "LT",
+
+ monthHeaderFormat: "MMMM YYYY",
+ dayHeaderFormat: "dddd MMM DD",
+ dayRangeHeaderFormat: weekRangeFormat,
+ agendaHeaderFormat: dateRangeFormat,
+
+ agendaDateFormat: "ddd MMM DD",
+ agendaTimeFormat: "LT",
+ agendaTimeRangeFormat: timeRangeFormat,
+};
+
+function fixUnit(unit) {
+ let datePart = unit ? unit.toLowerCase() : unit;
+ if (datePart === "FullYear") {
+ datePart = "year";
+ } else if (!datePart) {
+ datePart = undefined;
+ }
+ return datePart;
+}
+
+export default function (dayjs) {
+ const locale = (m, c) => (c ? m.locale(c) : m);
+
+ /*** BEGIN localized date arithmetic methods with dayjs ***/
+ function defineComparators(a, b, unit) {
+ const datePart = fixUnit(unit);
+ const dtA = datePart ? dayjs(a).startOf(datePart) : dayjs(a);
+ const dtB = datePart ? dayjs(b).startOf(datePart) : dayjs(b);
+ return [dtA, dtB, datePart];
+ }
+
+ function startOf(date = null, unit) {
+ const datePart = fixUnit(unit);
+ if (datePart) {
+ return dayjs(date).startOf(datePart).toDate();
+ }
+ return dayjs(date).toDate();
+ }
+
+ function endOf(date = null, unit) {
+ const datePart = fixUnit(unit);
+ if (datePart) {
+ return dayjs(date).endOf(datePart).toDate();
+ }
+ return dayjs(date).toDate();
+ }
+
+ // dayjs comparison operations *always* convert both sides to dayjs objects
+ // prior to running the comparisons
+ function eq(a, b, unit) {
+ const [dtA, dtB, datePart] = defineComparators(a, b, unit);
+ return dtA.isSame(dtB, datePart);
+ }
+
+ function neq(a, b, unit) {
+ return !eq(a, b, unit);
+ }
+
+ function gt(a, b, unit) {
+ const [dtA, dtB, datePart] = defineComparators(a, b, unit);
+ return dtA.isAfter(dtB, datePart);
+ }
+
+ function lt(a, b, unit) {
+ const [dtA, dtB, datePart] = defineComparators(a, b, unit);
+ return dtA.isBefore(dtB, datePart);
+ }
+
+ function gte(a, b, unit) {
+ const [dtA, dtB, datePart] = defineComparators(a, b, unit);
+ return dtA.isSameOrBefore(dtB, datePart);
+ }
+
+ function lte(a, b, unit) {
+ const [dtA, dtB, datePart] = defineComparators(a, b, unit);
+ return dtA.isSameOrBefore(dtB, datePart);
+ }
+
+ function inRange(day, min, max, unit = "day") {
+ const datePart = fixUnit(unit);
+ const mDay = dayjs(day);
+ const mMin = dayjs(min);
+ const mMax = dayjs(max);
+ return mDay.isBetween(mMin, mMax, datePart, "[]");
+ }
+
+ function min(dateA, dateB) {
+ const dtA = dayjs(dateA);
+ const dtB = dayjs(dateB);
+ const minDt = dayjs.min(dtA, dtB);
+ return minDt.toDate();
+ }
+
+ function max(dateA, dateB) {
+ const dtA = dayjs(dateA);
+ const dtB = dayjs(dateB);
+ const maxDt = dayjs.max(dtA, dtB);
+ return maxDt.toDate();
+ }
+
+ function merge(date, time) {
+ if (!date && !time) return null;
+
+ const tm = dayjs(time).format("HH:mm:ss");
+ const dt = dayjs(date).startOf("day").format("MM/DD/YYYY");
+ // We do it this way to avoid issues when timezone switching
+ return dayjs(`${dt} ${tm}`, "MM/DD/YYYY HH:mm:ss").toDate();
+ }
+
+ function add(date, adder, unit) {
+ const datePart = fixUnit(unit);
+ return dayjs(date).add(adder, datePart).toDate();
+ }
+
+ function range(start, end, unit = "day") {
+ const datePart = fixUnit(unit);
+ // because the add method will put these in tz, we have to start that way
+ let current = dayjs(start).toDate();
+ const days = [];
+
+ while (lte(current, end)) {
+ days.push(current);
+ current = add(current, 1, datePart);
+ }
+
+ return days;
+ }
+
+ function ceil(date, unit) {
+ const datePart = fixUnit(unit);
+ const floor = startOf(date, datePart);
+
+ return eq(floor, date) ? floor : add(floor, 1, datePart);
+ }
+
+ function diff(a, b, unit = "day") {
+ const datePart = fixUnit(unit);
+ // don't use 'defineComparators' here, as we don't want to mutate the values
+ const dtA = dayjs(a);
+ const dtB = dayjs(b);
+ return dtB.diff(dtA, datePart);
+ }
+
+ function minutes(date) {
+ const dt = dayjs(date);
+ return dt.minutes();
+ }
+
+ function firstOfWeek() {
+ const data = dayjs.localeData();
+ return data ? data.firstDayOfWeek() : 0;
+ }
+
+ function firstVisibleDay(date) {
+ return dayjs(date).startOf("month").startOf("week").toDate();
+ }
+
+ function lastVisibleDay(date) {
+ return dayjs(date).endOf("month").endOf("week").toDate();
+ }
+
+ function visibleDays(date) {
+ let current = firstVisibleDay(date);
+ const last = lastVisibleDay(date);
+ const days = [];
+
+ while (lte(current, last)) {
+ days.push(current);
+ current = add(current, 1, "d");
+ }
+
+ return days;
+ }
+ /*** END localized date arithmetic methods with dayjs ***/
+
+ /**
+ * Moved from TimeSlots.js, this method overrides the method of the same name
+ * in the localizer.js, using dayjs to construct the js Date
+ * @param {Date} dt - date to start with
+ * @param {Number} minutesFromMidnight
+ * @param {Number} offset
+ * @returns {Date}
+ */
+ function getSlotDate(dt, minutesFromMidnight, offset) {
+ return dayjs(dt)
+ .startOf("day")
+ .minute(minutesFromMidnight + offset)
+ .toDate();
+ }
+
+ // dayjs will automatically handle DST differences in it's calculations
+ function getTotalMin(start, end) {
+ return diff(start, end, "minutes");
+ }
+
+ function getMinutesFromMidnight(start) {
+ const dayStart = dayjs(start).startOf("day");
+ const day = dayjs(start);
+ return day.diff(dayStart, "minutes");
+ }
+
+ // These two are used by DateSlotMetrics
+ function continuesPrior(start, first) {
+ const mStart = dayjs(start);
+ const mFirst = dayjs(first);
+ return mStart.isBefore(mFirst, "day");
+ }
+
+ function continuesAfter(start, end, last) {
+ const mEnd = dayjs(end);
+ const mLast = dayjs(last);
+ return mEnd.isSameOrAfter(mLast, "minutes");
+ }
+
+ // These two are used by eventLevels
+ function sortEvents({
+ evtA: { start: aStart, end: aEnd, allDay: aAllDay },
+ evtB: { start: bStart, end: bEnd, allDay: bAllDay },
+ }) {
+ const startSort = +startOf(aStart, "day") - +startOf(bStart, "day");
+
+ const durA = diff(aStart, ceil(aEnd, "day"), "day");
+
+ const durB = diff(bStart, ceil(bEnd, "day"), "day");
+
+ return (
+ startSort || // sort by start Day first
+ Math.max(durB, 1) - Math.max(durA, 1) || // events spanning multiple days go first
+ !!bAllDay - !!aAllDay || // then allDay single day events
+ +aStart - +bStart || // then sort by start time *don't need dayjs conversion here
+ +aEnd - +bEnd // then sort by end time *don't need dayjs conversion here either
+ );
+ }
+
+ function inEventRange({
+ event: { start, end },
+ range: { start: rangeStart, end: rangeEnd },
+ }) {
+ const startOfDay = dayjs(start).startOf("day");
+ const eEnd = dayjs(end);
+ const rStart = dayjs(rangeStart);
+ const rEnd = dayjs(rangeEnd);
+
+ const startsBeforeEnd = startOfDay.isSameOrBefore(rEnd, "day");
+ // when the event is zero duration we need to handle a bit differently
+ const sameMin = !startOfDay.isSame(eEnd, "minutes");
+ const endsAfterStart = sameMin
+ ? eEnd.isAfter(rStart, "minutes")
+ : eEnd.isSameOrAfter(rStart, "minutes");
+
+ return startsBeforeEnd && endsAfterStart;
+ }
+
+ // dayjs treats 'day' and 'date' equality very different
+ // dayjs(date1).isSame(date2, 'day') would test that they were both the same day of the week
+ // dayjs(date1).isSame(date2, 'date') would test that they were both the same date of the month of the year
+ function isSameDate(date1, date2) {
+ const dt = dayjs(date1);
+ const dt2 = dayjs(date2);
+ return dt.isSame(dt2, "date");
+ }
+
+ /**
+ * This method, called once in the localizer constructor, is used by eventLevels
+ * 'eventSegments()' to assist in determining the 'span' of the event in the display,
+ * specifically when using a timezone that is greater than the browser native timezone.
+ * @returns number
+ */
+ function browserTZOffset() {
+ /**
+ * Date.prototype.getTimezoneOffset horrifically flips the positive/negative from
+ * what you see in it's string, so we have to jump through some hoops to get a value
+ * we can actually compare.
+ */
+ const dt = new Date();
+ const neg = /-/.test(dt.toString()) ? "-" : "";
+ const dtOffset = dt.getTimezoneOffset();
+ const comparator = Number(`${neg}${Math.abs(dtOffset)}`);
+ // dayjs correctly provides positive/negative offset, as expected
+ const mtOffset = dayjs().utcOffset();
+ return mtOffset > comparator ? 1 : 0;
+ }
+
+ return new DateLocalizer({
+ formats,
+
+ firstOfWeek,
+ firstVisibleDay,
+ lastVisibleDay,
+ visibleDays,
+
+ format(value, format, culture) {
+ return locale(dayjs(value), culture).format(format);
+ },
+
+ lt,
+ lte,
+ gt,
+ gte,
+ eq,
+ neq,
+ merge,
+ inRange,
+ startOf,
+ endOf,
+ range,
+ add,
+ diff,
+ ceil,
+ min,
+ max,
+ minutes,
+
+ getSlotDate,
+ getTotalMin,
+ getMinutesFromMidnight,
+ continuesPrior,
+ continuesAfter,
+ sortEvents,
+ inEventRange,
+ isSameDate,
+ browserTZOffset,
+ });
+}
diff --git a/src/components/forms/poll-options-form/month-calendar/month-calendar.tsx b/src/components/forms/poll-options-form/month-calendar/month-calendar.tsx
index 82e90a91f..9077a49ad 100644
--- a/src/components/forms/poll-options-form/month-calendar/month-calendar.tsx
+++ b/src/components/forms/poll-options-form/month-calendar/month-calendar.tsx
@@ -1,7 +1,5 @@
import clsx from "clsx";
-import differenceInMinutes from "date-fns/differenceInMinutes";
-import { addMinutes, setHours } from "date-fns/esm";
-import isSameDay from "date-fns/isSameDay";
+import dayjs from "dayjs";
import { usePlausible } from "next-plausible";
import * as React from "react";
@@ -126,12 +124,14 @@ const MonthCalendar: React.VoidFunctionComponent = ({
onClick={() => {
if (
datepicker.selection.some((selectedDate) =>
- isSameDay(selectedDate, day.date),
+ dayjs(selectedDate).isSame(day.date, "day"),
)
) {
onChange(removeAllOptionsForDay(options, day.date));
} else {
- const selectedDate = setHours(day.date, 12);
+ const selectedDate = dayjs(day.date)
+ .set("hour", 12)
+ .toDate();
const newOption: DateTimeOption = !isTimedEvent
? {
type: "date",
@@ -141,7 +141,9 @@ const MonthCalendar: React.VoidFunctionComponent = ({
type: "timeSlot",
start: formatDateWithoutTz(selectedDate),
end: formatDateWithoutTz(
- addMinutes(selectedDate, duration),
+ dayjs(selectedDate)
+ .add(duration, "minutes")
+ .toDate(),
),
};
@@ -208,7 +210,9 @@ const MonthCalendar: React.VoidFunctionComponent = ({
);
}
const startDate = new Date(`${option.date}T12:00:00`);
- const endDate = addMinutes(startDate, duration);
+ const endDate = dayjs(startDate)
+ .add(duration, "minutes")
+ .toDate();
return {
type: "timeSlot",
start: formatDateWithoutTz(startDate),
@@ -260,7 +264,9 @@ const MonthCalendar: React.VoidFunctionComponent = ({
{
- const newEnd = addMinutes(newStart, duration);
+ const newEnd = dayjs(newStart)
+ .add(duration, "minutes")
+ .toDate();
// replace enter with updated start time
onChange([
...options.slice(0, index),
@@ -273,13 +279,15 @@ const MonthCalendar: React.VoidFunctionComponent = ({
]);
onNavigate(newStart);
onChangeDuration(
- differenceInMinutes(newEnd, newStart),
+ dayjs(newEnd).diff(newStart, "minutes"),
);
}}
/>
{
onChange([
...options.slice(0, index),
@@ -291,7 +299,7 @@ const MonthCalendar: React.VoidFunctionComponent = ({
]);
onNavigate(newEnd);
onChangeDuration(
- differenceInMinutes(newEnd, startDate),
+ dayjs(newEnd).diff(startDate, "minutes"),
);
}}
/>
@@ -322,7 +330,9 @@ const MonthCalendar: React.VoidFunctionComponent = ({
type: "timeSlot",
start: startTime,
end: formatDateWithoutTz(
- addMinutes(new Date(startTime), duration),
+ dayjs(new Date(startTime))
+ .add(duration, "minutes")
+ .toDate(),
),
},
]);
diff --git a/src/components/forms/poll-options-form/month-calendar/time-picker.tsx b/src/components/forms/poll-options-form/month-calendar/time-picker.tsx
index 11f6fcf87..5a48a38ef 100644
--- a/src/components/forms/poll-options-form/month-calendar/time-picker.tsx
+++ b/src/components/forms/poll-options-form/month-calendar/time-picker.tsx
@@ -7,10 +7,9 @@ import {
} from "@floating-ui/react-dom-interactions";
import { Listbox } from "@headlessui/react";
import clsx from "clsx";
-import { addMinutes, format, isSameDay, setHours, setMinutes } from "date-fns";
+import dayjs from "dayjs";
import * as React from "react";
-import { usePreferences } from "@/components/preferences/use-preferences";
import { stopPropagation } from "@/utils/stop-propagation";
import ChevronDown from "../../../icons/chevron-down.svg";
@@ -27,9 +26,8 @@ const TimePicker: React.VoidFunctionComponent = ({
value,
onChange,
className,
- startFrom = setMinutes(setHours(value, 0), 0),
+ startFrom,
}) => {
- const { locale } = usePreferences();
const { reference, floating, x, y, strategy, refs } = useFloating({
strategy: "fixed",
middleware: [
@@ -47,10 +45,14 @@ const TimePicker: React.VoidFunctionComponent = ({
],
});
+ const startFromDate = startFrom
+ ? dayjs(startFrom)
+ : dayjs(value).startOf("day");
+
const options: React.ReactNode[] = [];
for (let i = 0; i < 96; i++) {
- const optionValue = addMinutes(startFrom, i * 15);
- if (!isSameDay(value, optionValue)) {
+ const optionValue = startFromDate.add(i * 15, "minutes");
+ if (!optionValue.isSame(value, "day")) {
// we only support event that start and end on the same day for now
// because react-big-calendar does not support events that span days
break;
@@ -61,7 +63,7 @@ const TimePicker: React.VoidFunctionComponent = ({
className={styleMenuItem}
value={optionValue.toISOString()}
>
- {format(optionValue, "p", { locale })}
+ {optionValue.format("LT")}
,
);
}
@@ -77,9 +79,7 @@ const TimePicker: React.VoidFunctionComponent = ({
<>
-
- {format(value, "p", { locale })}
-
+ {dayjs(value).format("LT")}
diff --git a/src/components/forms/poll-options-form/utils.ts b/src/components/forms/poll-options-form/utils.ts
index 741c4d695..dbfa291aa 100644
--- a/src/components/forms/poll-options-form/utils.ts
+++ b/src/components/forms/poll-options-form/utils.ts
@@ -1,9 +1,9 @@
-import { format } from "date-fns";
+import dayjs from "dayjs";
export const formatDateWithoutTz = (date: Date): string => {
- return format(date, "yyyy-MM-dd'T'HH:mm:ss");
+ return dayjs(date).format("YYYY-MM-DDTHH:mm:ss");
};
export const formatDateWithoutTime = (date: Date): string => {
- return format(date, "yyyy-MM-dd");
+ return dayjs(date).format("YYYY-MM-DD");
};
diff --git a/src/components/forms/poll-options-form/week-calendar.tsx b/src/components/forms/poll-options-form/week-calendar.tsx
index 641e6b0a4..a6f36ea29 100644
--- a/src/components/forms/poll-options-form/week-calendar.tsx
+++ b/src/components/forms/poll-options-form/week-calendar.tsx
@@ -1,22 +1,18 @@
import clsx from "clsx";
-import {
- addMinutes,
- differenceInMinutes,
- format,
- getDay,
- parse,
- startOfWeek,
-} from "date-fns";
+import dayjs from "dayjs";
import React from "react";
-import { Calendar, dateFnsLocalizer } from "react-big-calendar";
+import { Calendar } from "react-big-calendar";
import { useMount } from "react-use";
-import { usePreferences } from "@/components/preferences/use-preferences";
-
+import { getDuration } from "../../../utils/date-time-utils";
+import { usePreferences } from "../../preferences/use-preferences";
import DateNavigationToolbar from "./date-navigation-toolbar";
+import dayjsLocalizer from "./dayjs-localizer";
import { DateTimeOption, DateTimePickerProps } from "./types";
import { formatDateWithoutTime, formatDateWithoutTz } from "./utils";
+const localizer = dayjsLocalizer(dayjs);
+
const WeekCalendar: React.VoidFunctionComponent = ({
title,
options,
@@ -28,30 +24,12 @@ const WeekCalendar: React.VoidFunctionComponent = ({
}) => {
const [scrollToTime, setScrollToTime] = React.useState();
+ const { timeFormat } = usePreferences();
useMount(() => {
// Bit of a hack to force rbc to scroll to the right time when we close/open a modal
- setScrollToTime(addMinutes(date, -60));
+ setScrollToTime(dayjs(date).add(-60, "minutes").toDate());
});
- const { weekStartsOn, timeFormat, locale } = usePreferences();
-
- const localizer = React.useMemo(
- () =>
- dateFnsLocalizer({
- format,
- parse,
- startOfWeek: (date: Date | number) =>
- startOfWeek(date, {
- weekStartsOn: weekStartsOn === "monday" ? 1 : 0,
- }),
- getDay,
- locales: {
- default: locale,
- },
- }),
- [locale, weekStartsOn],
- );
-
return (
= ({
);
},
eventWrapper: (props) => {
+ const start = dayjs(props.event.start);
+ const end = dayjs(props.event.end);
return (
= ({
width: `calc(${props.style?.width}%)`,
}}
>
-
{format(props.event.start, "p", { locale })}
-
- {props.event.title}
-
+
{start.format("LT")}
+
{getDuration(start, end)}
);
},
@@ -158,15 +136,15 @@ const WeekCalendar: React.VoidFunctionComponent = ({
)}
>
- {format(date, "E")}
+ {dayjs(date).format("ddd")}
- {format(date, "dd")}
+ {dayjs(date).format("DD")}
);
},
},
timeSlotWrapper: ({ children }) => {
- return {children}
;
+ return {children}
;
},
}}
step={15}
@@ -182,12 +160,14 @@ const WeekCalendar: React.VoidFunctionComponent = ({
};
if (action === "select") {
- const diff = differenceInMinutes(endDate, startDate);
+ const diff = dayjs(endDate).diff(startDate, "minutes");
if (diff < 60 * 24) {
onChangeDuration(diff);
}
} else {
- newEvent.end = formatDateWithoutTz(addMinutes(startDate, duration));
+ newEvent.end = formatDateWithoutTz(
+ dayjs(startDate).add(duration, "minutes").toDate(),
+ );
}
const alreadyExists = options.some(
diff --git a/src/components/headless-date-picker.tsx b/src/components/headless-date-picker.tsx
index 2ff2cf2a6..958c4bd19 100644
--- a/src/components/headless-date-picker.tsx
+++ b/src/components/headless-date-picker.tsx
@@ -1,13 +1,4 @@
-import {
- addDays,
- addMonths,
- format,
- getMonth,
- isSameDay,
- isWeekend,
- startOfMonth,
- startOfWeek,
-} from "date-fns";
+import dayjs from "dayjs";
import React from "react";
interface DayProps {
@@ -45,52 +36,50 @@ export const useHeadlessDatePicker = (
const [localSelection, setSelection] = React.useState([]);
const selection = options?.selection ?? localSelection;
const [localNavigationDate, setNavigationDate] = React.useState(today);
- const navigationDate = options?.date ?? localNavigationDate;
+ const navigationDate = dayjs(options?.date ?? localNavigationDate);
- const firstDayOfMonth = startOfMonth(navigationDate);
- const firstDayOfFirstWeek = startOfWeek(firstDayOfMonth, {
- weekStartsOn: options?.weekStartsOn === "monday" ? 1 : 0,
- });
+ const firstDayOfMonth = navigationDate.startOf("month");
+ const firstDayOfFirstWeek = firstDayOfMonth.startOf("week");
- const currentMonth = getMonth(navigationDate);
+ const currentMonth = navigationDate.get("month");
const days: DayProps[] = [];
const daysOfWeek: string[] = [];
for (let i = 0; i < 7; i++) {
- daysOfWeek.push(format(addDays(firstDayOfFirstWeek, i), "EE"));
+ daysOfWeek.push(firstDayOfFirstWeek.add(i, "days").format("dd"));
}
let reachedEnd = false;
let i = 0;
do {
- const d = addDays(firstDayOfFirstWeek, i);
+ const d = firstDayOfFirstWeek.add(i, "days");
days.push({
- date: d,
- day: format(d, "d"),
- weekend: isWeekend(d),
- outOfMonth: getMonth(d) !== currentMonth,
- today: isSameDay(d, today),
- selected: selection.some((selectedDate) => isSameDay(selectedDate, d)),
+ date: d.toDate(),
+ day: d.format("D"),
+ weekend: d.day() === 0 || d.day() === 6,
+ outOfMonth: d.month() !== currentMonth,
+ today: d.isSame(today, "day"),
+ selected: selection.some((selectedDate) => d.isSame(selectedDate, "day")),
});
i++;
reachedEnd =
- i > 34 && i % 7 === 0 && addDays(d, 1).getMonth() !== currentMonth;
+ i > 34 && i % 7 === 0 && d.add(1, "day").month() !== currentMonth;
} while (reachedEnd === false);
return {
- navigationDate,
- label: format(navigationDate, "MMMM yyyy"),
+ navigationDate: navigationDate.toDate(),
+ label: navigationDate.format("MMMM YYYY"),
next: () => {
- const newDate = startOfMonth(addMonths(navigationDate, 1));
+ const newDate = navigationDate.add(1, "month").startOf("month").toDate();
if (!options?.date) {
setNavigationDate(newDate);
}
options?.onNavigationChange?.(newDate);
},
prev: () => {
- const newDate = startOfMonth(addMonths(navigationDate, -1));
+ const newDate = navigationDate.add(-1, "month").startOf("month").toDate();
if (!options?.date) {
setNavigationDate(newDate);
}
diff --git a/src/components/home/poll-demo.tsx b/src/components/home/poll-demo.tsx
index 3f801ee49..48eb026c8 100644
--- a/src/components/home/poll-demo.tsx
+++ b/src/components/home/poll-demo.tsx
@@ -1,4 +1,4 @@
-import { format } from "date-fns";
+import dayjs from "dayjs";
import { useTranslation } from "next-i18next";
import * as React from "react";
@@ -66,11 +66,11 @@ const PollDemo: React.VoidFunctionComponent = () => {
- {format(d, "E")}
+ {dayjs(d).format("ddd")}
-
{format(d, "dd")}
+
{dayjs(d).format("DD")}
- {format(d, "MMM")}
+ {dayjs(d).format("MMM")}
diff --git a/src/components/poll-context.tsx b/src/components/poll-context.tsx
index fb8dc0356..14e89455a 100644
--- a/src/components/poll-context.tsx
+++ b/src/components/poll-context.tsx
@@ -63,8 +63,6 @@ export const PollContextProvider: React.VoidFunctionComponent<{
const [targetTimeZone, setTargetTimeZone] =
React.useState(getBrowserTimeZone);
- const { locale } = usePreferences();
-
const getScore = React.useCallback(
(optionId: string) => {
return (participants ?? []).reduce(
@@ -88,6 +86,8 @@ export const PollContextProvider: React.VoidFunctionComponent<{
[participants],
);
+ const { timeFormat } = usePreferences();
+
const contextValue = React.useMemo(() => {
const highScore = poll.options.reduce((acc, curr) => {
const score = getScore(curr.id).yes;
@@ -99,7 +99,7 @@ export const PollContextProvider: React.VoidFunctionComponent<{
poll.options,
poll.timeZone,
targetTimeZone,
- locale,
+ timeFormat,
);
const getParticipantById = (participantId: string) => {
// TODO (Luke Vella) [2022-04-16]: Build an index instead
@@ -164,10 +164,10 @@ export const PollContextProvider: React.VoidFunctionComponent<{
admin,
getScore,
isDeleted,
- locale,
participants,
poll,
targetTimeZone,
+ timeFormat,
urlId,
user,
]);
diff --git a/src/components/poll/manage-poll/use-csv-exporter.ts b/src/components/poll/manage-poll/use-csv-exporter.ts
index bb49b2e9b..78dca669c 100644
--- a/src/components/poll/manage-poll/use-csv-exporter.ts
+++ b/src/components/poll/manage-poll/use-csv-exporter.ts
@@ -1,4 +1,4 @@
-import { format } from "date-fns";
+import dayjs from "dayjs";
import { useTranslation } from "next-i18next";
import { usePoll } from "@/components/poll-context";
@@ -50,10 +50,7 @@ export const useCsvExporter = () => {
link.setAttribute("href", encodedCsv);
link.setAttribute(
"download",
- `${poll.title.replace(/\s/g, "_")}-${format(
- Date.now(),
- "yyyyMMddhhmm",
- )}`,
+ `${poll.title.replace(/\s/g, "_")}-${dayjs().format("YYYYMMDDHHmm")}`,
);
document.body.appendChild(link);
link.click();
diff --git a/src/components/poll/poll-subheader.tsx b/src/components/poll/poll-subheader.tsx
index 2586bff75..5e4692514 100644
--- a/src/components/poll/poll-subheader.tsx
+++ b/src/components/poll/poll-subheader.tsx
@@ -1,16 +1,14 @@
-import { formatRelative } from "date-fns";
+import dayjs from "dayjs";
import { Trans, useTranslation } from "next-i18next";
import * as React from "react";
import Badge from "../badge";
import { usePoll } from "../poll-context";
-import { usePreferences } from "../preferences/use-preferences";
import Tooltip from "../tooltip";
const PollSubheader: React.VoidFunctionComponent = () => {
const { poll } = usePoll();
const { t } = useTranslation("app");
- const { locale } = usePreferences();
return (
@@ -45,9 +43,7 @@ const PollSubheader: React.VoidFunctionComponent = () => {
•
- {formatRelative(poll.createdAt, new Date(), {
- locale,
- })}
+ {dayjs(poll.createdAt).fromNow()}
);
diff --git a/src/components/preferences/preferences-provider.tsx b/src/components/preferences/preferences-provider.tsx
index 55ab906bd..f50f04066 100644
--- a/src/components/preferences/preferences-provider.tsx
+++ b/src/components/preferences/preferences-provider.tsx
@@ -1,15 +1,32 @@
-import { Locale } from "date-fns";
-import enGB from "date-fns/locale/en-GB";
-import enUS from "date-fns/locale/en-US";
+import dayjs from "dayjs";
+import en from "dayjs/locale/en";
+import duration from "dayjs/plugin/duration";
+import isBetween from "dayjs/plugin/isBetween";
+import isSameOrBefore from "dayjs/plugin/isSameOrBefore";
+import localeData from "dayjs/plugin/localeData";
+import localizedFormat from "dayjs/plugin/localizedFormat";
+import minMax from "dayjs/plugin/minMax";
+import relativeTime from "dayjs/plugin/relativeTime";
+import timezone from "dayjs/plugin/timezone";
+import utc from "dayjs/plugin/utc";
import * as React from "react";
import { useLocalStorage } from "react-use";
type TimeFormat = "12h" | "24h";
type StartOfWeek = "monday" | "sunday";
+dayjs.extend(localizedFormat);
+dayjs.extend(relativeTime);
+dayjs.extend(localeData);
+dayjs.extend(isSameOrBefore);
+dayjs.extend(isBetween);
+dayjs.extend(minMax);
+dayjs.extend(utc);
+dayjs.extend(timezone);
+dayjs.extend(duration);
+
export const PreferencesContext =
React.createContext<{
- locale: Locale;
weekStartsOn: StartOfWeek;
timeFormat: TimeFormat;
setWeekStartsOn: React.Dispatch<
@@ -29,13 +46,18 @@ const PreferencesProvider: React.VoidFunctionComponent<{
const [timeFormat = "12h", setTimeFormat] =
useLocalStorage("rallly-time-format");
+ dayjs.locale({
+ ...en,
+ weekStart: weekStartsOn === "monday" ? 1 : 0,
+ formats: { LT: timeFormat === "12h" ? "h:mm A" : "HH:mm" },
+ });
+
const contextValue = React.useMemo(
() => ({
weekStartsOn,
timeFormat,
setWeekStartsOn,
setTimeFormat,
- locale: timeFormat === "12h" ? enUS : enGB,
}),
[setTimeFormat, setWeekStartsOn, timeFormat, weekStartsOn],
);
diff --git a/src/components/profile.tsx b/src/components/profile.tsx
index 92faa50f4..555e28060 100644
--- a/src/components/profile.tsx
+++ b/src/components/profile.tsx
@@ -1,4 +1,4 @@
-import { formatRelative } from "date-fns";
+import dayjs from "dayjs";
import Head from "next/head";
import Link from "next/link";
import { useTranslation } from "next-i18next";
@@ -83,7 +83,7 @@ export const Profile: React.VoidFunctionComponent = () => {
- {formatRelative(poll.createdAt, new Date())}
+ {dayjs(poll.createdAt).fromNow()}
diff --git a/src/pages/api/house-keeping.ts b/src/pages/api/house-keeping.ts
index 00799c4a8..0cc2e00b4 100644
--- a/src/pages/api/house-keeping.ts
+++ b/src/pages/api/house-keeping.ts
@@ -1,5 +1,5 @@
import { Prisma } from "@prisma/client";
-import { addDays } from "date-fns";
+import dayjs from "dayjs";
import { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "~/prisma/db";
@@ -29,7 +29,7 @@ export default async function handler(
where: {
deleted: false,
touchedAt: {
- lte: addDays(new Date(), -30),
+ lte: dayjs().add(-30, "days").toDate(),
},
},
});
@@ -42,14 +42,14 @@ export default async function handler(
{
deleted: true,
deletedAt: {
- lte: addDays(new Date(), -7),
+ lte: dayjs().add(-7, "days").toDate(),
},
},
// demo polls that are 1 day old
{
demo: true,
createdAt: {
- lte: addDays(new Date(), -1),
+ lte: dayjs().add(-1, "days").toDate(),
},
},
],
diff --git a/src/server/routers/polls/demo.ts b/src/server/routers/polls/demo.ts
index 4993777c7..864e29d3c 100644
--- a/src/server/routers/polls/demo.ts
+++ b/src/server/routers/polls/demo.ts
@@ -1,5 +1,5 @@
import { VoteType } from "@prisma/client";
-import addMinutes from "date-fns/addMinutes";
+import dayjs from "dayjs";
import { prisma } from "~/prisma/db";
@@ -31,7 +31,6 @@ export const demo = createRouter().mutation("create", {
resolve: async () => {
const adminUrlId = await nanoid();
const demoUser = { name: "John Example", email: "noreply@rallly.co" };
- const today = new Date();
const options: Array<{ value: string; id: string }> = [];
@@ -59,7 +58,9 @@ export const demo = createRouter().mutation("create", {
id: participantId,
name,
userId: "user-demo",
- createdAt: addMinutes(today, i * -1),
+ createdAt: dayjs()
+ .add(i * -1, "minutes")
+ .toDate(),
});
options.forEach((option, index) => {
diff --git a/src/utils/date-time-utils.ts b/src/utils/date-time-utils.ts
index 6b514d591..54df98a42 100644
--- a/src/utils/date-time-utils.ts
+++ b/src/utils/date-time-utils.ts
@@ -1,14 +1,5 @@
import { Option } from "@prisma/client";
-import {
- differenceInHours,
- differenceInMinutes,
- format,
- formatDuration,
- isSameDay,
- Locale,
-} from "date-fns";
-import { formatInTimeZone } from "date-fns-tz";
-import spacetime from "spacetime";
+import dayjs from "dayjs";
import {
DateTimeOption,
@@ -49,19 +40,28 @@ export type ParsedDateTimeOpton = ParsedDateOption | ParsedTimeSlotOption;
const isTimeSlot = (value: string) => value.indexOf("/") !== -1;
-const getDuration = (startTime: Date, endTime: Date) => {
- const hours = Math.floor(differenceInHours(endTime, startTime));
- const minutes = Math.floor(
- differenceInMinutes(endTime, startTime) - hours * 60,
- );
- return formatDuration({ hours, minutes });
+export const getDuration = (startTime: dayjs.Dayjs, endTime: dayjs.Dayjs) => {
+ const hours = Math.floor(endTime.diff(startTime, "hours"));
+ const minutes = Math.floor(endTime.diff(startTime, "minute") - hours * 60);
+ let res = "";
+ if (hours) {
+ res += `${hours}h`;
+ }
+ if (hours && minutes) {
+ res += " ";
+ }
+ if (minutes) {
+ res += `${minutes}m`;
+ }
+ return res;
};
export const decodeOptions = (
options: Option[],
timeZone: string | null,
targetTimeZone: string,
- locale: Locale,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ _timeFormat: string, // TODO (Luke Vella) [2022-06-28]: Need to pass timeFormat so that we recalculate the options when timeFormat changes. There is definitely a better way to do this
):
| { pollType: "date"; options: ParsedDateOption[] }
| { pollType: "timeSlot"; options: ParsedTimeSlotOption[] } => {
@@ -71,7 +71,7 @@ export const decodeOptions = (
return {
pollType,
options: options.map((option) =>
- parseTimeSlotOption(option, timeZone, targetTimeZone, locale),
+ parseTimeSlotOption(option, timeZone, targetTimeZone),
),
};
} else {
@@ -88,14 +88,14 @@ const parseDateOption = (option: Option): ParsedDateOption => {
? // we add the time because otherwise Date will assume UTC time which might change the day for some time zones
option.value + "T00:00:00"
: option.value;
- const date = new Date(dateString);
+ const date = dayjs(dateString);
return {
type: "date",
optionId: option.id,
- day: format(date, "d"),
- dow: format(date, "EEE"),
- month: format(date, "MMM"),
- year: format(date, "yyyy"),
+ day: date.format("D"),
+ dow: date.format("ddd"),
+ month: date.format("MMM"),
+ year: date.format("YYYY"),
};
};
@@ -103,48 +103,29 @@ const parseTimeSlotOption = (
option: Option,
timeZone: string | null,
targetTimeZone: string,
- locale: Locale,
): ParsedTimeSlotOption => {
- const localeFormatInTimezone = (
- date: Date,
- timezone: string,
- formatString: string,
- ) => {
- return formatInTimeZone(date, timezone, formatString, {
- locale,
- });
- };
-
const [start, end] = option.value.split("/");
- if (timeZone && targetTimeZone) {
- const startDate = spacetime(start, timeZone).toNativeDate();
- const endDate = spacetime(end, timeZone).toNativeDate();
- return {
- type: "timeSlot",
- optionId: option.id,
- startTime: localeFormatInTimezone(startDate, targetTimeZone, "p"),
- endTime: localeFormatInTimezone(endDate, targetTimeZone, "p"),
- day: localeFormatInTimezone(startDate, targetTimeZone, "d"),
- dow: localeFormatInTimezone(startDate, targetTimeZone, "EEE"),
- month: localeFormatInTimezone(startDate, targetTimeZone, "MMM"),
- duration: getDuration(startDate, endDate),
- year: localeFormatInTimezone(startDate, targetTimeZone, "yyyy"),
- };
- } else {
- const startDate = new Date(start);
- const endDate = new Date(end);
- return {
- type: "timeSlot",
- optionId: option.id,
- startTime: format(startDate, "p"),
- endTime: format(endDate, "p"),
- day: format(startDate, "d"),
- dow: format(startDate, "E"),
- month: format(startDate, "MMM"),
- duration: getDuration(startDate, endDate),
- year: format(startDate, "yyyy"),
- };
- }
+
+ const startDate =
+ timeZone && targetTimeZone
+ ? dayjs(start).tz(timeZone, true).tz(targetTimeZone)
+ : dayjs(start);
+ const endDate =
+ timeZone && targetTimeZone
+ ? dayjs(end).tz(timeZone, true).tz(targetTimeZone)
+ : dayjs(end);
+
+ return {
+ type: "timeSlot",
+ optionId: option.id,
+ startTime: startDate.format("LT"),
+ endTime: endDate.format("LT"),
+ day: startDate.format("D"),
+ dow: startDate.format("ddd"),
+ month: startDate.format("MMM"),
+ duration: getDuration(startDate, endDate),
+ year: startDate.format("YYYY"),
+ };
};
export const removeAllOptionsForDay = (
@@ -152,18 +133,19 @@ export const removeAllOptionsForDay = (
date: Date,
) => {
return options.filter((option) => {
- const optionDate = spacetime(
+ const optionDate = new Date(
option.type === "date" ? option.date : option.start,
- ).toNativeDate();
- return !isSameDay(date, optionDate);
+ );
+ return !dayjs(date).isSame(optionDate, "day");
});
};
export const getDateProps = (date: Date) => {
+ const d = dayjs(date);
return {
- day: format(date, "d"),
- dow: format(date, "E"),
- month: format(date, "MMM"),
+ day: d.format("D"),
+ dow: d.format("ddd"),
+ month: d.format("MMM"),
};
};
diff --git a/tests/house-keeping.spec.ts b/tests/house-keeping.spec.ts
index 8ad2306e5..dabd6210a 100644
--- a/tests/house-keeping.spec.ts
+++ b/tests/house-keeping.spec.ts
@@ -1,6 +1,6 @@
import { expect, test } from "@playwright/test";
import { Prisma } from "@prisma/client";
-import { addDays } from "date-fns";
+import dayjs from "dayjs";
import { prisma } from "../prisma/db";
@@ -29,7 +29,7 @@ test.beforeAll(async ({ request, baseURL }) => {
type: "date",
userId: "user1",
deleted: true,
- deletedAt: addDays(new Date(), -6),
+ deletedAt: dayjs().add(-6, "days").toDate(),
participantUrlId: "p2",
adminUrlId: "a2",
},
@@ -40,7 +40,7 @@ test.beforeAll(async ({ request, baseURL }) => {
type: "date",
userId: "user1",
deleted: true,
- deletedAt: addDays(new Date(), -7),
+ deletedAt: dayjs().add(-7, "days").toDate(),
participantUrlId: "p3",
adminUrlId: "a3",
},
@@ -50,7 +50,7 @@ test.beforeAll(async ({ request, baseURL }) => {
id: "still-active-poll",
type: "date",
userId: "user1",
- touchedAt: addDays(new Date(), -29),
+ touchedAt: dayjs().add(-29, "days").toDate(),
participantUrlId: "p4",
adminUrlId: "a4",
},
@@ -60,7 +60,7 @@ test.beforeAll(async ({ request, baseURL }) => {
id: "inactive-poll",
type: "date",
userId: "user1",
- touchedAt: addDays(new Date(), -30),
+ touchedAt: dayjs().add(-30, "days").toDate(),
participantUrlId: "p5",
adminUrlId: "a5",
},
@@ -82,7 +82,7 @@ test.beforeAll(async ({ request, baseURL }) => {
id: "demo-poll-old",
type: "date",
userId: "user1",
- createdAt: addDays(new Date(), -2),
+ createdAt: dayjs().add(-2, "days").toDate(),
participantUrlId: "p7",
adminUrlId: "a7",
},
diff --git a/yarn.lock b/yarn.lock
index b655c9ffe..dfb041128 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2527,15 +2527,10 @@ date-arithmetic@^4.1.0:
resolved "https://registry.npmjs.org/date-arithmetic/-/date-arithmetic-4.1.0.tgz"
integrity sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==
-date-fns-tz@^1.2.2:
- version "1.2.2"
- resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.2.2.tgz#89432b54ce3fa7d050a2039e997e5b6a96df35dd"
- integrity sha512-vWtn44eEqnLbkACb7T5G5gPgKR4nY8NkNMOCyoY49NsRGHrcDmY2aysCyzDeA+u+vcDBn/w6nQqEDyouRs4m8w==
-
-date-fns@^2.28.0:
- version "2.28.0"
- resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
- integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
+dayjs@^1.11.3:
+ version "1.11.3"
+ resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.3.tgz#4754eb694a624057b9ad2224b67b15d552589258"
+ integrity sha512-xxwlswWOlGhzgQ4TKzASQkUhqERI3egRNqgV4ScR8wlANA/A9tZ7miXa44vTTKEq5l7vWoL5G57bG3zA+Kow0A==
debug@4:
version "4.3.3"
@@ -5023,10 +5018,10 @@ sourcemap-codec@1.4.8, sourcemap-codec@^1.4.8:
resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
-spacetime@^7.1.2:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/spacetime/-/spacetime-7.1.2.tgz#73a94f0ba7f0c0b2230259b5ccf78dd9fd34cd16"
- integrity sha512-MUTgK9KU9gMXhZddwe0nlgFnCJXT4RfuIRqyo8VcUexZa94zkxk1WpVv3THgvMFz1+Hq9okoNiGTvj6qBdN4Cg==
+spacetime@^7.1.4:
+ version "7.1.4"
+ resolved "https://registry.yarnpkg.com/spacetime/-/spacetime-7.1.4.tgz#f288e4dcecbb0ec56e76c3b2a09c55badc18cebe"
+ integrity sha512-ZzYuGjaMPE42p/fVU9Qcd+aMhgYzC83hgqMKyWlGILtponsLm5KJZgWzblLnOU8OblMQa3YXo/0IiZZ5JsTDfA==
spdx-correct@^3.0.0:
version "3.1.1"