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).
418 lines
24 KiB
Markdown
418 lines
24 KiB
Markdown
# 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):
|
|
|
|
```ts
|
|
{
|
|
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):
|
|
|
|
```prisma
|
|
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`
|
|
|
|
```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`)
|