fix(theme): make preference state deterministic
This commit is contained in:
+22
-16
@@ -1,8 +1,7 @@
|
||||
import React, { Suspense, lazy, useEffect, useMemo, useState } from "react";
|
||||
import React, { Suspense, createContext, lazy, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Box, Button, CssBaseline, IconButton, Typography } from "@mui/material";
|
||||
import { Box, Button, IconButton, Typography } from "@mui/material";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { CssVarsProvider } from "@mui/material/styles";
|
||||
|
||||
import DashboardIcon from "@mui/icons-material/Dashboard";
|
||||
import WorkOutlineIcon from "@mui/icons-material/WorkOutline";
|
||||
@@ -20,7 +19,6 @@ import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined";
|
||||
|
||||
import { Navigate, Route, Routes, useLocation, useNavigate, createBrowserRouter, RouterProvider } from "react-router-dom";
|
||||
|
||||
import { getTheme } from "./theme";
|
||||
import { ToastProvider } from "./toast";
|
||||
import { ConfirmProvider } from "./confirm";
|
||||
import { PromptProvider } from "./prompt";
|
||||
@@ -40,7 +38,8 @@ import { resolveCaptureUrl } from "./captureUrl";
|
||||
import { clearAuthClientState, setAuthUserKey } from "./auth";
|
||||
import { AccountPlanProvider } from "./accountPlan";
|
||||
import AppShell, { NavItem } from "./layout/AppShell";
|
||||
import { getThemeModePref, setThemeModePref, ThemeModePref } from "./themePrefs";
|
||||
import { getThemeModePref, setThemeModePref, subscribeToThemePreferenceChanges, ThemeModePref } from "./themePrefs";
|
||||
import JobTrackerThemeProvider from "./ThemeProvider";
|
||||
|
||||
const AddJobModal = lazy(() => import("./components/AddJobModal"));
|
||||
const KanbanBoard = lazy(() => import("./components/KanbanBoard"));
|
||||
@@ -66,6 +65,8 @@ const OperationsPage = lazy(() => import("./views/OperationsPage"));
|
||||
const NotFoundPage = lazy(() => import("./views/NotFoundPage"));
|
||||
|
||||
type AuthConfig = { requireAuth: boolean };
|
||||
type ThemeControls = { themeMode: ThemeModePref; onThemeModeChange: (value: ThemeModePref) => void };
|
||||
const ThemeControlsContext = createContext<ThemeControls | null>(null);
|
||||
type MeResponse = {
|
||||
provider?: "local" | "google" | "external";
|
||||
id?: string;
|
||||
@@ -383,19 +384,23 @@ function Shell({ jobPageSize, setJobPageSize, jobColumns, setJobColumns, themeMo
|
||||
);
|
||||
}
|
||||
|
||||
function ThemeAwareShell(props: Omit<React.ComponentProps<typeof Shell>, "themeMode" | "onThemeModeChange">) {
|
||||
const controls = useContext(ThemeControlsContext);
|
||||
if (!controls) throw new Error("Theme controls are unavailable.");
|
||||
return <Shell {...props} {...controls} />;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const systemPrefersDark = useMediaQuery("(prefers-color-scheme: dark)", { defaultMatches: true, noSsr: true });
|
||||
const [themeMode, setThemeMode] = useState<ThemeModePref>(() => getThemeModePref());
|
||||
const effectiveMode: "light" | "dark" = themeMode === "light" ? "light" : themeMode === "dark" ? "dark" : systemPrefersDark ? "dark" : "light";
|
||||
const theme = useMemo(() => getTheme(effectiveMode), [effectiveMode]);
|
||||
|
||||
useEffect(() => {
|
||||
const sync = () => { setThemeMode(getThemeModePref()); };
|
||||
window.addEventListener("auth-changed", sync);
|
||||
return () => window.removeEventListener("auth-changed", sync);
|
||||
return subscribeToThemePreferenceChanges(sync);
|
||||
}, []);
|
||||
|
||||
const onThemeModeChange = (v: ThemeModePref) => { setThemeModePref(v); setThemeMode(v); };
|
||||
const onThemeModeChange = useCallback((v: ThemeModePref) => { setThemeModePref(v); setThemeMode(v); }, []);
|
||||
const themeControls = useMemo(() => ({ themeMode, onThemeModeChange }), [onThemeModeChange, themeMode]);
|
||||
|
||||
const [jobPageSize, setJobPageSize] = useState<15 | 20 | 25>(() => {
|
||||
const raw = window.localStorage.getItem("jobPageSize");
|
||||
@@ -423,19 +428,20 @@ export default function App() {
|
||||
{ path: "/confirm-email-change", element: <VerifyEmailPage emailChange />, errorElement: <RouteErrorPage /> },
|
||||
{ path: "/microsoft-legacy-relink", element: <MicrosoftLegacyRelinkPage />, errorElement: <RouteErrorPage /> },
|
||||
{ path: "/cv/:slug", element: <PublicCvPage />, errorElement: <RouteErrorPage /> },
|
||||
{ path: "/*", element: <Shell jobPageSize={jobPageSize} setJobPageSize={setJobPageSize} jobColumns={jobColumns} setJobColumns={setJobColumns} themeMode={themeMode} onThemeModeChange={onThemeModeChange} />, errorElement: <RouteErrorPage /> },
|
||||
], { future: { v7_relativeSplatPath: true } }), [jobColumns, jobPageSize, themeMode]);
|
||||
{ path: "/*", element: <ThemeAwareShell jobPageSize={jobPageSize} setJobPageSize={setJobPageSize} jobColumns={jobColumns} setJobColumns={setJobColumns} />, errorElement: <RouteErrorPage /> },
|
||||
], { future: { v7_relativeSplatPath: true } }), [jobColumns, jobPageSize]);
|
||||
|
||||
return (
|
||||
<ToastProvider>
|
||||
<ConfirmProvider>
|
||||
<PromptProvider>
|
||||
<CssVarsProvider key={effectiveMode} theme={theme as any} defaultMode={effectiveMode} disableTransitionOnChange>
|
||||
<CssBaseline enableColorScheme />
|
||||
<JobTrackerThemeProvider preference={themeMode} systemPrefersDark={systemPrefersDark}>
|
||||
<I18nProvider>
|
||||
<RouterProvider router={router} future={{ v7_startTransition: true }} />
|
||||
<ThemeControlsContext.Provider value={themeControls}>
|
||||
<RouterProvider router={router} future={{ v7_startTransition: true }} />
|
||||
</ThemeControlsContext.Provider>
|
||||
</I18nProvider>
|
||||
</CssVarsProvider>
|
||||
</JobTrackerThemeProvider>
|
||||
</PromptProvider>
|
||||
</ConfirmProvider>
|
||||
</ToastProvider>
|
||||
|
||||
Reference in New Issue
Block a user