diff --git a/web/package-lock.json b/web/package-lock.json index cd09b79811..7e76155eb6 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -73,7 +73,7 @@ "react-markdown": "^9.0.1", "react-router-dom": "^6.30.3", "react-swipeable": "^7.0.2", - "react-zoom-pan-pinch": "^3.7.0", + "react-zoom-pan-pinch": "3.6.1", "remark-gfm": "^4.0.0", "scroll-into-view-if-needed": "^3.1.0", "sonner": "^2.0.7", @@ -12354,9 +12354,9 @@ } }, "node_modules/react-zoom-pan-pinch": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.7.0.tgz", - "integrity": "sha512-UmReVZ0TxlKzxSbYiAj+LeGRW8s8LraAFTXRAxzMYnNRgGPsxCudwZKVkjvGmjtx7SW/hZamt69NUmGf4xrkXA==", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/react-zoom-pan-pinch/-/react-zoom-pan-pinch-3.6.1.tgz", + "integrity": "sha512-SdPqdk7QDSV7u/WulkFOi+cnza8rEZ0XX4ZpeH7vx3UZEg7DoyuAy3MCmm+BWv/idPQL2Oe73VoC0EhfCN+sZQ==", "license": "MIT", "engines": { "node": ">=8", diff --git a/web/package.json b/web/package.json index 7d22dc7183..08d167150d 100644 --- a/web/package.json +++ b/web/package.json @@ -87,7 +87,7 @@ "react-markdown": "^9.0.1", "react-router-dom": "^6.30.3", "react-swipeable": "^7.0.2", - "react-zoom-pan-pinch": "^3.7.0", + "react-zoom-pan-pinch": "3.6.1", "remark-gfm": "^4.0.0", "scroll-into-view-if-needed": "^3.1.0", "sonner": "^2.0.7", diff --git a/web/src/components/overlay/ReviewActivityCalendar.tsx b/web/src/components/overlay/ReviewActivityCalendar.tsx index e925467db9..434d6a4759 100644 --- a/web/src/components/overlay/ReviewActivityCalendar.tsx +++ b/web/src/components/overlay/ReviewActivityCalendar.tsx @@ -20,6 +20,26 @@ function formatCalendarDay(day: Date): string { return `${y}-${m}-${d}`; } +function getTodayInTimezone(timezone?: string): { + year: number; + month: number; + day: number; + offset: number; +} { + const now = new Date(); + const offset = Math.round(getUTCOffset(now, timezone)); + + // shifting by the offset makes the UTC getters read the timezone's wall clock + const wallClock = new Date(now.getTime() + offset * 60000); + + return { + year: wallClock.getUTCFullYear(), + month: wallClock.getUTCMonth(), + day: wallClock.getUTCDate(), + offset, + }; +} + type ReviewActivityCalendarProps = { reviewSummary?: ReviewSummary; recordingsSummary?: RecordingsSummary; @@ -37,12 +57,14 @@ export default function ReviewActivityCalendar({ const [weekStartsOn] = useUserPersistence("weekStartsOn", 0); const disabledDates = useMemo(() => { - const tomorrow = new Date(); - tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0); - const future = new Date(); - future.setFullYear(tomorrow.getFullYear() + 10); - return { from: tomorrow, to: future }; - }, []); + // day cells are TZDate in `timezone`, so the cutoff must be a real instant + const { year, month, day, offset } = getTodayInTimezone(timezone); + // midday: ranges match by calendar day, so this dodges DST edges + const from = new Date(Date.UTC(year, month, day + 1, 12) - offset * 60000); + const to = new Date(from); + to.setFullYear(from.getFullYear() + 10); + return { from, to }; + }, [timezone]); const modifiers = useMemo(() => { const recordingsSet = new Set(); @@ -182,48 +204,25 @@ export function TimezoneAwareCalendar({ }; }, [recordingsSummary]); - const timezoneOffset = useMemo( - () => - timezone ? Math.round(getUTCOffset(new Date(), timezone)) : undefined, + // callers pre-shift dates so the local clock reads `timezone`, so boundaries + // are built in local time rather than as instants + const { year, month, day } = useMemo( + () => getTodayInTimezone(timezone), [timezone], ); + const disabledDates = useMemo(() => { - const tomorrow = new Date(); + // midday: ranges match by calendar day, so this dodges DST edges + const from = new Date(year, month, day + 1, 12); + const to = new Date(from); + to.setFullYear(from.getFullYear() + 10); + return { from, to }; + }, [year, month, day]); - if (timezoneOffset) { - tomorrow.setHours( - tomorrow.getHours() + 24, - tomorrow.getMinutes() + timezoneOffset, - 0, - 0, - ); - } else { - tomorrow.setHours(tomorrow.getHours() + 24, -1, 0, 0); - } - - const future = new Date(); - future.setFullYear(tomorrow.getFullYear() + 10); - return { from: tomorrow, to: future }; - }, [timezoneOffset]); - - const today = useMemo(() => { - if (!timezoneOffset) { - return undefined; - } - - const date = new Date(); - const utc = Date.UTC( - date.getUTCFullYear(), - date.getUTCMonth(), - date.getUTCDate(), - date.getUTCHours(), - date.getUTCMinutes(), - date.getUTCSeconds(), - ); - const todayUtc = new Date(utc); - todayUtc.setMinutes(todayUtc.getMinutes() + timezoneOffset, 0, 0); - return todayUtc; - }, [timezoneOffset]); + const today = useMemo( + () => new Date(year, month, day, 12), + [year, month, day], + ); return (