diff --git a/src/context/ThemeContext.tsx b/src/context/ThemeContext.tsx new file mode 100644 index 0000000..c4bb251 --- /dev/null +++ b/src/context/ThemeContext.tsx @@ -0,0 +1,94 @@ +// Datei: context/ThemeContext.tsx +"use client"; // Wichtig für die Verwendung von localStorage und useEffect in Next.js App Router + +import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'; + +type Theme = 'light' | 'dark' | 'system'; + +interface ThemeContextType { + theme: Theme; + setTheme: (theme: Theme) => void; + resolvedTheme: 'light' | 'dark'; // Der tatsächlich angewendete Theme (system aufgelöst) +} + +const ThemeContext = createContext(undefined); + +interface ThemeProviderProps { + children: ReactNode; + defaultTheme?: Theme; + storageKey?: string; +} + +export const ThemeProvider: React.FC = ({ + children, + defaultTheme = 'system', + storageKey = 'vite-ui-theme', // Du kannst den Key anpassen + }) => { + const [theme, setThemeState] = useState(() => { + if (typeof window === 'undefined') { + return defaultTheme; // Für SSR, falls kein window-Objekt vorhanden ist + } + try { + const storedTheme = window.localStorage.getItem(storageKey) as Theme | null; + return storedTheme || defaultTheme; + } catch (e) { + console.error('Error reading theme from localStorage', e); + return defaultTheme; + } + }); + + const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('light'); + + useEffect(() => { + const root = window.document.documentElement; + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + const updateTheme = (currentTheme: Theme) => { + let newResolvedTheme: 'light' | 'dark'; + + if (currentTheme === 'system') { + newResolvedTheme = mediaQuery.matches ? 'dark' : 'light'; + } else { + newResolvedTheme = currentTheme; + } + + root.classList.remove('light', 'dark'); + root.classList.add(newResolvedTheme); + setResolvedTheme(newResolvedTheme); + try { + window.localStorage.setItem(storageKey, currentTheme); + } catch (e) { + console.error('Error saving theme to localStorage', e); + } + }; + + updateTheme(theme); // Initiales Theme setzen + + const handleChange = () => { + if (theme === 'system') { + updateTheme('system'); // System-Präferenz hat sich geändert + } + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + }, [theme, storageKey]); + + const setTheme = (newTheme: Theme) => { + setThemeState(newTheme); + }; + + return ( + + {children} + + ); +}; + +export const useTheme = (): ThemeContextType => { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +}; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 94c28d4..6f6d4b2 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,11 +1,26 @@ -import "@/styles/globals.css"; -import type {AppProps} from "next/app"; -import {SessionProvider} from "next-auth/react"; +// Datei: pages/_app.tsx + +import { SessionProvider } from "next-auth/react"; +import type { AppProps } from 'next/app'; +import { ThemeProvider } from '../context/ThemeContext'; // Importiere den ThemeProvider +import '../styles/globals.css'; // Stelle sicher, dass deine globalen Styles importiert werden + +function MyApp({ Component, pageProps }: AppProps) { + // Die pageProps können eine 'session' Eigenschaft enthalten, wenn du getServerSideProps + // mit getSession in deinen Seiten verwendest. + const { session, ...restPageProps } = pageProps; -export default function App({Component, pageProps: {session, ...pageProps}}: AppProps) { return ( - + + + - ) + ); } + +export default MyApp; diff --git a/src/pages/admin/index.tsx b/src/pages/admin/index.tsx index 520bf7c..674e0dd 100644 --- a/src/pages/admin/index.tsx +++ b/src/pages/admin/index.tsx @@ -4,6 +4,7 @@ import { useSession, signOut } from 'next-auth/react'; import { useRouter } from 'next/router'; import { useEffect, useState, Fragment } from 'react'; import { Dialog, Transition } from '@headlessui/react'; +import { useTheme } from '@/context/ThemeContext'; // Pfad zum ThemeContext anpassen // Typen für die Admin-Seite (Frontend) interface AdminUser { @@ -24,7 +25,7 @@ interface AdminUser { interface AdminUsersApiResponse { users: AdminUser[]; totalUsers: number; - totalBalance: string; // Gesamtsaldo + totalBalance: string; } interface AdminTransaction { @@ -57,6 +58,45 @@ interface SetApprovalStatusApiResponse { approvedAt: Date | null; } +// ThemeSwitcher Komponente (kopiert aus Dashboard, ggf. in eine separate Datei auslagern) +const ThemeSwitcher: React.FC = () => { + const { theme, setTheme, resolvedTheme } = useTheme(); + const [mounted, setMounted] = useState(false); + + useEffect(() => setMounted(true), []); + + if (!mounted) { + return null; + } + + const cycleTheme = () => { + if (theme === 'light') setTheme('dark'); + else if (theme === 'dark') setTheme('system'); + else setTheme('light'); + }; + + return ( + + ); +}; + const AdminPanelPage: NextPage = () => { const { data: session, status } = useSession(); @@ -66,7 +106,7 @@ const AdminPanelPage: NextPage = () => { const [isLoadingUsers, setIsLoadingUsers] = useState(true); const [error, setError] = useState(null); const [successMessage, setSuccessMessage] = useState(null); - const [totalBalance, setTotalBalance] = useState(null); // State für Gesamtsaldo + const [totalBalance, setTotalBalance] = useState(null); const [selectedUserForAdjustment, setSelectedUserForAdjustment] = useState(null); const [adjustmentAmount, setAdjustmentAmount] = useState(''); @@ -108,7 +148,7 @@ const AdminPanelPage: NextPage = () => { approvedAt: user.approvedAt ? new Date(user.approvedAt).toLocaleString('de-DE') : null })); setUsers(formattedUsers); - setTotalBalance(data.totalBalance); // Gesamtsaldo setzen + setTotalBalance(data.totalBalance); } catch (err: unknown) { if (err instanceof Error) { setError(err.message); @@ -252,31 +292,32 @@ const AdminPanelPage: NextPage = () => { if (status === 'loading' || (status === 'authenticated' && session?.user?.role !== 'admin' && !isLoadingUsers) ) { return ( -
-

Laden oder Zugriff wird geprüft...

+
{/* Dark Mode Hintergrund */} +

Laden oder Zugriff wird geprüft...

{/* Dark Mode Text */}
); } if (status === 'unauthenticated') { return ( -
-

Bitte als Admin anmelden.

+
{/* Dark Mode Hintergrund */} +

Bitte als Admin anmelden.

{/* Dark Mode Text */}
); } return ( -
+
{/* Dark Mode Hintergrund */}
-

Admin Panel

-

Benutzerverwaltung & Übersicht

+

Admin Panel

{/* Dark Mode Text */} +

Benutzerverwaltung & Übersicht

{/* Dark Mode Text */}
-
+
{/* Container für Buttons */} + {/* NEU: Theme-Schalter hinzugefügt */} @@ -289,85 +330,84 @@ const AdminPanelPage: NextPage = () => {
- {error &&
{error}
} - {successMessage &&
{successMessage}
} + {error &&
{error}
} {/* Dark Mode Fehler */} + {successMessage &&
{successMessage}
} {/* Dark Mode Erfolg */} {/* Gesamtübersicht Sektion */} -
-

Gesamtübersicht

+
{/* Dark Mode Sektion */} +

Gesamtübersicht

{/* Dark Mode Text */}
-
-

Anzahl Benutzer

-

{users.length}

+
{/* Dark Mode Kachel */} +

Anzahl Benutzer

+

{users.length}

-
-

Gesamtsaldo

+
{/* Dark Mode Kachel */} +

Gesamtsaldo

{isLoadingUsers ? ( -

Lade...

+

Lade...

) : ( -

+

{totalBalance !== null ? `${totalBalance} €` : 'N/A'}

)}
- {/* Hier könnten weitere Kacheln für andere Statistiken hinzukommen */}
- -
-

Benutzerübersicht

+ {/* Benutzerliste */} +
{/* Dark Mode Sektion */} +

Benutzerübersicht

{/* Dark Mode Text */} {isLoadingUsers ? ( -

Benutzerliste wird geladen...

+

Benutzerliste wird geladen...

) : users.length > 0 ? (
- - +
{/* Dark Mode Tabelle */} + {/* Dark Mode Tabellenkopf */} - - - - - - + + + + + + - + {/* Dark Mode Tabellenkörper */} {users.map((user) => ( - - - - - - {/* Dark Mode Hover */} + + + + + ))} @@ -375,11 +415,11 @@ const AdminPanelPage: NextPage = () => {
NameE-MailSaldoRolleStatusAktionenNameE-MailSaldoRolleStatusAktionen
{user.name || 'N/A'}{user.email || 'N/A'}{user.balance ? `${user.balance} €` : '0.00 €'}{user.role} +
{user.name || 'N/A'}{user.email || 'N/A'}{user.balance ? `${user.balance} €` : '0.00 €'}{user.role} {user.isApproved ? ( - + Freigegeben ) : ( - + Ausstehend/Gesperrt )} {user.isApproved && user.approvedAt && ( -

am {user.approvedAt}

+

am {user.approvedAt}

)}
{user.isApproved ? ( ) : ( - + )} - - - + + +
) : ( -

Keine Benutzer gefunden.

+

Keine Benutzer gefunden.

)}
- {/* Modals (bleiben unverändert) */} + {/* Modal für Saldo anpassen */} setIsAdjustModalOpen(false)}> { leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
{/* Dark Mode Backdrop */}
{ leaveTo="opacity-0 scale-95" > - + Saldo anpassen für {selectedUserForAdjustment?.name || selectedUserForAdjustment?.email}
- + { value={adjustmentAmount} onChange={(e) => setAdjustmentAmount(e.target.value)} required - className="mt-1 block w-full px-3 py-2 border border-gray-400 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm text-gray-900" + className="mt-1 block w-full px-3 py-2 border border-gray-400 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm text-gray-900 dark:text-gray-100 bg-white dark:bg-slate-700" />
- +
- +