rebreak-monorepo/ops/PROFILE_PAGE_DESIGN.md
chahinebrini e76be7ee78 feat(profile): Profile-Page komplett + Header-Dropdown + UI-Pattern-Fixes
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).
2026-05-07 18:22:58 +02:00

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> rufen PATCH /api/auth/me auf, invalidieren useMe() (cachedMe-Flush) + reload.
  • <ApprovedDomainsSheet> lazy-loadet on-open /api/profile/me/approved-domains (separat, nicht im Initial-Bundle — Liste kann groß werden).
  • <DemographicsEditSheet> ruft PATCH /api/profile/demographics auf.
  • <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/me und /api/profile/me/* benutzen ausschließlich requireUser(event).id. Kein userId-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

  1. Plan-Pill Design — Free=neutral, Pro=orange, Legend=gold sichtbar oder zu plakativ? Soll Legend einen subtilen Goldverlauf bekommen oder nur ein Icon?
  2. 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).
  3. 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 wir provider-Feld in der me-response? (Quick win — aus user.app_metadata.provider.)
  4. 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):

  1. Tab-Bar _layout.tsx ergänzen um <NativeTabs.Screen name="profile">. Rest unverändert.
  2. app/(app)/profile.tsx Skeleton mit 6 Sektionen, alle Hardcoded-Demo-Daten.
  3. 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-Namespaces profile.*)
  • apps/rebreak-native/components/AppHeader.tsx (Profile-Item route → /(app)/profile)