add dark mode
This commit is contained in:
parent
380084b675
commit
f7d462b359
94
src/context/ThemeContext.tsx
Normal file
94
src/context/ThemeContext.tsx
Normal file
@ -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<ThemeContextType | undefined>(undefined);
|
||||
|
||||
interface ThemeProviderProps {
|
||||
children: ReactNode;
|
||||
defaultTheme?: Theme;
|
||||
storageKey?: string;
|
||||
}
|
||||
|
||||
export const ThemeProvider: React.FC<ThemeProviderProps> = ({
|
||||
children,
|
||||
defaultTheme = 'system',
|
||||
storageKey = 'vite-ui-theme', // Du kannst den Key anpassen
|
||||
}) => {
|
||||
const [theme, setThemeState] = useState<Theme>(() => {
|
||||
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 (
|
||||
<ThemeContext.Provider value={{ theme, setTheme, resolvedTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTheme = (): ThemeContextType => {
|
||||
const context = useContext(ThemeContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useTheme must be used within a ThemeProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@ -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 (
|
||||
<SessionProvider session={session}>
|
||||
<Component {...pageProps} />
|
||||
<ThemeProvider
|
||||
defaultTheme="system" // Standard-Theme (dies wird vom ThemeContext verwendet)
|
||||
storageKey="strichliste-theme" // Optional: Eigener Key für localStorage (wird vom ThemeContext verwendet)
|
||||
// 'attribute' und 'enableSystem' wurden entfernt, da sie nicht Teil unserer ThemeProviderProps sind
|
||||
>
|
||||
<Component {...restPageProps} />
|
||||
</ThemeProvider>
|
||||
</SessionProvider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
|
||||
@ -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 (
|
||||
<button
|
||||
onClick={cycleTheme}
|
||||
aria-label="Theme wechseln"
|
||||
className="p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
{resolvedTheme === 'dark' ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 text-yellow-400">
|
||||
<path fillRule="evenodd" d="M9.528 1.718a.75.75 0 0 1 .162.819A8.97 8.97 0 0 0 9 6a9 9 0 0 0 9 9 8.97 8.97 0 0 0 3.463-.69.75.75 0 0 1 .981.981A10.503 10.503 0 0 1 18 19.5a10.5 10.5 0 0 1-10.5-10.5A10.503 10.503 0 0 1 9.528 1.718Z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 text-orange-500">
|
||||
<path d="M12 2.25a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0V3a.75.75 0 0 1 .75-.75ZM7.5 12a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM18.894 6.166a.75.75 0 0 0-1.06-1.06l-1.591 1.59a.75.75 0 1 0 1.06 1.061l1.591-1.59ZM21.75 12a.75.75 0 0 1-.75.75h-2.25a.75.75 0 0 1 0-1.5H21a.75.75 0 0 1 .75.75ZM17.834 18.894a.75.75 0 0 0 1.06-1.06l-1.59-1.591a.75.75 0 1 0-1.061 1.06l1.59 1.591ZM12 18a.75.75 0 0 1 .75.75V21a.75.75 0 0 1-1.5 0v-2.25A.75.75 0 0 1 12 18ZM7.758 17.303a.75.75 0 0 0-1.061-1.06l-1.591 1.59a.75.75 0 0 0 1.06 1.061l1.591-1.59ZM6.166 7.758a.75.75 0 0 0-1.06 1.061l1.59 1.591a.75.75 0 0 0 1.061-1.06l-1.59-1.591ZM12 3.5A8.5 8.5 0 0 1 20.5 12c0 .312-.022.618-.065.916a.75.75 0 0 0-1.435-.276A7.001 7.001 0 0 0 12 5c-3.865 0-7 3.135-7 7 0 .096.005.19.014.284a.75.75 0 0 0 .727.71A.751.751 0 0 0 6.02 13a7.001 7.001 0 0 0 5.98-2.088.75.75 0 0 0-.364-1.118A8.502 8.502 0 0 1 12 3.5Z" />
|
||||
</svg>
|
||||
)}
|
||||
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400 capitalize">
|
||||
{theme === 'system' ? `System (${resolvedTheme})` : theme}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const AdminPanelPage: NextPage = () => {
|
||||
const { data: session, status } = useSession();
|
||||
@ -66,7 +106,7 @@ const AdminPanelPage: NextPage = () => {
|
||||
const [isLoadingUsers, setIsLoadingUsers] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [totalBalance, setTotalBalance] = useState<string | null>(null); // State für Gesamtsaldo
|
||||
const [totalBalance, setTotalBalance] = useState<string | null>(null);
|
||||
|
||||
const [selectedUserForAdjustment, setSelectedUserForAdjustment] = useState<AdminUser | null>(null);
|
||||
const [adjustmentAmount, setAdjustmentAmount] = useState<string>('');
|
||||
@ -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 (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<p className="text-lg text-gray-600">Laden oder Zugriff wird geprüft...</p>
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900"> {/* Dark Mode Hintergrund */}
|
||||
<p className="text-lg text-gray-600 dark:text-gray-300">Laden oder Zugriff wird geprüft...</p> {/* Dark Mode Text */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'unauthenticated') {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<p className="text-lg text-gray-600">Bitte als Admin anmelden.</p>
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900"> {/* Dark Mode Hintergrund */}
|
||||
<p className="text-lg text-gray-600 dark:text-gray-300">Bitte als Admin anmelden.</p> {/* Dark Mode Text */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-4 sm:p-6 lg:p-8">
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-slate-900 p-4 sm:p-6 lg:p-8 transition-colors duration-300"> {/* Dark Mode Hintergrund */}
|
||||
<header className="mb-8 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-800">Admin Panel</h1>
|
||||
<p className="text-gray-600">Benutzerverwaltung & Übersicht</p>
|
||||
<h1 className="text-3xl font-bold text-gray-800 dark:text-gray-100">Admin Panel</h1> {/* Dark Mode Text */}
|
||||
<p className="text-gray-600 dark:text-gray-400">Benutzerverwaltung & Übersicht</p> {/* Dark Mode Text */}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center space-x-4"> {/* Container für Buttons */}
|
||||
<ThemeSwitcher /> {/* NEU: Theme-Schalter hinzugefügt */}
|
||||
<button
|
||||
onClick={() => router.push('/dashboard')}
|
||||
className="mr-4 px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||
>
|
||||
Zum Dashboard
|
||||
</button>
|
||||
@ -289,85 +330,84 @@ const AdminPanelPage: NextPage = () => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{error && <div className="mb-4 p-3 bg-red-100 text-red-700 border border-red-300 rounded-md">{error}</div>}
|
||||
{successMessage && <div className="mb-4 p-3 bg-green-100 text-green-700 border border-green-300 rounded-md">{successMessage}</div>}
|
||||
{error && <div className="mb-4 p-3 bg-red-100 text-red-700 border border-red-300 rounded-md dark:bg-red-900 dark:text-red-200 dark:border-red-700">{error}</div>} {/* Dark Mode Fehler */}
|
||||
{successMessage && <div className="mb-4 p-3 bg-green-100 text-green-700 border border-green-300 rounded-md dark:bg-green-900 dark:text-green-200 dark:border-green-700">{successMessage}</div>} {/* Dark Mode Erfolg */}
|
||||
|
||||
{/* Gesamtübersicht Sektion */}
|
||||
<section className="mb-8 p-6 bg-white shadow-lg rounded-lg">
|
||||
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Gesamtübersicht</h2>
|
||||
<section className="mb-8 p-6 bg-white dark:bg-slate-800 shadow-lg rounded-lg"> {/* Dark Mode Sektion */}
|
||||
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-4">Gesamtübersicht</h2> {/* Dark Mode Text */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="p-4 bg-indigo-50 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-indigo-700">Anzahl Benutzer</h3>
|
||||
<p className="mt-1 text-3xl font-semibold text-indigo-600">{users.length}</p>
|
||||
<div className="p-4 bg-indigo-50 dark:bg-indigo-900/30 rounded-lg"> {/* Dark Mode Kachel */}
|
||||
<h3 className="text-sm font-medium text-indigo-700 dark:text-indigo-300">Anzahl Benutzer</h3>
|
||||
<p className="mt-1 text-3xl font-semibold text-indigo-600 dark:text-indigo-400">{users.length}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-green-50 rounded-lg">
|
||||
<h3 className="text-sm font-medium text-green-700">Gesamtsaldo</h3>
|
||||
<div className="p-4 bg-green-50 dark:bg-green-900/30 rounded-lg"> {/* Dark Mode Kachel */}
|
||||
<h3 className="text-sm font-medium text-green-700 dark:text-green-300">Gesamtsaldo</h3>
|
||||
{isLoadingUsers ? (
|
||||
<p className="mt-1 text-3xl font-semibold text-green-600">Lade...</p>
|
||||
<p className="mt-1 text-3xl font-semibold text-green-600 dark:text-green-400">Lade...</p>
|
||||
) : (
|
||||
<p className={`mt-1 text-3xl font-semibold ${totalBalance && parseFloat(totalBalance) < 0 ? 'text-red-600' : 'text-green-600'}`}>
|
||||
<p className={`mt-1 text-3xl font-semibold ${totalBalance && parseFloat(totalBalance) < 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}`}>
|
||||
{totalBalance !== null ? `${totalBalance} €` : 'N/A'}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Hier könnten weitere Kacheln für andere Statistiken hinzukommen */}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section className="p-6 bg-white shadow-lg rounded-lg">
|
||||
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Benutzerübersicht</h2>
|
||||
{/* Benutzerliste */}
|
||||
<section className="p-6 bg-white dark:bg-slate-800 shadow-lg rounded-lg"> {/* Dark Mode Sektion */}
|
||||
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-4">Benutzerübersicht</h2> {/* Dark Mode Text */}
|
||||
{isLoadingUsers ? (
|
||||
<p className="text-gray-500">Benutzerliste wird geladen...</p>
|
||||
<p className="text-gray-500 dark:text-gray-400">Benutzerliste wird geladen...</p>
|
||||
) : users.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> {/* Dark Mode Tabelle */}
|
||||
<thead className="bg-gray-50 dark:bg-slate-700"> {/* Dark Mode Tabellenkopf */}
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">E-Mail</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Saldo</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Rolle</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Aktionen</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Name</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">E-Mail</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Saldo</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Rolle</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Status</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<tbody className="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700"> {/* Dark Mode Tabellenkörper */}
|
||||
{users.map((user) => (
|
||||
<tr key={user.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{user.name || 'N/A'}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{user.email || 'N/A'}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 font-medium">{user.balance ? `${user.balance} €` : '0.00 €'}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">{user.role}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700">
|
||||
<tr key={user.id} className="dark:hover:bg-slate-700/50"> {/* Dark Mode Hover */}
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">{user.name || 'N/A'}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">{user.email || 'N/A'}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300 font-medium">{user.balance ? `${user.balance} €` : '0.00 €'}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">{user.role}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">
|
||||
{user.isApproved ? (
|
||||
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100">
|
||||
Freigegeben
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800">
|
||||
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-yellow-100 text-yellow-800 dark:bg-yellow-700 dark:text-yellow-100">
|
||||
Ausstehend/Gesperrt
|
||||
</span>
|
||||
)}
|
||||
{user.isApproved && user.approvedAt && (
|
||||
<p className="text-xs text-gray-500 mt-1">am {user.approvedAt}</p>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">am {user.approvedAt}</p>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
|
||||
{user.isApproved ? (
|
||||
<button
|
||||
onClick={() => handleToggleApproval(user.id, user.isApproved)}
|
||||
className="text-yellow-600 hover:text-yellow-900"
|
||||
className="text-yellow-600 hover:text-yellow-900 dark:text-yellow-400 dark:hover:text-yellow-300"
|
||||
disabled={user.id === session?.user?.id && users.filter(u => u.role === 'admin' && u.isApproved).length <= 1 && user.role === 'admin'}
|
||||
>
|
||||
Sperren
|
||||
</button>
|
||||
) : (
|
||||
<button onClick={() => handleToggleApproval(user.id, user.isApproved)} className="text-green-600 hover:text-green-900">Freigeben</button>
|
||||
<button onClick={() => handleToggleApproval(user.id, user.isApproved)} className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300">Freigeben</button>
|
||||
)}
|
||||
<button onClick={() => openAdjustModal(user)} className="text-indigo-600 hover:text-indigo-900">Anpassen</button>
|
||||
<button onClick={() => handleResetBalance(user.id)} className="text-red-600 hover:text-red-900">Reset</button>
|
||||
<button onClick={() => openTransactionsModal(user)} className="text-blue-600 hover:text-blue-900">Verlauf</button>
|
||||
<button onClick={() => openAdjustModal(user)} className="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">Anpassen</button>
|
||||
<button onClick={() => handleResetBalance(user.id)} className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">Reset</button>
|
||||
<button onClick={() => openTransactionsModal(user)} className="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Verlauf</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
@ -375,11 +415,11 @@ const AdminPanelPage: NextPage = () => {
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500">Keine Benutzer gefunden.</p>
|
||||
<p className="text-gray-500 dark:text-gray-400">Keine Benutzer gefunden.</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Modals (bleiben unverändert) */}
|
||||
{/* Modal für Saldo anpassen */}
|
||||
<Transition appear show={isAdjustModalOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={() => setIsAdjustModalOpen(false)}>
|
||||
<Transition.Child
|
||||
@ -392,14 +432,14 @@ const AdminPanelPage: NextPage = () => {
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm" />
|
||||
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm dark:bg-slate-900/80" /> {/* Dark Mode Backdrop */}
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as="div"
|
||||
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
|
||||
className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 p-6 text-left align-middle shadow-xl transition-all"
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
@ -408,12 +448,12 @@ const AdminPanelPage: NextPage = () => {
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel>
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">
|
||||
Saldo anpassen für {selectedUserForAdjustment?.name || selectedUserForAdjustment?.email}
|
||||
</Dialog.Title>
|
||||
<form onSubmit={handleAdjustBalance} className="mt-4 space-y-4">
|
||||
<div>
|
||||
<label htmlFor="adjustmentAmount" className="block text-sm font-medium text-gray-700">Betrag (€)</label>
|
||||
<label htmlFor="adjustmentAmount" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Betrag (€)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="adjustmentAmount"
|
||||
@ -422,44 +462,44 @@ const AdminPanelPage: NextPage = () => {
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="adjustmentAction" className="block text-sm font-medium text-gray-700">Aktion</label>
|
||||
<label htmlFor="adjustmentAction" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Aktion</label>
|
||||
<select
|
||||
id="adjustmentAction"
|
||||
name="adjustmentAction"
|
||||
value={adjustmentAction}
|
||||
onChange={(e) => setAdjustmentAction(e.target.value as 'increase' | 'decrease')}
|
||||
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border border-gray-400 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md text-gray-900"
|
||||
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border border-gray-400 dark:border-gray-600 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md text-gray-900 dark:text-gray-100 bg-white dark:bg-slate-700"
|
||||
>
|
||||
<option value="increase">Erhöhen</option>
|
||||
<option value="decrease">Verringern</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="adjustmentReason" className="block text-sm font-medium text-gray-700">Grund (optional)</label>
|
||||
<label htmlFor="adjustmentReason" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Grund (optional)</label>
|
||||
<textarea
|
||||
id="adjustmentReason"
|
||||
name="adjustmentReason"
|
||||
rows={3}
|
||||
value={adjustmentReason}
|
||||
onChange={(e) => setAdjustmentReason(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-gray-200 px-4 py-2 text-sm font-medium text-gray-800 hover:bg-gray-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-2"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-gray-200 dark:bg-slate-700 px-4 py-2 text-sm font-medium text-gray-800 dark:text-gray-100 hover:bg-gray-300 dark:hover:bg-slate-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-2"
|
||||
onClick={() => setIsAdjustModalOpen(false)}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 dark:bg-indigo-500 dark:hover:bg-indigo-600"
|
||||
>
|
||||
Saldo anpassen
|
||||
</button>
|
||||
@ -472,6 +512,7 @@ const AdminPanelPage: NextPage = () => {
|
||||
</Dialog>
|
||||
</Transition>
|
||||
|
||||
{/* Modal für Transaktionsverlauf */}
|
||||
<Transition appear show={isTransactionsModalOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={() => setIsTransactionsModalOpen(false)}>
|
||||
<Transition.Child
|
||||
@ -484,13 +525,13 @@ const AdminPanelPage: NextPage = () => {
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm" />
|
||||
<div className="absolute inset-0 bg-black/30 backdrop-blur-sm dark:bg-slate-900/80" /> {/* Dark Mode Backdrop */}
|
||||
</Transition.Child>
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as="div"
|
||||
className="w-full max-w-3xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all"
|
||||
className="w-full max-w-3xl transform overflow-hidden rounded-2xl bg-white dark:bg-slate-800 p-6 text-left align-middle shadow-xl transition-all"
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
@ -499,47 +540,47 @@ const AdminPanelPage: NextPage = () => {
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel>
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">
|
||||
Transaktionsverlauf für {selectedUserForTransactions?.name || selectedUserForTransactions?.email}
|
||||
</Dialog.Title>
|
||||
<div className="mt-4">
|
||||
{isLoadingUserTransactions ? (
|
||||
<p>Lade Transaktionen...</p>
|
||||
<p className="dark:text-gray-300">Lade Transaktionen...</p>
|
||||
) : userTransactions.length > 0 ? (
|
||||
<div className="overflow-x-auto max-h-96">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50 sticky top-0">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-slate-700 sticky top-0">
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Datum</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Typ</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Betrag</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Admin</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Beschreibung</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Datum</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Typ</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Betrag</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Admin</th>
|
||||
<th className="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase">Beschreibung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<tbody className="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{userTransactions.map(tx => (
|
||||
<tr key={tx.id}>
|
||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">{new Date(tx.createdAt).toLocaleString('de-DE')}</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">{tx.type}</td>
|
||||
<td className={`px-4 py-2 whitespace-nowrap text-sm font-medium ${parseFloat(tx.amount) >= 0 ? 'text-red-600' : 'text-green-600'}`}>
|
||||
<tr key={tx.id} className="dark:hover:bg-slate-700/50">
|
||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">{new Date(tx.createdAt).toLocaleString('de-DE')}</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">{tx.type}</td>
|
||||
<td className={`px-4 py-2 whitespace-nowrap text-sm font-medium ${parseFloat(tx.amount) >= 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}`}>
|
||||
{parseFloat(tx.amount).toFixed(2)} €
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">{tx.triggeredByAdmin?.name || (tx.triggeredByAdminId ? 'Unbekannt' : '-')}</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700">{tx.description || '-'}</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">{tx.triggeredByAdmin?.name || (tx.triggeredByAdminId ? 'Unbekannt' : '-')}</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300">{tx.description || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-700">Keine Transaktionen für diesen Benutzer gefunden.</p>
|
||||
<p className="text-gray-700 dark:text-gray-300">Keine Transaktionen für diesen Benutzer gefunden.</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 px-4 py-2 text-sm font-medium text-blue-900 hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
className="inline-flex justify-center rounded-md border border-transparent bg-blue-100 dark:bg-blue-900 px-4 py-2 text-sm font-medium text-blue-900 dark:text-blue-100 hover:bg-blue-200 dark:hover:bg-blue-800 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2"
|
||||
onClick={() => setIsTransactionsModalOpen(false)}
|
||||
>
|
||||
Schließen
|
||||
@ -551,6 +592,7 @@ const AdminPanelPage: NextPage = () => {
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -3,19 +3,16 @@ import type { NextPage } from 'next';
|
||||
import { useSession, signOut } from 'next-auth/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
// WICHTIG: Stelle sicher, dass hier KEINE Importe von '@prisma/client' stehen.
|
||||
// Auch nicht für Typen wie 'Transaction'. Definiere Typen für das Frontend manuell.
|
||||
import { useTheme } from '../context/ThemeContext'; // NEU: useTheme importieren
|
||||
|
||||
// Typ für Transaktionen, wie sie vom Backend (API) erwartet werden
|
||||
// und im Frontend verwendet werden. Alle Felder sind primitive Typen oder Strings.
|
||||
interface DashboardTransaction {
|
||||
id: string;
|
||||
userId: string;
|
||||
amount: string; // Betrag ist ein String (z.B. "10.50")
|
||||
type: string; // Behalten wir im Datentyp, entfernen es nur aus der Anzeige
|
||||
amount: string;
|
||||
type: string;
|
||||
description: string | null;
|
||||
createdAt: string; // Datum als ISO-String (z.B. "2023-05-15T10:30:00.000Z")
|
||||
createdAt: string;
|
||||
triggeredByAdminId: string | null;
|
||||
triggeredByAdmin?: {
|
||||
id: string;
|
||||
@ -23,28 +20,70 @@ interface DashboardTransaction {
|
||||
} | null;
|
||||
}
|
||||
|
||||
// Typ für die Antwort von /api/user/balance
|
||||
interface BalanceResponse {
|
||||
balance: string | null; // Saldo kommt als String von der API
|
||||
balance: string | null;
|
||||
userName?: string | null;
|
||||
}
|
||||
|
||||
// Typ für die Antwort von /api/user/transactions
|
||||
interface TransactionsApiResponse {
|
||||
transactions: DashboardTransaction[]; // Verwende den oben definierten Typ
|
||||
transactions: DashboardTransaction[];
|
||||
totalTransactions: number;
|
||||
}
|
||||
|
||||
// Typ für die Antwort von /api/user/increase-balance
|
||||
interface IncreaseBalanceApiResponse {
|
||||
message: string;
|
||||
newBalance: string; // Neuer Saldo als String
|
||||
newBalance: string;
|
||||
transactionId: string;
|
||||
}
|
||||
|
||||
const ThemeSwitcher: React.FC = () => {
|
||||
const { theme, setTheme, resolvedTheme } = useTheme();
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => setMounted(true), []);
|
||||
|
||||
if (!mounted) {
|
||||
// Verhindere Hydration Mismatch, indem der Schalter erst nach dem Mounten gerendert wird
|
||||
return null;
|
||||
}
|
||||
|
||||
// toggleTheme wurde entfernt, da cycleTheme verwendet wird
|
||||
|
||||
// Erweiterter Theme-Wechsel: Light, Dark, System
|
||||
const cycleTheme = () => {
|
||||
if (theme === 'light') setTheme('dark');
|
||||
else if (theme === 'dark') setTheme('system');
|
||||
else setTheme('light');
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={cycleTheme}
|
||||
aria-label="Theme wechseln"
|
||||
className="p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
{/* Icon basierend auf resolvedTheme oder theme */}
|
||||
{resolvedTheme === 'dark' ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 text-yellow-400">
|
||||
<path fillRule="evenodd" d="M9.528 1.718a.75.75 0 0 1 .162.819A8.97 8.97 0 0 0 9 6a9 9 0 0 0 9 9 8.97 8.97 0 0 0 3.463-.69.75.75 0 0 1 .981.981A10.503 10.503 0 0 1 18 19.5a10.5 10.5 0 0 1-10.5-10.5A10.503 10.503 0 0 1 9.528 1.718Z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" className="w-6 h-6 text-orange-500">
|
||||
<path d="M12 2.25a.75.75 0 0 1 .75.75v2.25a.75.75 0 0 1-1.5 0V3a.75.75 0 0 1 .75-.75ZM7.5 12a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM18.894 6.166a.75.75 0 0 0-1.06-1.06l-1.591 1.59a.75.75 0 1 0 1.06 1.061l1.591-1.59ZM21.75 12a.75.75 0 0 1-.75.75h-2.25a.75.75 0 0 1 0-1.5H21a.75.75 0 0 1 .75.75ZM17.834 18.894a.75.75 0 0 0 1.06-1.06l-1.59-1.591a.75.75 0 1 0-1.061 1.06l1.59 1.591ZM12 18a.75.75 0 0 1 .75.75V21a.75.75 0 0 1-1.5 0v-2.25A.75.75 0 0 1 12 18ZM7.758 17.303a.75.75 0 0 0-1.061-1.06l-1.591 1.59a.75.75 0 0 0 1.06 1.061l1.591-1.59ZM6.166 7.758a.75.75 0 0 0-1.06 1.061l1.59 1.591a.75.75 0 0 0 1.061-1.06l-1.59-1.591ZM12 3.5A8.5 8.5 0 0 1 20.5 12c0 .312-.022.618-.065.916a.75.75 0 0 0-1.435-.276A7.001 7.001 0 0 0 12 5c-3.865 0-7 3.135-7 7 0 .096.005.19.014.284a.75.75 0 0 0 .727.71A.751.751 0 0 0 6.02 13a7.001 7.001 0 0 0 5.98-2.088.75.75 0 0 0-.364-1.118A8.502 8.502 0 0 1 12 3.5Z" />
|
||||
</svg>
|
||||
)}
|
||||
<span className="ml-2 text-xs text-gray-500 dark:text-gray-400 capitalize">
|
||||
{theme === 'system' ? `System (${resolvedTheme})` : theme}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
const DashboardPage: NextPage = () => {
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
// const { theme } = useTheme(); // Entfernt, da nicht direkt in DashboardPage verwendet
|
||||
|
||||
const [balance, setBalance] = useState<string | null>(null);
|
||||
const [userName, setUserName] = useState<string | null>(null);
|
||||
@ -54,14 +93,12 @@ const DashboardPage: NextPage = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
|
||||
// Weiterleitung, wenn nicht authentifiziert
|
||||
useEffect(() => {
|
||||
if (status === 'unauthenticated') {
|
||||
router.push('/auth/signin');
|
||||
}
|
||||
}, [status, router]);
|
||||
|
||||
// Saldo abrufen
|
||||
useEffect(() => {
|
||||
if (status === 'authenticated') {
|
||||
setIsLoadingBalance(true);
|
||||
@ -89,11 +126,10 @@ const DashboardPage: NextPage = () => {
|
||||
}
|
||||
}, [status, session]);
|
||||
|
||||
// Transaktionsverlauf abrufen
|
||||
const fetchTransactions = async () => {
|
||||
if (status === 'authenticated') {
|
||||
setIsLoadingTransactions(true);
|
||||
setError(null); // Fehler zurücksetzen vor neuem Abruf
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch('/api/user/transactions');
|
||||
if (!res.ok) {
|
||||
@ -121,8 +157,6 @@ const DashboardPage: NextPage = () => {
|
||||
}
|
||||
}, [status]);
|
||||
|
||||
|
||||
// Saldo erhöhen (oder "Ausgabe hinzufügen")
|
||||
const handleIncreaseBalance = async (amount: number) => {
|
||||
setError(null);
|
||||
setSuccessMessage(null);
|
||||
@ -132,7 +166,7 @@ const DashboardPage: NextPage = () => {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ amount }), // API erwartet 'amount'
|
||||
body: JSON.stringify({ amount }),
|
||||
});
|
||||
const data: IncreaseBalanceApiResponse = await response.json();
|
||||
if (!response.ok) {
|
||||
@ -140,7 +174,7 @@ const DashboardPage: NextPage = () => {
|
||||
}
|
||||
setBalance(data.newBalance);
|
||||
setSuccessMessage(data.message);
|
||||
await fetchTransactions(); // Transaktionsliste neu laden
|
||||
await fetchTransactions();
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof Error) {
|
||||
setError(err.message);
|
||||
@ -155,28 +189,29 @@ const DashboardPage: NextPage = () => {
|
||||
|
||||
if (status === 'loading') {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<p className="text-lg text-gray-600">Laden...</p>
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900">
|
||||
<p className="text-lg text-gray-600 dark:text-gray-300">Laden...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'unauthenticated' || !session?.user) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100">
|
||||
<p className="text-lg text-gray-600">Bitte anmelden, um das Dashboard zu sehen.</p>
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900">
|
||||
<p className="text-lg text-gray-600 dark:text-gray-300">Bitte anmelden, um das Dashboard zu sehen.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-100 p-4 sm:p-6 lg:p-8">
|
||||
<div className="min-h-screen bg-gray-100 dark:bg-slate-900 p-4 sm:p-6 lg:p-8 transition-colors duration-300">
|
||||
<header className="mb-8 flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-800">Willkommen, {userName || session.user.name}!</h1>
|
||||
<p className="text-gray-600">Deine Strichliste</p>
|
||||
<h1 className="text-3xl font-bold text-gray-800 dark:text-gray-100">Willkommen, {userName || session.user.name}!</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400">Deine Strichliste</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4"> {/* Container für Buttons im Header */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<ThemeSwitcher />
|
||||
{session?.user?.role === 'admin' && (
|
||||
<button
|
||||
onClick={() => router.push('/admin')}
|
||||
@ -194,30 +229,28 @@ const DashboardPage: NextPage = () => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{error && <div className="mb-4 p-3 bg-red-100 text-red-700 border border-red-300 rounded-md">{error}</div>}
|
||||
{successMessage && <div className="mb-4 p-3 bg-green-100 text-green-700 border border-green-300 rounded-md">{successMessage}</div>}
|
||||
{error && <div className="mb-4 p-3 bg-red-100 text-red-700 border border-red-300 rounded-md dark:bg-red-900 dark:text-red-200 dark:border-red-700">{error}</div>}
|
||||
{successMessage && <div className="mb-4 p-3 bg-green-100 text-green-700 border border-green-300 rounded-md dark:bg-green-900 dark:text-green-200 dark:border-green-700">{successMessage}</div>}
|
||||
|
||||
{/* Saldo Sektion */}
|
||||
<section className="mb-8 p-6 bg-white shadow-lg rounded-lg">
|
||||
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Dein aktueller Saldo</h2>
|
||||
<section className="mb-8 p-6 bg-white dark:bg-slate-800 shadow-lg rounded-lg">
|
||||
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-4">Dein aktueller Saldo</h2>
|
||||
{isLoadingBalance ? (
|
||||
<p className="text-gray-500">Saldo wird geladen...</p>
|
||||
<p className="text-gray-500 dark:text-gray-400">Saldo wird geladen...</p>
|
||||
) : (
|
||||
<p className="text-4xl font-bold text-indigo-600">
|
||||
<p className="text-4xl font-bold text-indigo-600 dark:text-indigo-400">
|
||||
{balance !== null ? `${parseFloat(balance).toFixed(2)} €` : 'N/A'}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Ausgaben hinzufügen Sektion */}
|
||||
<section className="mb-8 p-6 bg-white shadow-lg rounded-lg">
|
||||
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Ausgaben hinzufügen</h2>
|
||||
<section className="mb-8 p-6 bg-white dark:bg-slate-800 shadow-lg rounded-lg">
|
||||
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-4">Ausgaben hinzufügen</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4">
|
||||
{predefinedAmounts.map((amount) => (
|
||||
<button
|
||||
key={amount}
|
||||
onClick={() => handleIncreaseBalance(amount)}
|
||||
className="px-4 py-3 bg-indigo-600 text-white font-medium rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150"
|
||||
className="px-4 py-3 bg-indigo-600 text-white font-medium rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150 dark:bg-indigo-500 dark:hover:bg-indigo-600"
|
||||
>
|
||||
+ {amount.toFixed(2)} €
|
||||
</button>
|
||||
@ -225,55 +258,37 @@ const DashboardPage: NextPage = () => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Transaktionsverlauf Sektion */}
|
||||
<section className="p-6 bg-white shadow-lg rounded-lg">
|
||||
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Dein Transaktionsverlauf</h2>
|
||||
<section className="p-6 bg-white dark:bg-slate-800 shadow-lg rounded-lg">
|
||||
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-4">Dein Transaktionsverlauf</h2>
|
||||
{isLoadingTransactions ? (
|
||||
<p className="text-gray-500">Transaktionen werden geladen...</p>
|
||||
<p className="text-gray-500 dark:text-gray-400">Transaktionen werden geladen...</p>
|
||||
) : transactions.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-slate-700">
|
||||
<tr>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Datum</th>
|
||||
{/* <th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Typ</th> */} {/* Entfernt */}
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Betrag</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Beschreibung</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Datum</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Betrag</th>
|
||||
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Beschreibung</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
<tbody className="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{transactions.map((tx) => (
|
||||
<tr key={tx.id}>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{new Date(tx.createdAt).toLocaleString('de-DE')}</td>
|
||||
{/* <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{tx.type}</td> */} {/* Entfernt */}
|
||||
<td className={`px-6 py-4 whitespace-nowrap text-sm font-medium ${parseFloat(tx.amount) >= 0 ? 'text-red-600' : 'text-green-600'}`}>
|
||||
<tr key={tx.id} className="dark:hover:bg-slate-700/50">
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{new Date(tx.createdAt).toLocaleString('de-DE')}</td>
|
||||
<td className={`px-6 py-4 whitespace-nowrap text-sm font-medium ${parseFloat(tx.amount) >= 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}`}>
|
||||
{parseFloat(tx.amount).toFixed(2)} €
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{tx.description || '-'}</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">{tx.description || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500">Keine Transaktionen vorhanden.</p>
|
||||
<p className="text-gray-500 dark:text-gray-400">Keine Transaktionen vorhanden.</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* Link zum Admin Panel wurde in den Header verschoben */}
|
||||
{/*
|
||||
{session?.user?.role === 'admin' && (
|
||||
<section className="mt-8 p-6 bg-white shadow-lg rounded-lg">
|
||||
<h2 className="text-2xl font-semibold text-gray-700 mb-4">Admin Bereich</h2>
|
||||
<button
|
||||
onClick={() => router.push('/admin')}
|
||||
className="px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
|
||||
>
|
||||
Zum Admin Panel
|
||||
</button>
|
||||
</section>
|
||||
)}
|
||||
*/}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
27
tailwind.config.ts
Normal file
27
tailwind.config.ts
Normal file
@ -0,0 +1,27 @@
|
||||
// Datei: tailwind.config.ts
|
||||
import type { Config } from 'tailwindcss';
|
||||
|
||||
const config: Config = {
|
||||
darkMode: 'class', // Diese Zeile hinzufügen oder anpassen
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}", // Falls du einen components-Ordner hast
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}", // Falls du das App Router Verzeichnis verwendest
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
// Hier kannst du dein Theme erweitern, z.B. mit Dark-Mode-spezifischen Farben,
|
||||
// aber für den Anfang reichen die Standard-Tailwind-Farben mit dem 'dark:' Präfix.
|
||||
// Beispiel:
|
||||
// colors: {
|
||||
// 'primary-light': '#ffffff',
|
||||
// 'primary-dark': '#1a202c',
|
||||
// 'text-light': '#1f2937',
|
||||
// 'text-dark': '#f7fafc',
|
||||
// },
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
Loading…
x
Reference in New Issue
Block a user