Profile (3 Iterationen):
- app/profile/index.tsx + components/profile/* (Header, StatsBar, Approved,
Streak, UrgeStats, Demographics, DigaMissionBanner)
- echte Live-Daten via useMe-Hook (Avatar/Nickname/Plan/Email/Provider-Pill)
- Demographics mit echten Inputs (TextInput + Bottom-Sheet-Selects),
debounced auto-save, Pro-Trial-Reward-Banner, Mikro-Why-Texte
- Approved Domains als plain integer (KEIN Plan-Slot/Cap)
- Friendly Hint-Text statt Progress-Bar (alignSelf:'stretch' Pattern)
- StatsBar zentriert mit 3 prominenten Cards (vertikale Dividers)
- Cooldown-Timeline als Liste mit 1px-Rail
- ApprovedDomainsList: Collapse-Chevron rechts in Title-Row (Pattern-Fix)
- Eigene vs fremde Profile-Ansicht streng getrennt (DSGVO/Anonymität)
Header-Dropdown (kein 3-Punkte-Icon):
- Avatar als Trigger im AppHeader (User-Wunsch)
- Custom-Modal beide Plattformen, Card-Style
- SOS prominent oben (nur Wort 'SOS' rot, Tagline 'wir sind für dich da' klein darunter)
- Profile/Settings/Games/Debug(__DEV__)/Logout
- Logout neutral (nicht rot — Recovery-tonal)
- AppHeader: neue showBack + title Props für Sub-Routes
Routes (Stub bis Phase C):
- app/profile/[userId].tsx — anonym (nur public-Stats)
- app/settings.tsx — Coming-Soon-Skeleton
- app/games.tsx — Standalone Games-Page mit GameCard-Grid
- app/debug.tsx — __DEV__-only
Game-Picker (Migration aus Nuxt):
- components/games/{GameCard, StarRating, GameRatingStars}
- 2x2 Grid, 56pt SVG-Icons (inline aus components/urge/gameSvgs.ts)
- Live-Backend /api/games/ratings (silent-fail)
- Re-use UrgeGames.tsx ohne TTS/Cooldown-Loop
UI-Pattern-Fixes (alle aus screenshot-User-Feedback 2026-05-07):
- Snake-Bug (food-pellet React-18-StrictMode-Reducer-double-call) gefixt
- Snake-Buttons platform-native (iOS-blue / Android-ripple)
- Tetris-Margins (16px paddingHorizontal)
- PostCard-Buttons Apple-44pt-Hit-Area (Image-Select, Image-Remove,
Cancel, Share-Pill — via hitSlop)
- ProfileHeader Demographics-Hint: alignSelf:'stretch' Pattern
- ApprovedDomainsList Collapse: Title flex:1 + Chevron rechts
- ProtectionDetailsSheet FAQ-Items: alignSelf:'stretch' defensive
- AppHeader Back-Button: neue showBack-Prop + chevron-back
Memory + Plan-Docs:
- 17 Memory-Files dokumentieren System-Wissen + Patterns
- ops/{CUTOVER, UI_MIGRATION, PROFILE_PAGE, WEBHOOK, GAMES_1V1,
RELEASE_READINESS, TESTING_STATE, MAESTRO_HOSTING}_*.md
Backend bleibt unverändert (Tier-LLM + Nickname + sort:latency
sind seit gestern deployed).
24 KiB
Profile Page — Detail Design Spec
Stand: 2026-05-07
Owner: rebreak-native-ui
Phase: 1 (Detail-Plan, kein Code).
Quellen: memory/project_profile_page_design.md, memory/feedback_anonymity_nickname.md, memory/project_llm_per_plan.md, ops/UI_MIGRATION_PLAN.md.
Profile-Page ist das UI-Showpiece. Streak-Tab entfällt komplett und wandert dezent in die Profile-Page. Settings ist parallel und funktional, aber kein Showpiece.
0. Routing & Datenmodell-Übersicht
Zwei Views, scharf getrennt:
/(app)/profile(eigenes Profil, Hero-Tab in Tab-Bar). Volle Sicht./profile/[userId](fremdes Profil). Anonymisiert: nur nickname, avatar, plan-tier (keine Email, keine Demographics, keine Cooldowns, keine SOS-Stats, keine Liste der blockierten Domains).
Beide laden via apiFetch aus eigenen Endpoints — kein client-side Filtern.
1. Visual Mock (ASCII-Wireframe)
Eigenes Profil — vertikal scrollbar, ein Screen, sechs Sektionen:
+------------------------------------------------------------+
| [<-- back] Profil [icon: cog] | <- minimal top bar
+------------------------------------------------------------+
| |
| +------------------+ |
| | | <- Avatar 96x96, runder |
| | avatar | Rahmen, plan-Akzent |
| | | (free=gray, pro=orange, |
| +------------------+ legend=gold) |
| [icon: camera-edit] |
| |
| Jonas_42 [icon: pencil] | <- nickname, inline-edit
| chahinebrini@gmail.com | <- email read-only,
| | subdued grau, klein
| [pill: legend] Mitglied seit 12.04.2026 |
| |
+------------------------------------------------------------+
STATS [icon: info]
+------------------------------------------------------------+
| +-------+ +-------+ +-------+ +-----------+ |
| | 12 | | 47 | | 134 | | 8 | |
| | Posts | | Folg- | | gebl. | | Approved | |
| | | | ower | | Dom. | | Domains > | |
| +-------+ +-------+ +-------+ +-----------+ |
| tap tap tap tap (highlight) |
+------------------------------------------------------------+
STREAK [icon: chevron] <- collapsible
+------------------------------------------------------------+
| 23 Tage geschützt |
| seit 14. April 2026 |
| |
| longest streak: 41 Tage |
| |
| COOLDOWN-VERLAUF |
| ----------------------------------- |
| | timeline-rail (1px line, vertical) | |
| | o 18.04. 16h Cooldown beendet | |
| | | "Stress nach Arbeit" | |
| | o 02.05. 4h Cooldown abgebr. | |
| | | ohne Reason | |
| | o 06.05. 24h aktiv | [pill: aktiv] |
| ----------------------------------- |
| [load more — last 30 days] |
+------------------------------------------------------------+
LYRA INSIGHTS <- SOS stats, dezent
+------------------------------------------------------------+
| Letzte 30 Tage |
| |
| 5 SOS-Sessions, 4 davon bewältigt [80% bar] |
| |
| Was hat am meisten geholfen? |
| [#] Atemübung ......... 3 Sessions |
| [#] Spiel ......... 1 Session |
| [#] Reden ......... 1 Session |
| |
| Häufigste Emotion: Stress |
+------------------------------------------------------------+
ANONYMER BEITRAG ZUR FORSCHUNG [icon: chevron] <- collapsed default
+------------------------------------------------------------+
| Optional. Hilft DiGA-Wirksamkeit zu belegen. |
| Nur aggregiert, nie personenbezogen. |
| [link: Mehr erfahren] |
| |
| ----- expanded state ----- |
| Geburtsjahr 1989 [icon: pencil] |
| Geschlecht divers [icon: pencil] |
| Familienstand ledig [icon: pencil] |
| Beruf Angestellt [icon: pencil] |
| Bundesland Bayern [icon: pencil] |
| Stadt (nicht angeg) [icon: pencil] |
| |
| [button: Einwilligung widerrufen] |
+------------------------------------------------------------+
Fremdes Profil — drastisch reduziert:
+------------------------------------------------------------+
| [avatar 96px] |
| Jonas_42 [pill: legend] |
| Mitglied seit April 2026 |
| [button: Folgen] [button: DM senden] |
+------------------------------------------------------------+
| +-------+ +-------+ +-----------+ |
| | 12 | | 47 | | 8 | |
| | Posts | | Folg- | | Approved | |
| | | | ower | | Domains | |
| +-------+ +-------+ +-----------+ |
+------------------------------------------------------------+
| Letzte Posts (5) ... |
+------------------------------------------------------------+
UX-Notizen:
- Edit-Icons stehen rechts neben dem editierbaren Wert, nicht in einem zentralen "Edit-Mode". Inline-Tap öffnet Bottom-Sheet mit Input.
- Collapse-Chevron rechts oben in Section-Header, animated 180-Grad-Rotation.
- Keine Tier/Score-Kacheln (alte Nuxt-Logik), nur "Mitglied seit"-Datum + Plan-Pill.
- Plan-Pill nutzt Plan-Akzentfarbe (free=neutral-300, pro=brandOrange, legend=Goldverlauf).
2. Component-Tree
Routen (neu):
app/(app)/profile.tsx→ eigenes Profil. Wird Tab-Bar-Eintrag, ersetzt Streak-Tab.app/profile/[userId].tsx→ fremdes Profil. Modal/Stack-Push.
Komponenten (neu):
<ProfileScreen> (top-level page, owns scroll + section refs)
<ProfileHeader> (avatar + nickname + email + plan-pill + member-since)
<AvatarPicker> (preset-grid + custom-upload trigger)
<AvatarCropSheet> (Bottom-Sheet mit Crop-UI für Custom-Photo)
<NicknameEditSheet> (Bottom-Sheet, valibot-validiert)
<StatsBar> (4 stat-cards horizontal, tappable)
<StatCard> (number + label, optional onPress)
<ApprovedDomainsSheet> (Bottom-Sheet mit Liste der approved domains)
<BlockedDomainsSheet> (analog, custom + global summary)
<StreakSection> (collapsible, owns streak + cooldown queries)
<StreakHero> (currentDays + startDate + longest)
<CooldownTimeline> (vertikale Liste, virtualisiert, paginiert)
<CooldownTimelineItem>
<LyraInsightsCard> (SOS-Stats: 30-Tage-Trend + helped-by-Bar)
<HelpedByBar> (atemuebung/spiel/reden mit Anteils-Balken)
<DemographicsAccordion>
<DemographicsConsentNotice>
<DemographicsField> (label + value + edit-icon)
<DemographicsEditSheet>
Komponenten (shared, reuse):
<Card>— vorhanden in components/Card.tsx<Button>— vorhanden<EmptyState>— vorhanden, für "noch keine approved domains"- Bottom-Sheet-Pattern aus
<PostCommentsSheet>(animated, Backdrop) <StreakBadge>(existiert) — innerhalb<StreakHero>als "longest"-Indikator nutzbar
State-Ownership:
<ProfileScreen>lädt einmalig/api/profile/me/full(oder kombiniert me + stats + streak + cooldowns + sos in einem Aufruf — siehe Sektion 3).<NicknameEditSheet>und<AvatarPicker>rufenPATCH /api/auth/meauf, invalidierenuseMe()(cachedMe-Flush) + reload.<ApprovedDomainsSheet>lazy-loadet on-open/api/profile/me/approved-domains(separat, nicht im Initial-Bundle — Liste kann groß werden).<DemographicsEditSheet>ruftPATCH /api/profile/demographicsauf.<CooldownTimeline>paginiert: erste 10 vom Initial-Aufruf, "load more" lädt/api/profile/me/cooldown-history?cursor=....
3. API-Endpoint-Liste
Existiert bereits:
GET /api/auth/me— eigene basis. Liefert id, email, username, nickname, avatar, plan, streak (current days), created_at. KEINE Stats. KEINE Demographics.PATCH /api/auth/me— username, nickname, avatar. Muss um demographic-Felder erweitert werden ODER separater Endpoint (siehe unten).GET /api/streak— current Streak-Row.GET /api/streak/events— Streak-History (max 50). Enthält "started" / "reset" / "milestone" / "relapse". Cooldown ist DAVON GETRENNT.GET /api/cooldown/status— nur aktiver Cooldown (single).GET /api/community/posts?userId=...— vorhanden indirekt (über filter-Params).GET /api/social/profile/[userId]— fremdes Profil. Returnt nickname, avatar, followersCount, postsCount, tier, recentPosts, isFollowing. Approved-Domains nicht enthalten — muss erweitert werden.GET /api/custom-domains— eigene custom domains (active + submitted + approved + rejected).
Neu nötig:
| Endpoint | Methode | Zweck | Shape |
|---|---|---|---|
/api/profile/me |
GET | Aggregat: alles für eigenes Profil in einem Roundtrip | { profile, stats, streak, recentCooldowns[], demographics, sosInsights } |
/api/profile/me/cooldown-history |
GET | Paginated cooldown-Liste | cursor-paginated, je 20, ältere ans Ende |
/api/profile/me/approved-domains |
GET | Liste approved domains für expanded sheet | [{ domain, approvedAt }] |
/api/profile/me/sos-insights |
GET | Aggregierte SOS-Stats (Trends, helped-by-Counts) | siehe shape unten |
/api/profile/me/demographics |
PATCH | Demographic-Felder setzen, audit-trail | body: subset; setzt demographics_consent_at automatisch auf now() wenn null |
/api/profile/me/demographics |
DELETE | Einwilligung widerrufen | nullt alle Demographic-Felder + demographics_consent_at |
/api/social/profile/[userId] |
GET | bereits da, ERWEITERN um approvedDomainsCount und blockedCustomCount (privacy: NICHT die Liste, nur Anzahl) |
additive change |
Aggregat-Endpoint /api/profile/me Shape (Vorschlag):
{
profile: { id, email, nickname, username, avatar, plan, createdAt },
stats: {
postsCount: number,
followersCount: number,
followingCount: number,
blockedCustomCount: number, // user-eigene custom domains aktiv
blockedGlobalCount: number, // size der globalen Blocklist (Kontext)
approvedDomainsCount: number, // submitted by user und approved
},
streak: { currentDays, longestDays, startDate, isActive, avgMonthlySavings },
recentCooldowns: Array<{
id, cooldownStartedAt, cooldownEndsAt, resolvedAt, cancelledAt, reason,
status: 'active' | 'resolved' | 'cancelled',
durationHours: number, // computed
}>, // erste 10
hasMoreCooldowns: boolean,
demographics: {
consentAt: string | null,
birthYear: number | null,
gender: string | null,
maritalStatus: string | null,
profession: string | null,
bundesland: string | null,
city: string | null,
},
sosInsights: {
last30Days: { sessions: number, overcome: number, overcomeRate: number },
helpedBy: { breathing: number, game: number, talk: number, other: number }, // counts
topEmotion: string | null,
},
}
SOS-Insights-Aggregat: aus sos_sessions (existiert) + urge_logs (existiert). helpedBy durch Heuristik aus gamesPlayed-Json + breathingCount + messages-Length. Edge-Case: 0 Sessions → null-State, UI zeigt EmptyState "noch keine SOS-Session".
Approved-Domains-Count: count der domain_submissions mit status='approved' und userId=user.id. Nicht aus user_custom_domains — da steht der approved-Status ebenfalls, aber domain_submissions ist source of truth für "von dir submitted und approved".
4. DB-Schema-Änderungen
Neue Spalten auf Profile (alle nullable, opt-in):
model Profile {
// ...bestehend...
birthYear Int? @map("birth_year") // nur Jahr (1900-2024), keine vollen Geburtsdaten
gender String? // 'male' | 'female' | 'divers' | 'no_answer'
maritalStatus String? @map("marital_status") // 'single' | 'partnered' | 'married' | 'divorced' | 'widowed' | 'no_answer'
profession String? // freitext, max 80 chars
bundesland String? // ISO-3166-2:DE Code (z.B. 'DE-BY')
city String? // freitext, max 80 chars
demographicsConsentAt DateTime? @map("demographics_consent_at") @db.Timestamptz(6)
lyraVoiceId String? @map("lyra_voice_id") // siehe UI_MIGRATION_PLAN §5 — gleiche Migration mitnehmen
}
Migration-File: backend/prisma/migrations/20260507_add_profile_demographics_and_lyra/migration.sql
ALTER TABLE "rebreak"."profiles"
ADD COLUMN "birth_year" INTEGER,
ADD COLUMN "gender" TEXT,
ADD COLUMN "marital_status" TEXT,
ADD COLUMN "profession" TEXT,
ADD COLUMN "bundesland" TEXT,
ADD COLUMN "city" TEXT,
ADD COLUMN "demographics_consent_at" TIMESTAMPTZ,
ADD COLUMN "lyra_voice_id" TEXT;
-- Index nur wenn wirklich für Aggregations-Queries gebraucht (DiGA-Reports auf
-- bundesland/birthYear). Initial: keine Indizes — kann später additiv kommen.
Validierungen (server-side, in me/demographics.patch.ts):
birthYear: integer, 1900..currentYear-13 (DSGVO Mindestalter 13).gender: enum-Liste oben.maritalStatus: enum-Liste oben.bundesland: regex^DE-(BW|BY|BE|BB|HB|HH|HE|MV|NI|NW|RP|SL|SN|ST|SH|TH)$.- Freitext-Felder: trim, max-Length, kein HTML, kein URL-Pattern (anti-spam).
DSGVO-Audit-Trail: Setzen eines beliebigen Demographic-Feldes setzt demographics_consent_at = now() falls null. Widerruf (DELETE) nullt alle Felder PLUS demographics_consent_at. Optional zusätzlich Append-only-Log-Tabelle demographics_consent_log (created_at, action='granted'|'revoked', user_id) — Empfehlung: jetzt parken, reicht später nachzurüsten falls BfArM/DiGA-Audit das fordert.
5. UI-Differential-Logik (eigenes vs fremdes Profil)
Nur auf eigenem Profil (/(app)/profile):
- Email-Anzeige (klein, subdued, unter nickname)
- Avatar-Edit + Nickname-Edit
- Streak-Sektion komplett
- Cooldown-Timeline
- Lyra-Insights / SOS-Stats
- Demographics-Sektion (mit Edit + Widerruf)
- Liste der eigenen blockierten Custom-Domains (in Sheet)
- Liste der eigenen approved domains (in Sheet)
Auf fremdem Profil (/profile/[userId]):
- Avatar (resolveAvatar), nickname (Fallback username), Plan-Pill, Mitglied-seit
- Stats: Posts, Follower, Approved-Domains-Count (motivational signal — siehe Sektion 6)
- Letzte 5 Posts
- Follow-Button + DM-Button
- KEIN: email, demographics, cooldowns, sos-insights, blocked-domains-Liste
Backend-Enforcement:
/api/profile/meund/api/profile/me/*benutzen ausschließlichrequireUser(event).id. KeinuserId-Param. Kein?as=..../api/social/profile/[userId]returnt nie email, nie demographics, nie cooldowns, nie sos-insights — auch wenn der Caller selbst der gleiche User ist (eigener Self-View geht zwingend über/api/profile/me).- Frontend hat zwei Routen, jede bindet sich an einen Endpoint. Kein gemeinsamer Component-Tree mit "isOwn"-Flag — separate Components, lehrt die Trennung im Code.
6. Risiken + Open Questions
6.1 Image-Cropper-Library für RN (Expo SDK 53, New Architecture)
| Library | Pro | Contra | Native-Module | Expo-kompat. |
|---|---|---|---|---|
react-native-image-crop-picker |
Mature, native UIs (UIImagePickerController + UCrop), Cropper-Quality top | Native-Module → eject/prebuild nötig (haben wir schon, dev-client läuft), Expo-Plugin existiert nicht offiziell, Maintenance-Drift | ja | mit prebuild + manuell config-plugin |
expo-image-picker + expo-image-manipulator |
Pure Expo, keine native-Coordination | Kein interaktiver Crop — manipulator macht nur fest definierte crop-Boxes ohne UI; eigener Crop-UI = Eigenbau auf react-native-gesture-handler + reanimated |
nein | ja |
react-native-image-cropper (custom) |
Flexibel, JS-only | Maintenance fragwürdig (unmaintained), keine native-Performance | nein | ja |
@react-native-community/image-editor |
offiziell genug, paired mit expo-image-picker |
wieder kein interaktiver Crop, nur api-crop | nein | ja |
Empfehlung: expo-image-picker (existiert bereits in package.json) für Pick + ein eigenes leichtes Crop-Sheet auf react-native-reanimated + react-native-gesture-handler (existieren). Square-only-Crop reicht für Avatar-Use-Case. Vermeidet Native-Module-Coordination mit zied/backyard und hält die Expo-only-Constraint. react-native-image-crop-picker als Phase-5-Followup, falls Square-Crop dem User zu wenig ist und z.B. Zoom-Pan-Pinch-Quality nicht reicht.
6.2 Cooldown-Timeline — Liste vs Chart vs Heatmap
Optionen:
- Liste (vertikales Timeline-Rail): zeigt Datum, Dauer, Reason, Status pro Eintrag. Detailreich, gut lesbar, scrollbar, paginierbar.
- Bar-Chart (horizontale x-Achse Datum, y-Achse Dauer-h): Übersichtlich, aber Reason geht verloren, Tap-to-detail wäre extra.
- Heatmap (calendar-grid): Schick, aber Cooldowns sind selten (vielleicht 1-3/Monat). Heatmap mit hauptsächlich leeren Zellen wirkt leer.
Empfehlung: Liste mit minimalem vertikalem Timeline-Rail (1px-Linie + Punkten). Begründung: Cooldowns sind sparse Events mit narrativem Wert (Reason ist informativ — "Stress nach Arbeit"), nicht numerisch-aggregierbar. Ein Chart würde das Personal/Reflexive verlieren. Das Rail gibt visuelle Hierarchie ("damals — jetzt") ohne Charting-Overhead. Status-Pills (aktiv/beendet/abgebrochen) farb-codiert. Detail-Tap für extended Info. Das ist die "Liebe zum Detail"-Lösung: Lesefluss > Datendichte.
6.3 Demographic-Felder — Pflicht oder optional, wann fragen?
Hard rule: optional, opt-in, jederzeit widerrufbar (DSGVO Art-9 + DiGA).
Wann fragen:
- Nicht im Onboarding. Das schreckt ab und kollidiert mit dem "Du gehst nicht allein"-Brand. Onboarding bleibt schlank.
- Nicht via aufdringliches Modal. Kein "Hey willst du nicht…" Pop-up beim Profile-Open.
- Genau eine Stelle: collapsible Sektion am unteren Ende der Profile-Page mit klarem Title "Anonymer Beitrag zur Forschung" + erklärendem Subtext + Link "Mehr erfahren" (Modal mit DSGVO/DiGA-Erklärung). Komplett collapsed by default. User entdeckt es organisch.
- Sanfter Nudge nach 30+ Tagen Streak: EINMAL eine dezente in-app-Banner-Karte (in der Home-Feed, nicht als Modal): "Hilf der Forschung — anonyme Demographics tragen zur DiGA-Wirksamkeitsstudie bei". Tap führt zur Profile-Demographics-Section. Banner danach gedismissed via AsyncStorage.
6.4 Anti-Vanity-Metric — was ist motivierend?
Insta-like Stats Reihenfolge fließt psychologisch in den User ein. Vorschlag:
- Posts und Follower sind klassische Vanity-Metrics — bei Glücksspiel-Recovery können sie schädlich sein (Druck, Vergleich, "echter Aktiver"-Performance).
- Approved-Domains ist die einzige Metric, die den Beitrag des Users zur kollektiven Sicherheit misst — direkt rebreak-Mission-aligned.
- Blockierte Domains (eigene custom + globale Anzahl als Kontext) zeigt persönlichen Schutz-Stand.
Empfehlung-Reihenfolge in Stats-Bar (links→rechts): Posts, Follower, Geblockt, Approved Domains (rechts, mit dezenter Akzentfarbe brandOrange-tinted bg statt neutral, gleiche Größe — kein "biggest is best" aber visuell hervorgehoben).
Open Question für User: Sollen wir Follower-Count komplett weglassen und durch was Sinnvolleres ersetzen (z.B. "Tage geschützt" als Zahl)? "Follower" kann in Recovery-Context die falsche Dynamik triggern. Alternative: 4 Cards = Posts / Tage-geschützt / Geblockt / Approved-Domains.
6.5 Weitere Open Questions
- Plan-Pill Design — Free=neutral, Pro=orange, Legend=gold sichtbar oder zu plakativ? Soll Legend einen subtilen Goldverlauf bekommen oder nur ein Icon?
- Bundesland-Erfassung — Reicht ISO-3166-2:DE Code (16 Bundesländer)? Oder brauchen wir Stadt+PLZ für DiGA-Reporting? PLZ ist DSGVO-sensibler (3-Stellen-PLZ ist Pseudonym-grenzwertig).
- Email auf eigenem Profil zeigen? Spec sagt ja. Aber subtil + read-only. Falls Sign-Up via Apple/Google: Display "via Apple Sign-In" — wir haben aktuell keinen Provider-Marker im
/api/auth/me. Brauchen wirprovider-Feld in der me-response? (Quick win — aususer.app_metadata.provider.) - Member-Since Datum-Format — "12. April 2026" oder "April 2026"? Letzteres ist privacy-friendlier auf fremden Profilen (UserId-Lookup wenn jemand das Datum kombiniert).
7. Implementation-Reihenfolge (Phase-2/3 Vorschlag)
Phase 2 (Skeleton, dummy-Daten):
- Tab-Bar
_layout.tsxergänzen um<NativeTabs.Screen name="profile">. Rest unverändert. app/(app)/profile.tsxSkeleton mit 6 Sektionen, alle Hardcoded-Demo-Daten.- Components-Stubs anlegen (ProfileHeader, StatsBar, StreakSection, LyraInsightsCard, DemographicsAccordion, CooldownTimeline) in
components/profile/.
Phase 3 (Wire-up):
4. /api/profile/me aggregat-Endpoint backend-side bauen.
5. Migration 20260507_add_profile_demographics_and_lyra schreiben + auf staging deployen.
6. useProfileMe-Hook + Komponente connecten.
7. /api/profile/me/cooldown-history + Pagination im Frontend.
8. /api/profile/me/sos-insights + LyraInsightsCard.
9. /api/profile/me/demographics PATCH/DELETE + Edit-Sheets.
10. AvatarPicker mit expo-image-picker + Custom-Square-Crop-Sheet.
Phase 4 (Polish):
11. /profile/[userId] Route mit reduzierter View.
12. /api/social/profile/[userId] erweitern um approvedDomainsCount.
13. Skeleton-Loading-State, Empty-States, Animated-Collapse.
8. Files (relevant für spätere Phasen)
Backend:
backend/prisma/schema.prisma(Profile-Model erweitern)backend/prisma/migrations/20260507_add_profile_demographics_and_lyra/migration.sql(neu)backend/server/api/profile/me.get.ts(neu)backend/server/api/profile/me/cooldown-history.get.ts(neu)backend/server/api/profile/me/sos-insights.get.ts(neu)backend/server/api/profile/me/approved-domains.get.ts(neu)backend/server/api/profile/me/demographics.patch.ts(neu)backend/server/api/profile/me/demographics.delete.ts(neu)backend/server/api/social/profile/[userId].get.ts(extend)backend/server/db/profile.ts(extend für demographic-fields + audit-stamp)
Frontend:
apps/rebreak-native/app/(app)/profile.tsx(neu)apps/rebreak-native/app/(app)/_layout.tsx(Tab hinzufügen)apps/rebreak-native/app/profile/[userId].tsx(neu)apps/rebreak-native/components/profile/*(neue Komponenten-Sammlung)apps/rebreak-native/hooks/useProfileMe.ts(neu)apps/rebreak-native/lib/demographics.ts(Enum-Listen + Validierung, neu)apps/rebreak-native/locales/{de,en}.json(neue String-Namespacesprofile.*)apps/rebreak-native/components/AppHeader.tsx(Profile-Item route →/(app)/profile)