mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
Add an `<AppSettingsProvider>` that fetches `bugReportEnabled` once via `/api/bugreport/status` and exposes it through `useAppSettings()`. The sidebar consumes the flag and conditionally renders the Bug Report entry — both in the collapsed icon mode and the expanded label mode. The bottom-nav and topbar do not surface bugreport today, so no changes were needed there. The provider mounts inside `Providers` (above the toaster), so any future flag-gated UI can reuse the same hook without re-fetching the status payload. Defaults to `true` while the query is loading or the user is unauthenticated so the entry doesn't flicker out under the cursor on slow connections. Tests: SSR-render `<SidebarNav>` with both flag states and assert the `/bugreport` href appears / disappears accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
139 lines
4.0 KiB
TypeScript
139 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
import { I18nProvider } from "@/lib/i18n/context";
|
|
import type { Locale } from "@/lib/i18n/config";
|
|
import { Toaster } from "@/components/ui/sonner";
|
|
import { AppSettingsProvider } from "@/components/app-settings-provider";
|
|
|
|
// ── Theme Context ────────────────────────────────────
|
|
|
|
type Theme = "dark" | "light" | "system";
|
|
|
|
interface ThemeContextValue {
|
|
theme: Theme;
|
|
resolvedTheme: "dark" | "light";
|
|
setTheme: (theme: Theme) => void;
|
|
}
|
|
|
|
const ThemeContext = createContext<ThemeContextValue>({
|
|
theme: "system",
|
|
resolvedTheme: "dark",
|
|
setTheme: () => {},
|
|
});
|
|
|
|
export function useTheme() {
|
|
return useContext(ThemeContext);
|
|
}
|
|
|
|
function getSystemTheme(): "dark" | "light" {
|
|
if (typeof window === "undefined") return "dark";
|
|
return window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
? "dark"
|
|
: "light";
|
|
}
|
|
|
|
function applyTheme(resolved: "dark" | "light") {
|
|
document.documentElement.classList.remove("dark", "light");
|
|
document.documentElement.classList.add(resolved);
|
|
}
|
|
|
|
function ThemeProvider({ children }: { children: ReactNode }) {
|
|
const [theme, setThemeState] = useState<Theme>(() => {
|
|
if (typeof window === "undefined") return "system";
|
|
const saved = localStorage.getItem("healthlog-theme") as Theme | null;
|
|
return saved === "light" || saved === "dark" ? saved : "system";
|
|
});
|
|
|
|
const [resolvedTheme, setResolvedTheme] = useState<"dark" | "light">(() => {
|
|
if (typeof window === "undefined") return "dark";
|
|
const saved = localStorage.getItem("healthlog-theme") as Theme | null;
|
|
const t = saved === "light" || saved === "dark" ? saved : "system";
|
|
return t === "system" ? getSystemTheme() : t;
|
|
});
|
|
|
|
// Apply theme on mount (inline script handles FOUC, this ensures classes are in sync)
|
|
useEffect(() => {
|
|
applyTheme(resolvedTheme);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
// Listen for OS preference changes when in system mode
|
|
useEffect(() => {
|
|
if (theme !== "system") return;
|
|
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
const handler = (e: MediaQueryListEvent) => {
|
|
const resolved = e.matches ? "dark" : "light";
|
|
setResolvedTheme(resolved);
|
|
applyTheme(resolved);
|
|
};
|
|
mq.addEventListener("change", handler);
|
|
return () => mq.removeEventListener("change", handler);
|
|
}, [theme]);
|
|
|
|
const setTheme = useCallback((next: Theme) => {
|
|
setThemeState(next);
|
|
if (next === "system") {
|
|
localStorage.removeItem("healthlog-theme");
|
|
const resolved = getSystemTheme();
|
|
setResolvedTheme(resolved);
|
|
applyTheme(resolved);
|
|
} else {
|
|
localStorage.setItem("healthlog-theme", next);
|
|
setResolvedTheme(next);
|
|
applyTheme(next);
|
|
}
|
|
}, []);
|
|
|
|
return (
|
|
<ThemeContext.Provider value={{ theme, resolvedTheme, setTheme }}>
|
|
{children}
|
|
</ThemeContext.Provider>
|
|
);
|
|
}
|
|
|
|
// ── Root Providers ───────────────────────────────────
|
|
|
|
export function Providers({
|
|
children,
|
|
initialLocale,
|
|
}: {
|
|
children: ReactNode;
|
|
initialLocale?: Locale;
|
|
}) {
|
|
const [queryClient] = useState(
|
|
() =>
|
|
new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
staleTime: 5 * 60 * 1000,
|
|
gcTime: 10 * 60 * 1000,
|
|
refetchOnWindowFocus: false,
|
|
retry: 1,
|
|
},
|
|
},
|
|
}),
|
|
);
|
|
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<ThemeProvider>
|
|
<I18nProvider initialLocale={initialLocale}>
|
|
<AppSettingsProvider>
|
|
{children}
|
|
<Toaster position="bottom-right" richColors />
|
|
</AppSettingsProvider>
|
|
</I18nProvider>
|
|
</ThemeProvider>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|