Files
Speisekarte/components/AccountManager.tsx
T
florian.wilde a6165de10c Mehrsprachige Oberfläche + lokalisierte Gericht-/Bereichsnamen
Alle Seiten und Komponenten (öffentliche Speisekarte, Login, Kantinen-
und Admin-Bereich, eigenes Konto) verwenden jetzt die Wörterbücher aus
lib/i18n statt fest codierter deutscher Texte. Gerichte/Bereiche/
Allergene/Zusatzstoffe zeigen den Namen zur aktuell gewählten Sprache;
Gerichte- und Kennzeichnungs-Formulare bekommen Englisch-/Polnisch-
Felder mit Übersetzen-Button (siehe vorherigen Commit). Datumsformat-
ierung (formatDateLong) nimmt jetzt ein locale-Argument statt fix
'de-DE'.
2026-07-18 02:08:03 +02:00

334 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useMemo, useState } from "react";
import { useRouter } from "next/navigation";
import { createClient } from "@/lib/supabase/client";
import { formatMessage } from "@/lib/i18n/shared";
import type { Dict } from "@/lib/i18n/shared";
import PasskeyManager from "@/components/PasskeyManager";
import { useDict } from "@/components/i18n/LocaleProvider";
function authErrorMessage(dict: Dict, message: string): string {
if (/invalid login credentials/i.test(message))
return dict.account.wrongCurrentPassword;
if (/should be at least/i.test(message)) return dict.account.passwordTooShort;
if (/email.*already|already.*registered/i.test(message))
return dict.account.emailAlreadyUsed;
return message;
}
export default function AccountManager({
role,
initialFullName,
initialEmail,
initialPasswordAuthEnabled,
}: {
role: string;
initialFullName: string;
initialEmail: string;
initialPasswordAuthEnabled: boolean;
}) {
const supabase = useMemo(() => createClient(), []);
const router = useRouter();
const dict = useDict();
const roleLabel = role === "admin" ? dict.common.roleAdmin : dict.common.roleStaff;
// Persönliche Daten
const [fullName, setFullName] = useState(initialFullName);
const [nameError, setNameError] = useState<string | null>(null);
const [nameInfo, setNameInfo] = useState<string | null>(null);
const [nameBusy, setNameBusy] = useState(false);
// E-Mail-Adresse
const [email, setEmail] = useState(initialEmail);
const [emailError, setEmailError] = useState<string | null>(null);
const [emailInfo, setEmailInfo] = useState<string | null>(null);
const [emailBusy, setEmailBusy] = useState(false);
// Passwort
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [newPasswordRepeat, setNewPasswordRepeat] = useState("");
const [pwError, setPwError] = useState<string | null>(null);
const [pwInfo, setPwInfo] = useState<string | null>(null);
const [pwBusy, setPwBusy] = useState(false);
// Passwort-Anmeldung an/aus
const [passwordAuthEnabled, setPasswordAuthEnabled] = useState(
initialPasswordAuthEnabled
);
const [passkeyCount, setPasskeyCount] = useState<number | null>(null);
const [authError, setAuthError] = useState<string | null>(null);
const [authInfo, setAuthInfo] = useState<string | null>(null);
const [authBusy, setAuthBusy] = useState(false);
useEffect(() => {
supabase.auth.passkey
.list()
.then(({ data }) => setPasskeyCount((data ?? []).length));
}, [supabase]);
async function saveName(e: React.FormEvent) {
e.preventDefault();
setNameError(null);
setNameInfo(null);
const name = fullName.trim();
if (!name) {
setNameError(dict.account.nameRequired);
return;
}
setNameBusy(true);
const { error } = await supabase
.from("profiles")
.update({ full_name: name })
.eq("id", (await supabase.auth.getUser()).data.user?.id ?? "");
if (error)
setNameError(formatMessage(dict.account.saveError, { message: error.message }));
else {
setNameInfo(dict.account.nameSaved);
router.refresh();
}
setNameBusy(false);
}
async function saveEmail(e: React.FormEvent) {
e.preventDefault();
setEmailError(null);
setEmailInfo(null);
const next = email.trim();
if (!next || next === initialEmail) return;
setEmailBusy(true);
const { error } = await supabase.auth.updateUser({ email: next });
if (error) {
setEmailError(
formatMessage(dict.account.changeError, {
message: authErrorMessage(dict, error.message),
})
);
} else {
setEmailInfo(dict.account.emailSent);
}
setEmailBusy(false);
}
async function savePassword(e: React.FormEvent) {
e.preventDefault();
setPwError(null);
setPwInfo(null);
if (newPassword.length < 8) {
setPwError(dict.account.passwordTooShort);
return;
}
if (newPassword !== newPasswordRepeat) {
setPwError(dict.account.passwordMismatch);
return;
}
setPwBusy(true);
// Zur Verifizierung: aktuelles Passwort gegenprüfen, bevor geändert wird.
const { error: verifyError } = await supabase.auth.signInWithPassword({
email: initialEmail,
password: currentPassword,
});
if (verifyError) {
setPwError(authErrorMessage(dict, verifyError.message));
setPwBusy(false);
return;
}
const { error } = await supabase.auth.updateUser({ password: newPassword });
if (error) {
setPwError(
formatMessage(dict.account.changeError, {
message: authErrorMessage(dict, error.message),
})
);
} else {
setPwInfo(dict.account.passwordChanged);
setCurrentPassword("");
setNewPassword("");
setNewPasswordRepeat("");
}
setPwBusy(false);
}
async function togglePasswordAuth() {
setAuthError(null);
setAuthInfo(null);
const next = !passwordAuthEnabled;
if (!next && (passkeyCount ?? 0) === 0) {
setAuthError(dict.account.passwordAuthNeedsPasskey);
return;
}
if (!next && !window.confirm(dict.account.confirmDisablePasswordAuth)) return;
setAuthBusy(true);
const { error } = await supabase
.from("profiles")
.update({ password_auth_enabled: next })
.eq("id", (await supabase.auth.getUser()).data.user?.id ?? "");
if (error) {
setAuthError(formatMessage(dict.account.saveError, { message: error.message }));
} else {
setPasswordAuthEnabled(next);
setAuthInfo(
next ? dict.account.passwordAuthEnabled : dict.account.passwordAuthDisabled
);
}
setAuthBusy(false);
}
return (
<div className="max-w-3xl space-y-6">
<div>
<h1 className="text-xl font-bold text-[var(--c-primary)]">
{dict.account.title}
</h1>
<p className="text-sm text-gray-500">
{initialEmail} – {roleLabel}
</p>
</div>
<section className="card">
<h2 className="font-semibold mb-3">{dict.account.personalTitle}</h2>
<form onSubmit={saveName} className="space-y-3">
<div className="max-w-80">
<label className="label" htmlFor="account-name">
{dict.common.name}
</label>
<input
id="account-name"
className="input"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
required
/>
</div>
{nameError && (
<p className="text-sm text-[var(--c-del)]" role="alert">
{nameError}
</p>
)}
{nameInfo && <p className="text-sm text-green-700">{nameInfo}</p>}
<button type="submit" className="btn btn-add" disabled={nameBusy}>
{dict.account.saveName}
</button>
</form>
</section>
<section className="card">
<h2 className="font-semibold mb-3">{dict.account.emailTitle}</h2>
<form onSubmit={saveEmail} className="space-y-3">
<div className="max-w-80">
<label className="label" htmlFor="account-email">
{dict.common.email}
</label>
<input
id="account-email"
type="email"
className="input"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
{emailError && (
<p className="text-sm text-[var(--c-del)]" role="alert">
{emailError}
</p>
)}
{emailInfo && <p className="text-sm text-green-700">{emailInfo}</p>}
<button
type="submit"
className="btn btn-add"
disabled={emailBusy || email.trim() === initialEmail}
>
{dict.account.changeEmail}
</button>
</form>
</section>
<section className="card">
<h2 className="font-semibold mb-3">{dict.account.passwordTitle}</h2>
<form onSubmit={savePassword} className="space-y-3">
<div className="max-w-80">
<label className="label" htmlFor="account-current-password">
{dict.account.currentPassword}
</label>
<input
id="account-current-password"
type="password"
className="input"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
autoComplete="current-password"
required
/>
</div>
<div className="max-w-80">
<label className="label" htmlFor="account-new-password">
{dict.account.newPassword}
</label>
<input
id="account-new-password"
type="password"
className="input"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
autoComplete="new-password"
minLength={8}
required
/>
</div>
<div className="max-w-80">
<label className="label" htmlFor="account-new-password-repeat">
{dict.account.newPasswordRepeat}
</label>
<input
id="account-new-password-repeat"
type="password"
className="input"
value={newPasswordRepeat}
onChange={(e) => setNewPasswordRepeat(e.target.value)}
autoComplete="new-password"
minLength={8}
required
/>
</div>
{pwError && (
<p className="text-sm text-[var(--c-del)]" role="alert">
{pwError}
</p>
)}
{pwInfo && <p className="text-sm text-green-700">{pwInfo}</p>}
<button type="submit" className="btn btn-add" disabled={pwBusy}>
{dict.account.changePassword}
</button>
</form>
</section>
<section className="card">
<h2 className="font-semibold mb-1">{dict.account.passwordAuthTitle}</h2>
<p className="text-sm text-gray-600 mb-3">{dict.account.passwordAuthHint}</p>
{authError && (
<p className="mb-2 text-sm text-[var(--c-del)]" role="alert">
{authError}
</p>
)}
{authInfo && <p className="mb-2 text-sm text-green-700">{authInfo}</p>}
<label className="flex items-center gap-2 cursor-pointer text-sm w-fit">
<input
type="checkbox"
checked={passwordAuthEnabled}
onChange={togglePasswordAuth}
disabled={authBusy || passkeyCount === null}
/>
{dict.account.passwordAuthCheckbox}
</label>
</section>
<PasskeyManager />
</div>
);
}