Fixes TypeScript strict mode error where callback parameters implicitly had 'any' type.
119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { createContext, useContext, useState, useCallback, useSyncExternalStore, type ReactNode } from "react";
|
|
import dayjs from "dayjs";
|
|
import { type TimeRange, getTimeRange } 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<TimeRangeContextType>({
|
|
range: "30d",
|
|
setRange: () => {},
|
|
customStart: "",
|
|
customEnd: "",
|
|
setCustomStart: () => {},
|
|
setCustomEnd: () => {},
|
|
getEffectiveRange: () => ({}),
|
|
});
|
|
|
|
// Use useSyncExternalStore to safely read localStorage without hydration mismatch
|
|
const STORAGE_KEY = "time-range";
|
|
const DEFAULT_RANGE = "30d";
|
|
|
|
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();
|
|
}
|
|
|
|
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 [range, setRangeState] = useState<TimeRange>(saved?.range ?? DEFAULT_RANGE);
|
|
const [customStart, setCustomStartState] = useState(
|
|
saved?.customStart || dayjs().subtract(7, "day").format("YYYY-MM-DD")
|
|
);
|
|
const [customEnd, setCustomEndState] = useState(
|
|
saved?.customEnd || dayjs().format("YYYY-MM-DD")
|
|
);
|
|
|
|
const setRange = useCallback((r: TimeRange) => {
|
|
setRangeState(r);
|
|
setCustomStartState((prev: string) => {
|
|
setCustomEndState((end: string) => { persist(r, prev, end); return end; });
|
|
return prev;
|
|
});
|
|
}, []);
|
|
|
|
const setCustomStart = useCallback((s: string) => {
|
|
setCustomStartState(s);
|
|
setRangeState((r: TimeRange) => {
|
|
setCustomEndState((end: string) => { persist(r, s, end); return end; });
|
|
return r;
|
|
});
|
|
}, []);
|
|
|
|
const setCustomEnd = useCallback((e: string) => {
|
|
setCustomEndState(e);
|
|
setRangeState((r: TimeRange) => {
|
|
setCustomStartState((start: string) => { persist(r, start, e); return start; });
|
|
return r;
|
|
});
|
|
}, []);
|
|
|
|
const getEffectiveRange = useCallback(() => {
|
|
if (range === "custom") {
|
|
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;
|
|
}
|
|
return getTimeRange(range);
|
|
}, [range, customStart, customEnd]);
|
|
|
|
return (
|
|
<TimeRangeContext.Provider value={{
|
|
range, setRange,
|
|
customStart, customEnd,
|
|
setCustomStart,
|
|
setCustomEnd,
|
|
getEffectiveRange,
|
|
}}>
|
|
{children}
|
|
</TimeRangeContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useTimeRange() {
|
|
return useContext(TimeRangeContext);
|
|
}
|