feat: global time range context with custom date picker
Lift time range state into a shared React context so the selected
range persists across page navigation and browser refreshes
(localStorage). Add a "Custom" option with a popover date picker
that lets users specify arbitrary start/end dates. All preset end
times now use endOf("day") (23:59:59) instead of the current moment.
This commit is contained in:
@@ -26,6 +26,10 @@ const translations = {
|
||||
"time.7d": "7 天",
|
||||
"time.30d": "30 天",
|
||||
"time.all": "全部",
|
||||
"time.custom": "自定义",
|
||||
"time.startDate": "开始日期",
|
||||
"time.endDate": "结束日期",
|
||||
"time.confirm": "确认",
|
||||
// granularity
|
||||
"gran.day": "日",
|
||||
"gran.week": "周",
|
||||
@@ -108,6 +112,10 @@ const translations = {
|
||||
"time.7d": "7 Days",
|
||||
"time.30d": "30 Days",
|
||||
"time.all": "All",
|
||||
"time.custom": "Custom",
|
||||
"time.startDate": "Start",
|
||||
"time.endDate": "End",
|
||||
"time.confirm": "Confirm",
|
||||
"gran.day": "Day",
|
||||
"gran.week": "Week",
|
||||
"gran.month": "Month",
|
||||
|
||||
93
lib/time-range-context.tsx
Normal file
93
lib/time-range-context.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext, useState, useCallback, 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: () => ({}),
|
||||
});
|
||||
|
||||
function loadSaved(): { range: TimeRange; customStart: string; customEnd: string } | null {
|
||||
if (typeof window === "undefined") return null;
|
||||
try {
|
||||
const raw = localStorage.getItem("time-range");
|
||||
if (!raw) return null;
|
||||
const parsed = JSON.parse(raw);
|
||||
return {
|
||||
range: parsed.range || "30d",
|
||||
customStart: parsed.customStart || "",
|
||||
customEnd: parsed.customEnd || "",
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function persist(range: TimeRange, customStart: string, customEnd: string) {
|
||||
localStorage.setItem("time-range", JSON.stringify({ range, customStart, customEnd }));
|
||||
}
|
||||
|
||||
export function TimeRangeProvider({ children }: { children: ReactNode }) {
|
||||
const [range, setRangeState] = useState<TimeRange>(() => loadSaved()?.range ?? "30d");
|
||||
const [customStart, setCustomStartState] = useState(() => loadSaved()?.customStart || dayjs().subtract(7, "day").format("YYYY-MM-DD"));
|
||||
const [customEnd, setCustomEndState] = useState(() => loadSaved()?.customEnd || dayjs().format("YYYY-MM-DD"));
|
||||
|
||||
const setRange = useCallback((r: TimeRange) => {
|
||||
setRangeState(r);
|
||||
setCustomStartState(prev => { persist(r, prev, customEnd); return prev; });
|
||||
}, [customEnd]);
|
||||
|
||||
const setCustomStart = useCallback((s: string) => {
|
||||
setCustomStartState(s);
|
||||
persist(range, s, customEnd);
|
||||
}, [range, customEnd]);
|
||||
|
||||
const setCustomEnd = useCallback((e: string) => {
|
||||
setCustomEndState(e);
|
||||
persist(range, customStart, e);
|
||||
}, [range, customStart]);
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -27,13 +27,14 @@ export type TimeRange = "today" | "7d" | "30d" | "all" | "custom";
|
||||
|
||||
export function getTimeRange(range: TimeRange): { start?: number; end?: number } {
|
||||
const now = dayjs();
|
||||
const end = now.endOf("day").unix();
|
||||
switch (range) {
|
||||
case "today":
|
||||
return { start: now.startOf("day").unix(), end: now.unix() };
|
||||
return { start: now.startOf("day").unix(), end };
|
||||
case "7d":
|
||||
return { start: now.subtract(7, "day").startOf("day").unix(), end: now.unix() };
|
||||
return { start: now.subtract(7, "day").startOf("day").unix(), end };
|
||||
case "30d":
|
||||
return { start: now.subtract(30, "day").startOf("day").unix(), end: now.unix() };
|
||||
return { start: now.subtract(30, "day").startOf("day").unix(), end };
|
||||
case "all":
|
||||
return {};
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user