From 5b5606cb8a5013c8c329e27367071a48adf3ebc6 Mon Sep 17 00:00:00 2001 From: Nicolas Mowen Date: Sat, 30 Mar 2024 13:07:30 -0600 Subject: [PATCH] Make export date/time respect configured timezone in config (#10750) * Make export page timezone aware * Fix changeover --- web/src/components/overlay/ExportDialog.tsx | 49 +++++++++++--- .../overlay/ReviewActivityCalendar.tsx | 66 +++++++++++++++++++ web/src/utils/dateUtil.ts | 2 +- 3 files changed, 107 insertions(+), 10 deletions(-) diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index b4b0ae65d..a9d597f83 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -20,11 +20,12 @@ import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import useSWR from "swr"; import { FrigateConfig } from "@/types/frigateConfig"; import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover"; -import ReviewActivityCalendar from "./ReviewActivityCalendar"; +import { TimezoneAwareCalendar } from "./ReviewActivityCalendar"; import { SelectSeparator } from "../ui/select"; import { isDesktop } from "react-device-detect"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import SaveExportOverlay from "./SaveExportOverlay"; +import { getUTCOffset } from "@/utils/dateUtil"; const EXPORT_OPTIONS = [ "1", @@ -305,14 +306,42 @@ function CustomTimeSelector({ // times - const startTime = useMemo( - () => range?.after || latestTime - 3600, - [range, latestTime], + const timezoneOffset = useMemo( + () => + config?.ui.timezone + ? Math.round(getUTCOffset(new Date(), config.ui.timezone)) + : undefined, + [config?.ui.timezone], ); - const endTime = useMemo( - () => range?.before || latestTime, - [range, latestTime], + const localTimeOffset = useMemo( + () => + Math.round( + getUTCOffset( + new Date(), + Intl.DateTimeFormat().resolvedOptions().timeZone, + ), + ), + [], ); + + const startTime = useMemo(() => { + let time = range?.after || latestTime - 3600; + + if (timezoneOffset) { + time = time + (timezoneOffset - localTimeOffset) * 60; + } + + return time; + }, [range, latestTime, timezoneOffset, localTimeOffset]); + const endTime = useMemo(() => { + let time = range?.before || latestTime; + + if (timezoneOffset) { + time = time + (timezoneOffset - localTimeOffset) * 60; + } + + return time; + }, [range, latestTime, timezoneOffset, localTimeOffset]); const formattedStart = useFormattedTimestamp( startTime, config?.ui.time_format == "24hour" @@ -367,7 +396,8 @@ function CustomTimeSelector({ - { if (!day) { @@ -428,7 +458,8 @@ function CustomTimeSelector({ - { if (!day) { diff --git a/web/src/components/overlay/ReviewActivityCalendar.tsx b/web/src/components/overlay/ReviewActivityCalendar.tsx index 009339fc8..7ddc528e4 100644 --- a/web/src/components/overlay/ReviewActivityCalendar.tsx +++ b/web/src/components/overlay/ReviewActivityCalendar.tsx @@ -2,6 +2,7 @@ import { ReviewSummary } from "@/types/review"; import { Calendar } from "../ui/calendar"; import { useMemo } from "react"; import { FaCircle } from "react-icons/fa"; +import { getUTCOffset } from "@/utils/dateUtil"; type ReviewActivityCalendarProps = { reviewSummary?: ReviewSummary; @@ -76,3 +77,68 @@ function ReviewActivityDay({ reviewSummary, day }: ReviewActivityDayProps) { ); } + +type TimezoneAwareCalendarProps = { + timezone?: string; + selectedDay?: Date; + onSelect: (day?: Date) => void; +}; +export function TimezoneAwareCalendar({ + timezone, + selectedDay, + onSelect, +}: TimezoneAwareCalendarProps) { + const timezoneOffset = useMemo( + () => + timezone ? Math.round(getUTCOffset(new Date(), timezone)) : undefined, + [timezone], + ); + const disabledDates = useMemo(() => { + const tomorrow = new Date(); + + 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]); + + return ( + + ); +} diff --git a/web/src/utils/dateUtil.ts b/web/src/utils/dateUtil.ts index 8d8b7dfbf..5eb7ea760 100644 --- a/web/src/utils/dateUtil.ts +++ b/web/src/utils/dateUtil.ts @@ -235,7 +235,7 @@ export const getDurationFromTimestamps = ( * @param timezone string representation of the timezone the user is requesting * @returns number of minutes offset from UTC */ -const getUTCOffset = (date: Date, timezone: string): number => { +export const getUTCOffset = (date: Date, timezone: string): number => { // If timezone is in UTC±HH:MM format, parse it to get offset const utcOffsetMatch = timezone.match(/^UTC([+-])(\d{2}):(\d{2})$/); if (utcOffsetMatch) {