From 9f97c0e7985dc9c91dc93158869d3734cd261b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20K=C3=BCnzel?= <simonk@fsmpi.rwth-aachen.de> Date: Sat, 10 May 2025 02:13:38 +0200 Subject: [PATCH] Add ThemeProvider --- src/pages/_app.tsx | 11 +++++--- src/videoag/miscellaneous/Theme.tsx | 37 +++++++++++++++++++++++++ src/videoag/site/DefaultLayout.tsx | 43 ++++++++--------------------- 3 files changed, 55 insertions(+), 36 deletions(-) create mode 100644 src/videoag/miscellaneous/Theme.tsx diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 4048d7b..86505ff 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 0000000..cedb7be --- /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 7c643e5..94e6a79 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> -- GitLab