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 [postalCode, setPostalCode] = useState("");
|
||||||
const [city, setCity] = useState("");
|
const [city, setCity] = useState("");
|
||||||
const [image, setImage] = useState<string | null>(null);
|
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[]>([
|
const [dietPreferences, setDietPreferences] = useState<DietPreference[]>([
|
||||||
"vegan",
|
"vegan",
|
||||||
"vegetarian",
|
"vegetarian",
|
||||||
|
|
@ -173,6 +177,9 @@ export default function EinstellungenPage() {
|
||||||
setPostalCode(profileQuery.data.postalCode ?? "");
|
setPostalCode(profileQuery.data.postalCode ?? "");
|
||||||
setCity(profileQuery.data.city ?? "");
|
setCity(profileQuery.data.city ?? "");
|
||||||
setImage(profileQuery.data.image ?? null);
|
setImage(profileQuery.data.image ?? null);
|
||||||
|
setUsername(profileQuery.data.username ?? "");
|
||||||
|
setBio(profileQuery.data.bio ?? "");
|
||||||
|
setProfileVisible(profileQuery.data.profileVisible ?? false);
|
||||||
}
|
}
|
||||||
}, [profileQuery.data]);
|
}, [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({
|
const changeMyPassword = trpc.users.changeMyPassword.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setCurrentPassword("");
|
setCurrentPassword("");
|
||||||
|
|
@ -562,6 +597,75 @@ export default function EinstellungenPage() {
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
) : null}
|
) : 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" ? (
|
{active === "ernaehrung" ? (
|
||||||
<SectionCard
|
<SectionCard
|
||||||
title="Rezept- und Mahlzeiten-Präferenzen"
|
title="Rezept- und Mahlzeiten-Präferenzen"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue