add profile page
This commit is contained in:
parent
da349fb406
commit
ad53609099
100
src/pages/api/user/profile.ts
Normal file
100
src/pages/api/user/profile.ts
Normal 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` });
|
||||
}
|
||||
}
|
||||
@ -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
198
src/pages/profile.tsx
Normal 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">
|
||||
← 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;
|
||||
Loading…
x
Reference in New Issue
Block a user