mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-11-04 08:05:42 +00:00 
			
		
		
		
	feat: add support for light mode
This commit is contained in:
		@@ -12,7 +12,7 @@
 | 
			
		||||
    <link rel="manifest" href="/site.webmanifest" />
 | 
			
		||||
    <title>Tinyauth</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body class="dark">
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <script type="module" src="/src/main.tsx"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
 
 | 
			
		||||
@@ -18,9 +18,10 @@ export const LanguageSelector = () => {
 | 
			
		||||
    setLanguage(option as SupportedLanguage);
 | 
			
		||||
    i18n.changeLanguage(option as SupportedLanguage);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Select onValueChange={handleSelect} value={language}>
 | 
			
		||||
      <SelectTrigger className="absolute top-5 right-5">
 | 
			
		||||
      <SelectTrigger className="bg-card">
 | 
			
		||||
        <SelectValue placeholder="Select language" />
 | 
			
		||||
      </SelectTrigger>
 | 
			
		||||
      <SelectContent>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import { LanguageSelector } from "../language/language";
 | 
			
		||||
import { Outlet } from "react-router";
 | 
			
		||||
import { useCallback, useEffect, useState } from "react";
 | 
			
		||||
import { DomainWarning } from "../domain-warning/domain-warning";
 | 
			
		||||
import { ThemeSwitch } from "../theme-switch/theme-switch";
 | 
			
		||||
 | 
			
		||||
const BaseLayout = ({ children }: { children: React.ReactNode }) => {
 | 
			
		||||
  const { backgroundImage, title } = useAppContext();
 | 
			
		||||
@@ -20,7 +21,10 @@ const BaseLayout = ({ children }: { children: React.ReactNode }) => {
 | 
			
		||||
        backgroundPosition: "center",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      <LanguageSelector />
 | 
			
		||||
      <div className="absolute top-5 right-5 flex flex-row gap-2">
 | 
			
		||||
        <ThemeSwitch />
 | 
			
		||||
        <LanguageSelector />
 | 
			
		||||
      </div>
 | 
			
		||||
      {children}
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										53
									
								
								frontend/src/components/providers/theme-provider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								frontend/src/components/providers/theme-provider.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { createContext, useEffect, useState } from "react";
 | 
			
		||||
 | 
			
		||||
interface ThemeSchema {
 | 
			
		||||
  darkMode: boolean;
 | 
			
		||||
  setDarkMode: (darkMode: boolean) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ThemeContext = createContext<ThemeSchema | null>(null);
 | 
			
		||||
 | 
			
		||||
export const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
 | 
			
		||||
  const [darkMode, setDarkMode] = useState<boolean>(false);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const storedTheme = localStorage.getItem("tinyauth-theme");
 | 
			
		||||
    if (storedTheme) {
 | 
			
		||||
      setDarkMode(storedTheme === "dark");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const prefersDark = window.matchMedia(
 | 
			
		||||
      "(prefers-color-scheme: dark)",
 | 
			
		||||
    ).matches;
 | 
			
		||||
    setDarkMode(prefersDark);
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const rootElement = document.documentElement;
 | 
			
		||||
    rootElement.classList.remove("dark", "light");
 | 
			
		||||
    rootElement.classList.add(darkMode ? "dark" : "light");
 | 
			
		||||
  }, [darkMode]);
 | 
			
		||||
 | 
			
		||||
  const values = {
 | 
			
		||||
    darkMode,
 | 
			
		||||
    setDarkMode: (darkMode: boolean) => {
 | 
			
		||||
      localStorage.setItem("tinyauth-theme", darkMode ? "dark" : "light");
 | 
			
		||||
      setDarkMode(darkMode);
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ThemeContext.Provider value={values}>{children}</ThemeContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const useTheme = () => {
 | 
			
		||||
  const context = React.useContext(ThemeContext);
 | 
			
		||||
 | 
			
		||||
  if (!context) {
 | 
			
		||||
    throw new Error("useTheme must be used within a ThemeProvider");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return context;
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										23
									
								
								frontend/src/components/theme-switch/theme-switch.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								frontend/src/components/theme-switch/theme-switch.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import { MoonIcon, SunIcon } from "lucide-react";
 | 
			
		||||
import { useTheme } from "../providers/theme-provider";
 | 
			
		||||
import { Button } from "../ui/button";
 | 
			
		||||
 | 
			
		||||
export const ThemeSwitch = () => {
 | 
			
		||||
  const { darkMode, setDarkMode } = useTheme();
 | 
			
		||||
 | 
			
		||||
  const toggleTheme = () => {
 | 
			
		||||
    setDarkMode(!darkMode);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Button
 | 
			
		||||
      className="bg-card hover:bg-card/90 text-card-foreground"
 | 
			
		||||
      aria-label={`Switch to ${darkMode ? "light" : "dark"} mode`}
 | 
			
		||||
      onClick={() => {
 | 
			
		||||
        toggleTheme();
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {darkMode ? <SunIcon /> : <MoonIcon />}
 | 
			
		||||
    </Button>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -6,7 +6,7 @@ import { cn } from "@/lib/utils";
 | 
			
		||||
import { Loader2 } from "lucide-react";
 | 
			
		||||
 | 
			
		||||
const buttonVariants = cva(
 | 
			
		||||
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
 | 
			
		||||
  "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive hover:cursor-pointer",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ function SelectTrigger({
 | 
			
		||||
      data-slot="select-trigger"
 | 
			
		||||
      data-size={size}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-card dark:hover:bg-card/90 flex w-fit items-center justify-between gap-2 rounded-md border bg-card hover:bg-card/90 px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
 | 
			
		||||
        "hover:cursor-pointer border-input data-[placeholder]:text-card-foreground [&_svg:not([class*='text-'])]:text-card-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-card dark:hover:bg-card/90 flex w-fit items-center justify-between gap-2 rounded-md border bg-card hover:bg-card/90 px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
 | 
			
		||||
        className,
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import { useTheme } from "next-themes";
 | 
			
		||||
import { useTheme } from "../providers/theme-provider";
 | 
			
		||||
import { Toaster as Sonner, ToasterProps } from "sonner";
 | 
			
		||||
 | 
			
		||||
const Toaster = ({ ...props }: ToasterProps) => {
 | 
			
		||||
  const { theme = "system" } = useTheme();
 | 
			
		||||
  const { darkMode } = useTheme();
 | 
			
		||||
  const theme = darkMode ? "dark" : "light";
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Sonner
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
 | 
			
		||||
import { AppContextProvider } from "./context/app-context.tsx";
 | 
			
		||||
import { UserContextProvider } from "./context/user-context.tsx";
 | 
			
		||||
import { Toaster } from "@/components/ui/sonner";
 | 
			
		||||
import { ThemeProvider } from "./components/providers/theme-provider.tsx";
 | 
			
		||||
 | 
			
		||||
const queryClient = new QueryClient();
 | 
			
		||||
 | 
			
		||||
@@ -24,25 +25,27 @@ createRoot(document.getElementById("root")!).render(
 | 
			
		||||
    <QueryClientProvider client={queryClient}>
 | 
			
		||||
      <AppContextProvider>
 | 
			
		||||
        <UserContextProvider>
 | 
			
		||||
          <BrowserRouter>
 | 
			
		||||
            <Routes>
 | 
			
		||||
              <Route element={<Layout />} errorElement={<ErrorPage />}>
 | 
			
		||||
                <Route path="/" element={<App />} />
 | 
			
		||||
                <Route path="/login" element={<LoginPage />} />
 | 
			
		||||
                <Route path="/logout" element={<LogoutPage />} />
 | 
			
		||||
                <Route path="/continue" element={<ContinuePage />} />
 | 
			
		||||
                <Route path="/totp" element={<TotpPage />} />
 | 
			
		||||
                <Route
 | 
			
		||||
                  path="/forgot-password"
 | 
			
		||||
                  element={<ForgotPasswordPage />}
 | 
			
		||||
                />
 | 
			
		||||
                <Route path="/unauthorized" element={<UnauthorizedPage />} />
 | 
			
		||||
                <Route path="/error" element={<ErrorPage />} />
 | 
			
		||||
                <Route path="*" element={<NotFoundPage />} />
 | 
			
		||||
              </Route>
 | 
			
		||||
            </Routes>
 | 
			
		||||
          </BrowserRouter>
 | 
			
		||||
          <Toaster />
 | 
			
		||||
          <ThemeProvider>
 | 
			
		||||
            <BrowserRouter>
 | 
			
		||||
              <Routes>
 | 
			
		||||
                <Route element={<Layout />} errorElement={<ErrorPage />}>
 | 
			
		||||
                  <Route path="/" element={<App />} />
 | 
			
		||||
                  <Route path="/login" element={<LoginPage />} />
 | 
			
		||||
                  <Route path="/logout" element={<LogoutPage />} />
 | 
			
		||||
                  <Route path="/continue" element={<ContinuePage />} />
 | 
			
		||||
                  <Route path="/totp" element={<TotpPage />} />
 | 
			
		||||
                  <Route
 | 
			
		||||
                    path="/forgot-password"
 | 
			
		||||
                    element={<ForgotPasswordPage />}
 | 
			
		||||
                  />
 | 
			
		||||
                  <Route path="/unauthorized" element={<UnauthorizedPage />} />
 | 
			
		||||
                  <Route path="/error" element={<ErrorPage />} />
 | 
			
		||||
                  <Route path="*" element={<NotFoundPage />} />
 | 
			
		||||
                </Route>
 | 
			
		||||
              </Routes>
 | 
			
		||||
            </BrowserRouter>
 | 
			
		||||
            <Toaster />
 | 
			
		||||
          </ThemeProvider>
 | 
			
		||||
        </UserContextProvider>
 | 
			
		||||
      </AppContextProvider>
 | 
			
		||||
    </QueryClientProvider>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user