From 9ab6db9c0e9879a12657a59105a5d1fe3bd2dcda Mon Sep 17 00:00:00 2001 From: Sebastian Mayer Date: Sat, 30 May 2026 11:59:59 +0200 Subject: [PATCH] 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) --- .../admin/app/(authed)/einstellungen/page.tsx | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/apps/admin/app/(authed)/einstellungen/page.tsx b/apps/admin/app/(authed)/einstellungen/page.tsx index b4a98f1..cae7cbf 100644 --- a/apps/admin/app/(authed)/einstellungen/page.tsx +++ b/apps/admin/app/(authed)/einstellungen/page.tsx @@ -139,6 +139,10 @@ export default function EinstellungenPage() { const [postalCode, setPostalCode] = useState(""); const [city, setCity] = useState(""); const [image, setImage] = useState(null); + // Community-Profil (Handle, Bio, Sichtbarkeit) + const [username, setUsername] = useState(""); + const [bio, setBio] = useState(""); + const [profileVisible, setProfileVisible] = useState(false); const [dietPreferences, setDietPreferences] = useState([ "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() { ) : null} + {active === "profil" ? ( +
+ +
+
+ + setUsername(event.target.value)} + placeholder="z. B. KochHeld" + className={inputClass} + /> +

+ {profileQuery.data?.handle + ? `Dein Handle: ${profileQuery.data.handle}` + : "Buchstaben, Ziffern, _ und -. Eine vierstellige Kennung (#XXXX) wird automatisch und dauerhaft vergeben."} +

+
+ +
+ +