Files
HealthLog/src/components/providers.tsx
T
Marc-André BombeckandClaude Opus 4.7 85aa15b35b fix(nav): hide bug-report entry in nav when admin disables the toggle
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>
2026-05-09 20:14:10 +02:00

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>
);
}