163 lines
5.1 KiB
TypeScript
163 lines
5.1 KiB
TypeScript
'use client';
|
|
|
|
/**
|
|
* Profil-Modal: der angemeldete Benutzer kann seinen Anzeigenamen
|
|
* (Username) aendern. E-Mail und Rolle sind nur lesbar dargestellt.
|
|
*
|
|
* Der Username ist optional und eindeutig; beim Speichern prueft der
|
|
* Server das Format und die Eindeutigkeit. Ein leerer Wert loescht
|
|
* den Username (das Badge faellt auf die E-Mail zurueck).
|
|
*
|
|
* Wird aus der Sidebar ueber das Profil-Badge geoeffnet; nach dem
|
|
* Speichern aktualisiert router.refresh() die Sidebar sofort.
|
|
*/
|
|
import { useCallback, useEffect, useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { Button, ErrorMessage, Input, Label, Badge } from '@/components/ui/ui';
|
|
import { ModalShell } from '@/components/ui/ModalShell';
|
|
|
|
interface Profile {
|
|
id: string;
|
|
email: string;
|
|
username: string | null;
|
|
role: 'ADMIN' | 'USER';
|
|
}
|
|
|
|
export function ProfileModal({ onClose }: { onClose: () => void }) {
|
|
const router = useRouter();
|
|
const [profile, setProfile] = useState<Profile | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [success, setSuccess] = useState(false);
|
|
|
|
const loadProfile = useCallback(async () => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
try {
|
|
const response = await fetch('/api/me/profile', { cache: 'no-store' });
|
|
if (!response.ok) {
|
|
throw new Error('Profil konnte nicht geladen werden.');
|
|
}
|
|
const data = (await response.json()) as { user: Profile };
|
|
setProfile(data.user);
|
|
} catch (loadError) {
|
|
setError(
|
|
loadError instanceof Error
|
|
? loadError.message
|
|
: 'Profil konnte nicht geladen werden.',
|
|
);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
void loadProfile();
|
|
}, [loadProfile]);
|
|
|
|
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
setIsSaving(true);
|
|
setError(null);
|
|
setSuccess(false);
|
|
|
|
const formData = new FormData(event.currentTarget);
|
|
const username = String(formData.get('username') ?? '').trim();
|
|
|
|
try {
|
|
const response = await fetch('/api/me/profile', {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: username || null }),
|
|
});
|
|
if (!response.ok) {
|
|
const data = (await response.json().catch(() => ({}))) as { error?: string };
|
|
setError(data.error ?? 'Speichern fehlgeschlagen.');
|
|
return;
|
|
}
|
|
const data = (await response.json()) as { user: Profile };
|
|
setProfile(data.user);
|
|
setSuccess(true);
|
|
// Sidebar + Seite aktualisieren, damit der neue Name ueberall
|
|
// sofort sichtbar wird.
|
|
router.refresh();
|
|
} catch {
|
|
setError('Netzwerkfehler. Bitte erneut versuchen.');
|
|
} finally {
|
|
setIsSaving(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<ModalShell
|
|
title="Profil"
|
|
ariaLabel="Profil bearbeiten"
|
|
onClose={onClose}
|
|
>
|
|
{isLoading ? (
|
|
<p className="py-4 text-sm text-slate-600 dark:text-slate-400">
|
|
Profil wird geladen …
|
|
</p>
|
|
) : !profile ? (
|
|
<ErrorMessage message={error ?? 'Profil konnte nicht geladen werden.'} />
|
|
) : (
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<ErrorMessage message={error} />
|
|
{success && (
|
|
<p className="rounded-md bg-slate-100 px-3 py-2 text-sm text-slate-700 dark:bg-slate-800 dark:text-slate-300">
|
|
Profil wurde gespeichert.
|
|
</p>
|
|
)}
|
|
|
|
<div>
|
|
<Label htmlFor="profile-email">E-Mail</Label>
|
|
<Input
|
|
id="profile-email"
|
|
type="email"
|
|
value={profile.email}
|
|
readOnly
|
|
disabled
|
|
aria-readonly
|
|
/>
|
|
<p className="mt-1 text-xs text-slate-500">
|
|
Die E-Mail-Adresse kann nicht geaendert werden.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label htmlFor="profile-username">Benutzername</Label>
|
|
<Input
|
|
id="profile-username"
|
|
name="username"
|
|
type="text"
|
|
maxLength={32}
|
|
defaultValue={profile.username ?? ''}
|
|
placeholder="z. B. max_mustermann"
|
|
autoComplete="username"
|
|
/>
|
|
<p className="mt-1 text-xs text-slate-500">
|
|
2-32 Zeichen; Buchstaben, Ziffern, _ - . erlaubt. Leer lassen, um
|
|
den Namen zu entfernen (es wird dann die E-Mail angezeigt).
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Rolle</Label>
|
|
<div className="pt-1">
|
|
<Badge variant={profile.role === 'ADMIN' ? 'accent' : 'neutral'}>
|
|
{profile.role === 'ADMIN' ? 'Administrator' : 'Benutzer'}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end">
|
|
<Button type="submit" disabled={isSaving}>
|
|
{isSaving ? 'Speichern …' : 'Speichern'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
</ModalShell>
|
|
);
|
|
} |