diff --git a/artifacts/skillguard/src/App.tsx b/artifacts/skillguard/src/App.tsx index 43b324c..af80756 100644 --- a/artifacts/skillguard/src/App.tsx +++ b/artifacts/skillguard/src/App.tsx @@ -2,6 +2,7 @@ import { Switch, Route, Router as WouterRouter } from "wouter"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { Toaster } from "@/components/ui/toaster"; import { TooltipProvider } from "@/components/ui/tooltip"; +import { ThemeProvider } from "@/lib/theme"; import { PublicLayout } from "@/components/public-layout"; import { AppLayout } from "@/components/layout"; import { RequireAdmin } from "@/components/require-admin"; @@ -99,9 +100,11 @@ function AppRoutes() { function App() { return ( - - - + + + + + ); } diff --git a/artifacts/skillguard/src/components/layout.tsx b/artifacts/skillguard/src/components/layout.tsx index 3a14f95..b536db1 100644 --- a/artifacts/skillguard/src/components/layout.tsx +++ b/artifacts/skillguard/src/components/layout.tsx @@ -1,10 +1,13 @@ import { Link, useLocation } from "wouter"; import { useTranslation } from "react-i18next"; -import { Shield, LayoutDashboard, History, Settings, LogOut, ExternalLink } from "lucide-react"; +import { Shield, LayoutDashboard, History, Settings, LogOut, ExternalLink, Sun, Moon } from "lucide-react"; import { useGetMe } from "@workspace/api-client-react"; import { useQueryClient } from "@tanstack/react-query"; import { Sidebar, SidebarContent, SidebarHeader, SidebarMenu, SidebarMenuItem, SidebarMenuButton, SidebarProvider, SidebarGroup, SidebarGroupContent, SidebarGroupLabel, SidebarFooter } from "@/components/ui/sidebar"; import { LanguageSwitcher } from "@/components/language-switcher"; +import { useTheme } from "@/lib/theme"; +import { Button } from "@/components/ui/button"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; const basePath = import.meta.env.BASE_URL.replace(/\/$/, ""); @@ -13,6 +16,7 @@ export function AppLayout({ children }: { children: React.ReactNode }) { const { t } = useTranslation(); const { data: me } = useGetMe(); const qc = useQueryClient(); + const { theme, toggleTheme } = useTheme(); async function handleSignOut() { await fetch(`${basePath}/api/auth/logout`, { @@ -98,6 +102,18 @@ export function AppLayout({ children }: { children: React.ReactNode }) { +
+ + + + + + {t(theme === "dark" ? "common.theme.switchToLight" : "common.theme.switchToDark")} + + +
diff --git a/artifacts/skillguard/src/components/public-layout.tsx b/artifacts/skillguard/src/components/public-layout.tsx index 5ac384f..2ee7fef 100644 --- a/artifacts/skillguard/src/components/public-layout.tsx +++ b/artifacts/skillguard/src/components/public-layout.tsx @@ -1,8 +1,10 @@ import { Link, useLocation } from "wouter"; import { useTranslation } from "react-i18next"; -import { Shield, ShieldCheck, Settings } from "lucide-react"; +import { Shield, ShieldCheck, Settings, Sun, Moon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { LanguageSwitcher } from "@/components/language-switcher"; +import { useTheme } from "@/lib/theme"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; const CATALOG_ANCHOR_ID = "skill-katalog"; @@ -18,6 +20,7 @@ function scrollToCatalog(attempts = 20) { export function PublicLayout({ children }: { children: React.ReactNode }) { const [location, setLocation] = useLocation(); const { t } = useTranslation(); + const { theme, toggleTheme } = useTheme(); const handleCatalogClick = (e: React.MouseEvent) => { e.preventDefault(); @@ -62,6 +65,16 @@ export function PublicLayout({ children }: { children: React.ReactNode }) { + + + + + + {t(theme === "dark" ? "common.theme.switchToLight" : "common.theme.switchToDark")} + + diff --git a/artifacts/skillguard/src/i18n/locales/de/common.ts b/artifacts/skillguard/src/i18n/locales/de/common.ts index 530e0b4..ee2ade0 100644 --- a/artifacts/skillguard/src/i18n/locales/de/common.ts +++ b/artifacts/skillguard/src/i18n/locales/de/common.ts @@ -64,6 +64,10 @@ export default { signedIn: "Angemeldet", signOut: "Abmelden", }, + theme: { + switchToDark: "Dunkelmodus aktivieren", + switchToLight: "Hellmodus aktivieren", + }, actions: { back: "Zurück", cancel: "Abbrechen", diff --git a/artifacts/skillguard/src/i18n/locales/en/common.ts b/artifacts/skillguard/src/i18n/locales/en/common.ts index 17b31ca..6fb2cb6 100644 --- a/artifacts/skillguard/src/i18n/locales/en/common.ts +++ b/artifacts/skillguard/src/i18n/locales/en/common.ts @@ -64,6 +64,10 @@ export default { signedIn: "Signed in", signOut: "Sign out", }, + theme: { + switchToDark: "Switch to dark mode", + switchToLight: "Switch to light mode", + }, actions: { back: "Back", cancel: "Cancel", diff --git a/artifacts/skillguard/src/i18n/locales/es/common.ts b/artifacts/skillguard/src/i18n/locales/es/common.ts index 068e0f5..db109de 100644 --- a/artifacts/skillguard/src/i18n/locales/es/common.ts +++ b/artifacts/skillguard/src/i18n/locales/es/common.ts @@ -64,6 +64,10 @@ export default { signedIn: "Sesión iniciada", signOut: "Cerrar sesión", }, + theme: { + switchToDark: "Activar modo oscuro", + switchToLight: "Activar modo claro", + }, actions: { back: "Atrás", cancel: "Cancelar", diff --git a/artifacts/skillguard/src/lib/theme.tsx b/artifacts/skillguard/src/lib/theme.tsx new file mode 100644 index 0000000..88ee2af --- /dev/null +++ b/artifacts/skillguard/src/lib/theme.tsx @@ -0,0 +1,54 @@ +import { createContext, useContext, useEffect, useState } from "react"; + +type Theme = "light" | "dark"; + +const STORAGE_KEY = "skillguard-theme"; + +function getInitialTheme(): Theme { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored === "dark" || stored === "light") return stored; + } catch {} + return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} + +function applyTheme(theme: Theme) { + document.documentElement.classList.toggle("dark", theme === "dark"); +} + +interface ThemeContextValue { + theme: Theme; + toggleTheme: () => void; +} + +const ThemeContext = createContext({ + theme: "light", + toggleTheme: () => {}, +}); + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [theme, setTheme] = useState(() => { + const t = getInitialTheme(); + applyTheme(t); + return t; + }); + + useEffect(() => { + applyTheme(theme); + try { localStorage.setItem(STORAGE_KEY, theme); } catch {} + }, [theme]); + + function toggleTheme() { + setTheme(t => (t === "dark" ? "light" : "dark")); + } + + return ( + + {children} + + ); +} + +export function useTheme() { + return useContext(ThemeContext); +}