Feature: Community-Profil-Card im Einstellungen-Profil-Tab
Erganzt den Profil-Tab um eine Community-Profil-Karte (UI-Teil zum
Backend-Fundament 634e67c):
- Benutzername-Feld mit Hinweis auf den vergebenen Handle (Username#XXXX).
- Bio-Textarea (max 280 Zeichen) mit Zeichenzaehler.
- Sichtbarkeits-Checkbox (Privacy-by-Default: aus) mit Datenschutzhinweis,
dass Gewicht, Kalorien und Ziele immer privat bleiben.
- Eigener Submit, der setMyHandle (nur bei eingegebenem Username) und
updateMyCommunityProfile aufruft und myProfile invalidiert.
Typecheck gruen (db/api/admin), Admin-Tests 10/10.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
634e67c602
commit
9ab6db9c0e
1 changed files with 104 additions and 0 deletions
|
|
@ -139,6 +139,10 @@ export default function EinstellungenPage() {
|
|||
const [postalCode, setPostalCode] = useState("");
|
||||
const [city, setCity] = useState("");
|
||||
const [image, setImage] = useState<string | null>(null);
|
||||
// Community-Profil (Handle, Bio, Sichtbarkeit)
|
||||
const [username, setUsername] = useState("");
|
||||
const [bio, setBio] = useState("");
|
||||
const [profileVisible, setProfileVisible] = useState(false);
|
||||
const [dietPreferences, setDietPreferences] = useState<DietPreference[]>([
|
||||
"vegan",
|
||||
"vegetarian",
|
||||
|
|
@ -173,6 +177,9 @@ export default function EinstellungenPage() {
|
|||
setPostalCode(profileQuery.data.postalCode ?? "");
|
||||
setCity(profileQuery.data.city ?? "");
|
||||
setImage(profileQuery.data.image ?? null);
|
||||
setUsername(profileQuery.data.username ?? "");
|
||||
setBio(profileQuery.data.bio ?? "");
|
||||
setProfileVisible(profileQuery.data.profileVisible ?? false);
|
||||
}
|
||||
}, [profileQuery.data]);
|
||||
|
||||
|
|
@ -191,6 +198,34 @@ export default function EinstellungenPage() {
|
|||
},
|
||||
});
|
||||
|
||||
const setMyHandle = trpc.users.setMyHandle.useMutation();
|
||||
const updateMyCommunityProfile = trpc.users.updateMyCommunityProfile.useMutation();
|
||||
const communityPending = setMyHandle.isPending || updateMyCommunityProfile.isPending;
|
||||
|
||||
const onSubmitCommunity = async (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
setStatusMessage(null);
|
||||
setErrorMessage(null);
|
||||
try {
|
||||
// Handle nur setzen, wenn ein Username eingegeben wurde. Den Discriminator
|
||||
// vergibt der Server stabil bei der Erst-Vergabe.
|
||||
const trimmedUsername = username.trim();
|
||||
if (trimmedUsername) {
|
||||
await setMyHandle.mutateAsync({ username: trimmedUsername });
|
||||
}
|
||||
await updateMyCommunityProfile.mutateAsync({
|
||||
bio: bio.trim() || null,
|
||||
profileVisible,
|
||||
});
|
||||
await utils.users.myProfile.invalidate();
|
||||
setStatusMessage("Community-Profil gespeichert.");
|
||||
} catch (error) {
|
||||
setErrorMessage(
|
||||
error instanceof Error ? error.message : "Speichern fehlgeschlagen.",
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const changeMyPassword = trpc.users.changeMyPassword.useMutation({
|
||||
onSuccess: () => {
|
||||
setCurrentPassword("");
|
||||
|
|
@ -562,6 +597,75 @@ export default function EinstellungenPage() {
|
|||
</SectionCard>
|
||||
) : null}
|
||||
|
||||
{active === "profil" ? (
|
||||
<form onSubmit={onSubmitCommunity}>
|
||||
<SectionCard
|
||||
title="Community-Profil"
|
||||
description="Dein öffentlicher Auftritt. Gewicht, Kalorien und Ziele bleiben immer privat."
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className={labelClass} htmlFor="community-username">
|
||||
Benutzername
|
||||
</label>
|
||||
<input
|
||||
id="community-username"
|
||||
value={username}
|
||||
onChange={(event) => setUsername(event.target.value)}
|
||||
placeholder="z. B. KochHeld"
|
||||
className={inputClass}
|
||||
/>
|
||||
<p className="mt-1.5 text-xs text-gray-500">
|
||||
{profileQuery.data?.handle
|
||||
? `Dein Handle: ${profileQuery.data.handle}`
|
||||
: "Buchstaben, Ziffern, _ und -. Eine vierstellige Kennung (#XXXX) wird automatisch und dauerhaft vergeben."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className={labelClass} htmlFor="community-bio">
|
||||
Über mich
|
||||
</label>
|
||||
<textarea
|
||||
id="community-bio"
|
||||
value={bio}
|
||||
maxLength={280}
|
||||
rows={3}
|
||||
onChange={(event) => setBio(event.target.value)}
|
||||
placeholder="Kurz über dich – optional."
|
||||
className={inputClass}
|
||||
/>
|
||||
<p className="mt-1.5 text-right text-xs text-gray-500">
|
||||
{bio.length}/280
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label className="flex items-start gap-3 rounded-lg bg-gray-50 p-3 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={profileVisible}
|
||||
onChange={(event) => setProfileVisible(event.target.checked)}
|
||||
className="mt-0.5 h-4 w-4 shrink-0"
|
||||
/>
|
||||
<span className="text-gray-700">
|
||||
<span className="font-semibold">Profil öffentlich sichtbar machen</span>
|
||||
<span className="mt-0.5 block text-gray-500">
|
||||
Andere können dann deinen Benutzernamen und deine Bio sehen.
|
||||
Gewicht, Kalorien und Ziele bleiben immer privat.
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button type="submit" disabled={communityPending} className={buttonPrimary}>
|
||||
{communityPending ? "Speichert..." : "Community-Profil speichern"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</SectionCard>
|
||||
</form>
|
||||
) : null}
|
||||
|
||||
{active === "ernaehrung" ? (
|
||||
<SectionCard
|
||||
title="Rezept- und Mahlzeiten-Präferenzen"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue