add dark mode

This commit is contained in:
Elias Bennour 2025-05-22 00:00:30 +02:00
parent 380084b675
commit f7d462b359
5 changed files with 355 additions and 162 deletions

View 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;
};

View File

@ -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;

View File

@ -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>
);
};

View File

@ -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
View 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;