add profile page

This commit is contained in:
Elias Bennour 2025-05-22 00:33:05 +02:00
parent da349fb406
commit ad53609099
3 changed files with 326 additions and 9 deletions

View File

@ -0,0 +1,100 @@
// Datei: pages/api/user/profile.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { getServerSession } from 'next-auth/next';
import { authOptions } from '../auth/[...nextauth]'; // Pfad anpassen
import prisma from '../../../lib/prisma'; // Pfad anpassen
interface ProfileUpdateRequest {
name?: string;
// email?: string; // E-Mail-Änderung ist komplexer wegen Verifizierung, lassen wir erstmal weg
}
interface ProfileResponse {
id: string;
name: string | null;
email: string | null;
// Füge hier weitere Felder hinzu, die du im Profil anzeigen möchtest
}
interface ErrorResponse {
message: string;
errors?: { field: string; message: string }[];
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse<ProfileResponse | ErrorResponse | { message: string }>
) {
const session = await getServerSession(req, res, authOptions);
if (!session || !session.user || !session.user.id) {
return res.status(401).json({ message: 'Nicht autorisiert.' });
}
const userId = session.user.id;
if (req.method === 'GET') {
try {
const user = await prisma.user.findUnique({
where: { id: userId },
select: {
id: true,
name: true,
email: true,
// Wähle hier weitere Felder aus, die du im Profil anzeigen möchtest
},
});
if (!user) {
return res.status(404).json({ message: 'Benutzer nicht gefunden.' });
}
return res.status(200).json(user);
} catch (error) {
console.error('Fehler beim Abrufen des Profils:', error);
return res.status(500).json({ message: 'Fehler beim Abrufen des Profils.' });
}
} else if (req.method === 'PUT') {
const { name } = req.body as ProfileUpdateRequest;
const validationErrors: { field: string; message: string }[] = [];
if (name !== undefined) {
if (typeof name !== 'string' || name.trim().length < 2) {
validationErrors.push({ field: 'name', message: 'Name ist erforderlich und muss mindestens 2 Zeichen lang sein.' });
}
} else {
// Wenn nichts zum Aktualisieren gesendet wird
return res.status(400).json({ message: 'Keine Daten zum Aktualisieren angegeben.' });
}
if (validationErrors.length > 0) {
return res.status(400).json({ message: 'Validierungsfehler.', errors: validationErrors });
}
try {
const updatedUser = await prisma.user.update({
where: { id: userId },
data: {
...(name && { name: name.trim() }),
},
select: { // Gib die aktualisierten Daten zurück
id: true,
name: true,
email: true,
}
});
// Wichtig: Die Session muss nicht unbedingt serverseitig aktualisiert werden,
// da NextAuth die Session bei der nächsten Anfrage neu validiert/aufbaut.
// Für eine sofortige UI-Aktualisierung muss das Frontend die neuen Daten verwenden.
return res.status(200).json({ message: 'Profil erfolgreich aktualisiert.', ...updatedUser });
} catch (error) {
console.error('Fehler beim Aktualisieren des Profils:', error);
return res.status(500).json({ message: 'Fehler beim Aktualisieren des Profils.' });
}
} else {
res.setHeader('Allow', ['GET', 'PUT']);
return res.status(405).json({ message: `Method ${req.method} Not Allowed` });
}
}

View File

@ -2,6 +2,7 @@
import type { NextPage } from 'next';
import { useSession, signOut } from 'next-auth/react';
import { useRouter } from 'next/router';
import Link from 'next/link'; // Link-Komponente importieren
import { useEffect, useState } from 'react';
interface DashboardTransaction {
@ -59,8 +60,18 @@ const DashboardPage: NextPage = () => {
useEffect(() => {
if (status === 'authenticated') {
setIsLoadingBalance(true);
fetch('/api/user/balance')
fetch('/api/user/profile') // Profil-API aufrufen, um ggf. aktualisierten Namen zu bekommen
.then(async (res) => {
if (!res.ok) {
// Versuche, den Saldo trotzdem zu laden, falls Profil nicht kritisch ist
console.warn('Fehler beim Laden des Profils für den Namen, fahre mit Saldo fort.');
return fetch('/api/user/balance'); // Lade Saldo separat
}
const profileData = await res.json();
setUserName(profileData.name || session?.user?.name || null);
return fetch('/api/user/balance'); // Lade Saldo
})
.then(async (res) => { // Dieser .then Block ist für die Saldo-Antwort
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.message || 'Fehler beim Laden des Saldos');
@ -69,19 +80,22 @@ const DashboardPage: NextPage = () => {
})
.then((data) => {
setBalance(data.balance);
setUserName(data.userName || session?.user?.name || null);
// userName wird bereits im vorherigen .then gesetzt, falls Profil erfolgreich geladen wurde
if (!userName && session?.user?.name) { // Fallback, falls Profil-Fetch fehlschlug
setUserName(session.user.name);
}
})
.catch((err: unknown) => {
if (err instanceof Error) {
setError(err.message);
} else {
setError('Unbekannter Fehler beim Laden des Saldos aufgetreten.');
setError('Unbekannter Fehler beim Laden der Dashboard-Daten.');
}
console.error("Fehler Saldo:", err);
console.error("Fehler Dashboard Daten:", err);
})
.finally(() => setIsLoadingBalance(false));
}
}, [status, session]);
}, [status, session, userName]); // userName als Abhängigkeit hinzugefügt, um Header ggf. neu zu rendern
const fetchTransactions = async () => {
if (status === 'authenticated') {
@ -157,7 +171,7 @@ const DashboardPage: NextPage = () => {
handleAddExpense(amountNumber, true);
};
const predefinedAmounts = [0.10, 0.20, 0.50, 1.00, 1.50, 2.00]; // 1.50 hinzugefügt
const predefinedAmounts = [0.10, 0.20, 0.50, 1.00, 1.50, 2.00];
if (status === 'loading') {
return (
@ -179,10 +193,15 @@ const DashboardPage: NextPage = () => {
<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 flex-col sm:flex-row sm:justify-between sm:items-center">
<div className="mb-4 sm:mb-0">
<h1 className="text-3xl font-bold text-gray-800 dark:text-gray-100">Willkommen, {userName || session.user.name}!</h1>
<h1 className="text-3xl font-bold text-gray-800 dark:text-gray-100">Willkommen, {userName || session.user.name || 'Benutzer'}!</h1>
<p className="text-gray-600 dark:text-gray-400">Deine Strichliste</p>
</div>
<div className="flex flex-col space-y-2 sm:flex-row sm:space-y-0 sm:space-x-4">
<div className="flex flex-col items-stretch space-y-2 sm:flex-row sm:items-center sm:space-y-0 sm:space-x-4"> {/* items-stretch für mobile Buttons */}
<Link href="/profile" legacyBehavior>
<a className="w-full sm:w-auto px-4 py-2 text-center 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">
Profil
</a>
</Link>
{session?.user?.role === 'admin' && (
<button
onClick={() => router.push('/admin')}
@ -216,7 +235,7 @@ const DashboardPage: NextPage = () => {
<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="mb-6 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4"> {/* md:grid-cols-6 für bessere Darstellung mit mehr Buttons */}
<div className="mb-6 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4">
{predefinedAmounts.map((amount) => (
<button
key={amount}

198
src/pages/profile.tsx Normal file
View File

@ -0,0 +1,198 @@
// Datei: pages/profile.tsx
import type { NextPage } from 'next';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/router';
import Link from 'next/link';
import { useEffect, useState, FormEvent } from 'react';
interface UserProfile {
id: string;
name: string | null;
email: string | null;
}
interface ProfileUpdateResponse extends UserProfile {
message?: string; // Für Erfolgsmeldungen von der API
}
interface ApiErrorResponse {
message: string;
errors?: { field: string; message: string }[];
}
const ProfilePage: NextPage = () => {
const { data: session, status } = useSession();
const router = useRouter();
const [profile, setProfile] = useState<UserProfile | null>(null);
const [name, setName] = useState<string>('');
const [isLoading, setIsLoading] = useState(true);
const [isUpdating, setIsUpdating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [fieldErrors, setFieldErrors] = useState<{ field: string; message: string }[]>([]);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
// Weiterleitung und initiales Laden der Profildaten
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/auth/signin');
} else if (status === 'authenticated') {
setIsLoading(true);
fetch('/api/user/profile')
.then(async (res) => {
if (!res.ok) {
const errorData: ApiErrorResponse = await res.json();
throw new Error(errorData.message || 'Fehler beim Laden des Profils');
}
return res.json() as Promise<UserProfile>;
})
.then((data) => {
setProfile(data);
setName(data.name || '');
})
.catch((err: unknown) => {
if (err instanceof Error) {
setError(err.message);
} else {
setError('Unbekannter Fehler beim Laden des Profils.');
}
console.error("Fehler beim Laden des Profils:", err);
})
.finally(() => setIsLoading(false));
}
}, [status, router]);
const getFieldError = (fieldName: string): string | undefined => {
return fieldErrors.find(err => err.field === fieldName)?.message;
}
const handleNameChangeSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsUpdating(true);
setError(null);
setFieldErrors([]);
setSuccessMessage(null);
if (!name.trim() || name.trim().length < 2) {
setFieldErrors([{ field: 'name', message: 'Name ist erforderlich und muss mindestens 2 Zeichen lang sein.' }]);
setIsUpdating(false);
return;
}
try {
const response = await fetch('/api/user/profile', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: name.trim() }),
});
const data: ProfileUpdateResponse | ApiErrorResponse = await response.json();
if (!response.ok) {
const errorData = data as ApiErrorResponse;
if (errorData.errors) {
setFieldErrors(errorData.errors);
}
throw new Error(errorData.message || 'Fehler beim Aktualisieren des Namens.');
}
const successData = data as ProfileUpdateResponse;
setSuccessMessage(successData.message || 'Name erfolgreich aktualisiert!');
setProfile(prev => prev ? { ...prev, name: successData.name } : null);
setName(successData.name || '');
// Optional: Session clientseitig aktualisieren, wenn der Name in der Session verwendet wird
// Dies erfordert, dass die API die aktualisierten Daten zurückgibt und die Session
// eine Struktur hat, die dies unterstützt. NextAuth aktualisiert die Session
// normalerweise bei der nächsten Interaktion oder beim Neuladen.
// Für eine sofortige Aktualisierung im Header (falls der Name dort angezeigt wird):
// Man könnte `useSession().update({ name: successData.name })` verwenden,
// wenn die `update` Funktion für die Session-Strategie verfügbar ist.
} catch (err: unknown) {
if (err instanceof Error) {
setError(err.message);
} else {
setError('Ein unbekannter Fehler ist aufgetreten.');
}
console.error("Fehler beim Aktualisieren des Namens:", err);
} finally {
setIsUpdating(false);
}
};
if (status === 'loading' || isLoading) {
return (
<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">Profil wird geladen...</p>
</div>
);
}
if (!session || !profile) {
// Sollte durch useEffect bereits umgeleitet werden, aber als Fallback
return (
<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 Profil zu sehen.</p>
</div>
);
}
return (
<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">
<Link href="/dashboard" className="text-indigo-600 dark:text-indigo-400 hover:underline">
&larr; Zurück zum Dashboard
</Link>
</header>
<div className="max-w-xl mx-auto bg-white dark:bg-slate-800 shadow-lg rounded-lg p-6 sm:p-8">
<h1 className="text-3xl font-bold text-gray-800 dark:text-gray-100 mb-6">Profil verwalten</h1>
{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>}
<div className="mb-6">
<h2 className="text-xl font-semibold text-gray-700 dark:text-gray-200">Aktuelle Informationen</h2>
<p className="text-gray-600 dark:text-gray-400 mt-2">
<span className="font-medium">E-Mail:</span> {profile.email}
</p>
<p className="text-gray-600 dark:text-gray-400">
<span className="font-medium">Aktueller Name:</span> {profile.name || 'Nicht festgelegt'}
</p>
</div>
<form onSubmit={handleNameChangeSubmit} className="space-y-6 border-t border-gray-200 dark:border-gray-700 pt-6">
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Name ändern
</label>
<div className="mt-1">
<input
type="text"
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
className={`appearance-none block w-full px-3 py-2 border rounded-md shadow-sm placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none sm:text-sm bg-white dark:bg-slate-700 text-gray-900 dark:text-gray-100 ${getFieldError('name') ? 'border-red-500 focus:ring-red-500 focus:border-red-500' : 'border-gray-300 dark:border-gray-600 focus:ring-indigo-500 focus:border-indigo-500'}`}
/>
{getFieldError('name') && <p className="mt-1 text-xs text-red-600 dark:text-red-400">{getFieldError('name')}</p>}
</div>
</div>
<div>
<button
type="submit"
disabled={isUpdating || name === (profile.name || '')}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 dark:bg-indigo-500 dark:hover:bg-indigo-600"
>
{isUpdating ? 'Speichern...' : 'Namen speichern'}
</button>
</div>
</form>
</div>
</div>
);
};
export default ProfilePage;