"use client"; import { createContext, useContext, useState, useCallback, useRef, useEffect, useSyncExternalStore, type ReactNode } from "react"; import dayjs from "dayjs"; import { type TimeRange } from "@/lib/utils"; interface TimeRangeContextType { range: TimeRange; setRange: (r: TimeRange) => void; customStart: string; customEnd: string; setCustomStart: (s: string) => void; setCustomEnd: (s: string) => void; /** Returns { start?, end? } unix timestamps (seconds) ready for API calls */ getEffectiveRange: () => { start?: number; end?: number }; } const TimeRangeContext = createContext({ range: "7d", setRange: () => {}, customStart: "", customEnd: "", setCustomStart: () => {}, setCustomEnd: () => {}, getEffectiveRange: () => ({}), }); const STORAGE_KEY = "time-range"; const DEFAULT_RANGE: TimeRange = "7d"; let listeners: Array<() => void> = []; function subscribe(cb: () => void) { listeners = [...listeners, cb]; return () => { listeners = listeners.filter(l => l !== cb); }; } function emitChange() { for (const l of listeners) l(); } function getSnapshot(): string { return localStorage.getItem(STORAGE_KEY) ?? ""; } function getServerSnapshot(): string { return ""; } function persist(range: TimeRange, customStart: string, customEnd: string) { localStorage.setItem(STORAGE_KEY, JSON.stringify({ range, customStart, customEnd })); emitChange(); } /** Convert a preset to concrete date strings */ function presetDates(preset: TimeRange): { start: string; end: string } { const today = dayjs().format("YYYY-MM-DD"); switch (preset) { case "today": return { start: today, end: today }; case "7d": return { start: dayjs().subtract(6, "day").format("YYYY-MM-DD"), end: today }; case "30d": return { start: dayjs().subtract(29, "day").format("YYYY-MM-DD"), end: today }; default: return { start: today, end: today }; } } function initDates(saved: { range?: TimeRange; customStart?: string; customEnd?: string } | null) { if (saved?.customStart && saved?.customEnd) { return { start: saved.customStart, end: saved.customEnd }; } const r = (saved?.range as TimeRange) ?? DEFAULT_RANGE; if (r === "all") return { start: "", end: "" }; return presetDates(r); } export function TimeRangeProvider({ children }: { children: ReactNode }) { const raw = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); const saved = (() => { if (!raw) return null; try { return JSON.parse(raw); } catch { return null; } })(); const init = initDates(saved); const [range, setRangeState] = useState(saved?.range ?? DEFAULT_RANGE); const [customStart, setCustomStartState] = useState(init.start); const [customEnd, setCustomEndState] = useState(init.end); const rangeRef = useRef(range); const customStartRef = useRef(customStart); const customEndRef = useRef(customEnd); useEffect(() => { rangeRef.current = range; }, [range]); useEffect(() => { customStartRef.current = customStart; }, [customStart]); useEffect(() => { customEndRef.current = customEnd; }, [customEnd]); // Fetch actual date boundaries when "all" is selected (including on mount) const fetchDateRange = useCallback(async () => { try { const res = await fetch("/api/date-range"); const { minDate, maxDate } = await res.json(); if (minDate && maxDate) { setCustomStartState(minDate); setCustomEndState(maxDate); persist("all", minDate, maxDate); } } catch { /* ignore */ } }, []); // On mount: if saved range is "all", fetch real boundaries useEffect(() => { if (range === "all") fetchDateRange(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const setRange = useCallback((r: TimeRange) => { setRangeState(r); if (r === "all") { // Query first, then backfill dates fetchDateRange(); } else if (r !== "custom") { const { start, end } = presetDates(r); setCustomStartState(start); setCustomEndState(end); persist(r, start, end); } else { persist(r, customStartRef.current, customEndRef.current); } }, [fetchDateRange]); const setCustomStart = useCallback((s: string) => { setCustomStartState(s); persist(rangeRef.current, s, customEndRef.current); }, []); const setCustomEnd = useCallback((e: string) => { setCustomEndState(e); persist(rangeRef.current, customStartRef.current, e); }, []); // Always derive timestamps from customStart/customEnd — single source of truth const getEffectiveRange = useCallback(() => { // "all" with dates not yet loaded → no filter if (range === "all" && !customStart && !customEnd) return {}; const result: { start?: number; end?: number } = {}; if (customStart) result.start = dayjs(customStart).startOf("day").unix(); if (customEnd) result.end = dayjs(customEnd).endOf("day").unix(); return result; }, [range, customStart, customEnd]); return ( {children} ); } export function useTimeRange() { return useContext(TimeRangeContext); }