diff --git a/web/src/components/filter/CalendarFilterButton.tsx b/web/src/components/filter/CalendarFilterButton.tsx index f16368d63..876eb9ab0 100644 --- a/web/src/components/filter/CalendarFilterButton.tsx +++ b/web/src/components/filter/CalendarFilterButton.tsx @@ -1,6 +1,7 @@ import { useFormattedRange, useFormattedTimestamp, + useTimezone, } from "@/hooks/use-date-utils"; import { RecordingsSummary, ReviewSummary } from "@/types/review"; import { Button } from "../ui/button"; @@ -15,6 +16,8 @@ import { DateRange } from "react-day-picker"; import { useState } from "react"; import PlatformAwareDialog from "../overlay/dialog/PlatformAwareDialog"; import { useTranslation } from "react-i18next"; +import useSWR from "swr"; +import { FrigateConfig } from "@/types/frigateConfig"; type CalendarFilterButtonProps = { reviewSummary?: ReviewSummary; @@ -29,10 +32,12 @@ export default function CalendarFilterButton({ updateSelectedDay, }: CalendarFilterButtonProps) { const { t } = useTranslation(["components/filter", "views/events"]); + const { data: config } = useSWR("config"); const [open, setOpen] = useState(false); const selectedDate = useFormattedTimestamp( day == undefined ? 0 : day?.getTime() / 1000 + 1, t("time.formattedTimestampMonthDay", { ns: "common" }), + config?.ui.timezone, ); const trigger = ( @@ -98,12 +103,15 @@ export function CalendarRangeFilterButton({ updateSelectedRange, }: CalendarRangeFilterButtonProps) { const { t } = useTranslation(["components/filter"]); + const { data: config } = useSWR("config"); + const timezone = useTimezone(config); const [open, setOpen] = useState(false); const selectedDate = useFormattedRange( range?.from == undefined ? 0 : range.from.getTime() / 1000 + 1, range?.to == undefined ? 0 : range.to.getTime() / 1000 - 1, t("time.formattedTimestampMonthDay", { ns: "common" }), + config?.ui.timezone, ); const trigger = ( @@ -128,6 +136,7 @@ export function CalendarRangeFilterButton({ { updateSelectedRange(range.range); diff --git a/web/src/components/overlay/ExportDialog.tsx b/web/src/components/overlay/ExportDialog.tsx index 44b55bfe3..b1b9b9dc2 100644 --- a/web/src/components/overlay/ExportDialog.tsx +++ b/web/src/components/overlay/ExportDialog.tsx @@ -406,12 +406,14 @@ function CustomTimeSelector({ config?.ui.time_format == "24hour" ? t("time.formattedTimestamp.24hour", { ns: "common" }) : t("time.formattedTimestamp.12hour", { ns: "common" }), + config?.ui.timezone, ); const formattedEnd = useFormattedTimestamp( endTime, config?.ui.time_format == "24hour" ? t("time.formattedTimestamp.24hour", { ns: "common" }) : t("time.formattedTimestamp.12hour", { ns: "common" }), + config?.ui.timezone, ); const startClock = useMemo(() => { diff --git a/web/src/components/overlay/ReviewActivityCalendar.tsx b/web/src/components/overlay/ReviewActivityCalendar.tsx index 9988d0904..a0ffd0528 100644 --- a/web/src/components/overlay/ReviewActivityCalendar.tsx +++ b/web/src/components/overlay/ReviewActivityCalendar.tsx @@ -3,10 +3,13 @@ import { Calendar } from "../ui/calendar"; import { ButtonHTMLAttributes, useEffect, useMemo, useRef } from "react"; import { FaCircle } from "react-icons/fa"; import { getUTCOffset } from "@/utils/dateUtil"; -import { type DayButtonProps } from "react-day-picker"; +import { type DayButtonProps, TZDate } from "react-day-picker"; import { LAST_24_HOURS_KEY } from "@/types/filter"; import { usePersistence } from "@/hooks/use-persistence"; import { cn } from "@/lib/utils"; +import { FrigateConfig } from "@/types/frigateConfig"; +import useSWR from "swr"; +import { useTimezone } from "@/hooks/use-date-utils"; type WeekStartsOnType = 0 | 1 | 2 | 3 | 4 | 5 | 6; @@ -22,6 +25,8 @@ export default function ReviewActivityCalendar({ selectedDay, onSelect, }: ReviewActivityCalendarProps) { + const { data: config } = useSWR("config"); + const timezone = useTimezone(config); const [weekStartsOn] = usePersistence("weekStartsOn", 0); const disabledDates = useMemo(() => { @@ -45,7 +50,7 @@ export default function ReviewActivityCalendar({ } const parts = date.split("-"); - const cal = new Date(date); + const cal = new TZDate(date, timezone); cal.setFullYear( parseInt(parts[0]), @@ -65,7 +70,7 @@ export default function ReviewActivityCalendar({ } const parts = date.split("-"); - const cal = new Date(date); + const cal = new TZDate(date, timezone); cal.setFullYear( parseInt(parts[0]), @@ -82,7 +87,7 @@ export default function ReviewActivityCalendar({ } return { alerts, detections, recordings }; - }, [reviewSummary, recordingsSummary]); + }, [reviewSummary, recordingsSummary, timezone]); return ( ); } diff --git a/web/src/components/ui/calendar-range.tsx b/web/src/components/ui/calendar-range.tsx index 218cc79bb..f439cb082 100644 --- a/web/src/components/ui/calendar-range.tsx +++ b/web/src/components/ui/calendar-range.tsx @@ -12,6 +12,7 @@ import { import { Switch } from "./switch"; import { cn } from "@/lib/utils"; import { LuCheck } from "react-icons/lu"; +import { TZDate } from "react-day-picker"; import { t } from "i18next"; export interface DateRangePickerProps { @@ -32,19 +33,24 @@ export interface DateRangePickerProps { locale?: string; /** Option for showing compare feature */ showCompare?: boolean; + /** timezone */ + timezone?: string; } -const getDateAdjustedForTimezone = (dateInput: Date | string): Date => { +const getDateAdjustedForTimezone = ( + dateInput: Date | string, + timezone?: string, +): Date => { if (typeof dateInput === "string") { // Split the date string to get year, month, and day parts const parts = dateInput.split("-").map((part) => parseInt(part, 10)); // Create a new Date object using the local timezone // Note: Month is 0-indexed, so subtract 1 from the month part - const date = new Date(parts[0], parts[1] - 1, parts[2]); + const date = new TZDate(parts[0], parts[1] - 1, parts[2], timezone); return date; } else { // If dateInput is already a Date object, return it directly - return dateInput; + return new TZDate(dateInput, timezone); } }; @@ -73,7 +79,12 @@ const PRESETS: Preset[] = [ /** The DateRangePicker component allows a user to select a range of dates */ export function DateRangePicker({ - initialDateFrom = new Date(new Date().setHours(0, 0, 0, 0)), + timezone, + initialDateFrom = (() => { + const date = new TZDate(new Date(), timezone); + date.setHours(0, 0, 0, 0); + return date; + })(), initialDateTo, initialCompareFrom, initialCompareTo, @@ -84,18 +95,27 @@ export function DateRangePicker({ const [isOpen, setIsOpen] = useState(false); const [range, setRange] = useState({ - from: getDateAdjustedForTimezone(initialDateFrom), + from: getDateAdjustedForTimezone(initialDateFrom, timezone), to: initialDateTo - ? getDateAdjustedForTimezone(initialDateTo) - : getDateAdjustedForTimezone(initialDateFrom), + ? getDateAdjustedForTimezone(initialDateTo, timezone) + : getDateAdjustedForTimezone(initialDateFrom, timezone), }); const [rangeCompare, setRangeCompare] = useState( initialCompareFrom ? { - from: new Date(new Date(initialCompareFrom).setHours(0, 0, 0, 0)), + from: new TZDate( + new Date(initialCompareFrom).setHours(0, 0, 0, 0), + timezone, + ), to: initialCompareTo - ? new Date(new Date(initialCompareTo).setHours(0, 0, 0, 0)) - : new Date(new Date(initialCompareFrom).setHours(0, 0, 0, 0)), + ? new TZDate( + new Date(initialCompareTo).setHours(0, 0, 0, 0), + timezone, + ) + : new TZDate( + new Date(initialCompareFrom).setHours(0, 0, 0, 0), + timezone, + ), } : undefined, ); @@ -128,8 +148,8 @@ export function DateRangePicker({ const getPresetRange = (presetName: string): DateRange => { const preset = PRESETS.find(({ name }) => name === presetName); if (!preset) throw new Error(`Unknown date range preset: ${presetName}`); - const from = new Date(); - const to = new Date(); + const from = new TZDate(new Date(), timezone); + const to = new TZDate(new Date(), timezone); const first = from.getDate() - from.getDay(); switch (preset.name) { @@ -191,16 +211,18 @@ export function DateRangePicker({ setRange(range); if (rangeCompare) { const rangeCompare = { - from: new Date( + from: new TZDate( range.from.getFullYear() - 1, range.from.getMonth(), range.from.getDate(), + timezone, ), to: range.to - ? new Date( + ? new TZDate( range.to.getFullYear() - 1, range.to.getMonth(), range.to.getDate(), + timezone, ) : undefined, }; @@ -212,16 +234,18 @@ export function DateRangePicker({ for (const preset of PRESETS) { const presetRange = getPresetRange(preset.name); - const normalizedRangeFrom = new Date(range.from); + const normalizedRangeFrom = new TZDate(range.from, timezone); normalizedRangeFrom.setHours(0, 0, 0, 0); - const normalizedPresetFrom = new Date( + const normalizedPresetFrom = new TZDate( presetRange.from.setHours(0, 0, 0, 0), + timezone, ); - const normalizedRangeTo = new Date(range.to ?? 0); + const normalizedRangeTo = new TZDate(range.to ?? new Date(0), timezone); normalizedRangeTo.setHours(0, 0, 0, 0); - const normalizedPresetTo = new Date( + const normalizedPresetTo = new TZDate( presetRange.to?.setHours(0, 0, 0, 0) ?? 0, + timezone, ); if ( @@ -401,6 +425,7 @@ export function DateRangePicker({ ), ) } + timeZone={timezone} /> diff --git a/web/src/hooks/use-date-utils.ts b/web/src/hooks/use-date-utils.ts index 5fa8dd585..dbe3085d4 100644 --- a/web/src/hooks/use-date-utils.ts +++ b/web/src/hooks/use-date-utils.ts @@ -21,22 +21,29 @@ export function useFormattedTimestamp( return formattedTimestamp; } -export function useFormattedRange(start: number, end: number, format: string) { +export function useFormattedRange( + start: number, + end: number, + format: string, + timezone?: string, +) { const locale = useDateLocale(); const formattedStart = useMemo(() => { return formatUnixTimestampToDateTime(start, { + timezone, date_format: format, locale, }); - }, [format, start, locale]); + }, [format, start, timezone, locale]); const formattedEnd = useMemo(() => { return formatUnixTimestampToDateTime(end, { + timezone, date_format: format, locale, }); - }, [format, end, locale]); + }, [format, end, timezone, locale]); return `${formattedStart} - ${formattedEnd}`; }