diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 4048d7b3fb4312049519b03e9c56fd6921219367..86505ff4f5a57489ef7bd549505c67b96ba90e5e 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -6,6 +6,7 @@ import { AuthStatusProvider } from "@/videoag/authentication/AuthStatus"; import { showErrorToast } from "@/videoag/error/ErrorDisplay"; import { LanguageProvider } from "@/videoag/localization/LanguageProvider"; import { ReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary"; +import { ThemeProvider } from "@/videoag/miscellaneous/Theme"; import Title from "@/videoag/miscellaneous/TitleComponent"; import { RealBackendProvider } from "@/videoag/api/ApiProvider"; import { EditModeProvider } from "@/videoag/object_management/EditModeProvider"; @@ -40,10 +41,12 @@ export default function App({ Component, pageProps }: AppProps) { <AuthStatusProvider> <EditModeProvider> <LanguageProvider> - <ToastContainer /> - <DefaultLayout> - <Component {...pageProps} /> - </DefaultLayout> + <ThemeProvider> + <ToastContainer /> + <DefaultLayout> + <Component {...pageProps} /> + </DefaultLayout> + </ThemeProvider> </LanguageProvider> </EditModeProvider> </AuthStatusProvider> diff --git a/src/videoag/miscellaneous/Theme.tsx b/src/videoag/miscellaneous/Theme.tsx new file mode 100644 index 0000000000000000000000000000000000000000..cedb7be7f51d822357077c6b566196909b8d67ae --- /dev/null +++ b/src/videoag/miscellaneous/Theme.tsx @@ -0,0 +1,37 @@ +import type React from "react"; +import { createContext, useContext, useEffect } from "react"; + +import { useLocalStorageState } from "@/videoag/miscellaneous/Storage"; + +type ThemeContextType = { + theme: string; + setTheme: (newValue: string | ((oldValue: string) => string)) => void; +}; + +const ThemeContext = createContext<ThemeContextType>({ + theme: "dark", + setTheme: () => {}, +}); + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = useLocalStorageState<string>("theme", "dark"); + + useEffect(() => { + try { + let currentTheme = document.documentElement.getAttribute("data-bs-theme"); + if (theme !== currentTheme) { + document.documentElement.classList.add("no-transition"); + document.documentElement.setAttribute("data-bs-theme", theme); + document.documentElement.classList.remove("no-transition"); + } + } catch (e) { + console.error("Could not update theme on document", e); + } + }, [theme]); + + return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>; +} + +export function useTheme() { + return useContext(ThemeContext); +} diff --git a/src/videoag/site/DefaultLayout.tsx b/src/videoag/site/DefaultLayout.tsx index 7c643e5993c7783d4ef64ddeaa08d71929f90557..94e6a7947ed8a92327c2bbe433891bf82c19f3da 100644 --- a/src/videoag/site/DefaultLayout.tsx +++ b/src/videoag/site/DefaultLayout.tsx @@ -13,9 +13,9 @@ import UserField from "@/videoag/authentication/UserField"; import { showErrorToast, ErrorComponent } from "@/videoag/error/ErrorDisplay"; import { FallbackErrorBoundary } from "@/videoag/error/FallbackErrorBoundary"; import { useLanguage } from "@/videoag/localization/LanguageProvider"; -import { storageGetOrDefault, storageSet } from "@/videoag/miscellaneous/Storage"; import { ReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary"; import { StylizedText } from "@/videoag/miscellaneous/StylizedText"; +import { useTheme } from "@/videoag/miscellaneous/Theme"; import { useEditMode } from "@/videoag/object_management/EditModeProvider"; import AnnouncementComponent from "@/videoag/site/AnnouncementComponent"; import { basePath } from "@/../basepath"; @@ -108,38 +108,9 @@ export function Search({ function NavBar({ status }: { status?: GetStatusResponse }) { const [navbarOpen, setNavbarOpen] = useState(false); + const { setTheme } = useTheme(); const { language, setLanguageCode } = useLanguage(); - useEffect(() => { - try { - let currentTheme = document.documentElement.getAttribute("data-bs-theme"); - if (storageGetOrDefault("theme", currentTheme) !== currentTheme) { - document.documentElement.classList.add("no-transition"); - document.documentElement.setAttribute( - "data-bs-theme", - storageGetOrDefault("theme", currentTheme), - ); - document.documentElement.classList.remove("no-transition"); - } - } catch (e) { - console.error("Could not load theme from local storage", e); - } - }, []); - - const switchTheme = () => { - const theme = document.documentElement.getAttribute("data-bs-theme"); - if (theme === "dark") { - document.documentElement.setAttribute("data-bs-theme", "light"); - } else { - document.documentElement.setAttribute("data-bs-theme", "dark"); - } - try { - storageSet("theme", theme === "dark" ? "light" : "dark"); - } catch (e) { - console.error("Could not save theme to local storage", e); - } - }; - const switchLanguage = () => { setLanguageCode(language.getCode() === "de" ? "en" : "de"); }; @@ -267,7 +238,15 @@ function NavBar({ status }: { status?: GetStatusResponse }) { <UserField isUnavailable={status?.status === "unavailable"} /> <div> - <button className={"btn"} type="button" onClick={switchTheme}> + <button + className={"btn"} + type="button" + onClick={() => + setTheme((oldValue) => + oldValue === "dark" ? "light" : "dark", + ) + } + > <span aria-hidden="true" className={"bi bi-moon-stars"} /> </button> </div>