mirror of
				https://github.com/steveiliop56/tinyauth.git
				synced 2025-10-30 21:55:43 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			nightly
			...
			feat/light
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | f5f18bc2f6 | 
| @@ -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