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).
266 lines
12 KiB
Markdown
266 lines
12 KiB
Markdown
# UI Migration Plan — Settings + Profile (Nuxt → rebreak-native)
|
||
|
||
Stand: 2026-05-07
|
||
Scope: rebreak-native (Expo / RN). Owner: rebreak-native-ui.
|
||
|
||
Phase 1 = NUR Plan (dieses Dokument). Keine Code-Änderungen in `apps/rebreak-native/`.
|
||
|
||
---
|
||
|
||
## 1. Status Quo
|
||
|
||
### Nuxt-App (`~/mono/trucko-monorepo/apps/rebreak/app/pages/`)
|
||
|
||
Bereits gebaute Pages, die in rebreak-native fehlen oder nur als Stub existieren:
|
||
|
||
- `app/settings.vue` (~520 LOC) — 3 UTabs: **Streak**, **Profil**, **Einstellungen**
|
||
- Streak: `<StreakTab />` (separate Component)
|
||
- Profil: nickname-Edit + Avatar (HERO_AVATARS preset ODER Foto-Upload mit Cropper)
|
||
- Einstellungen-Tab enthält:
|
||
- Hilfe & FAQ (Link)
|
||
- Appearance: system / light / dark (UColorMode)
|
||
- Language: i18n locale-switch
|
||
- Dev-Tools: Keyboard-Resize-Test, Family-Controls Spike
|
||
- **Devices** (`useDeviceStore`) — Liste mit Limit-Progressbar, current-device-Badge
|
||
- **Community-Domains** (approved + rejected) aus `/api/custom-domains`
|
||
- **Subscription** (Stripe Portal — `/api/stripe/portal`) — paid only
|
||
- Logout
|
||
- `app/profile/[userId].vue` — fremdes Profil ansehen (nickname, tier, posts, follow, recent posts, DM-link)
|
||
|
||
### rebreak-native — was schon existiert
|
||
|
||
- `app/settings.tsx` (223 LOC) — Stub-Scaffold. Sections vorhanden, aber Handlers leer (`onPress: () => {}`).
|
||
- `components/AppHeader.tsx` — Dropdown-Menu funktioniert, hat schon `editProfile` + `settings` Items, beide routen auf `/settings`. SOS-Button im Dropdown ist auch da.
|
||
- `app/urge.tsx` — SOS-Page mit `KeyboardAvoidingView` + `<TtsProviderToggle />` + `<LlmProviderToggle />` als Floating-Bar (`st.ttsToggleBar`, line 1128–1131).
|
||
- `lib/ttsProvider.ts` — AsyncStorage-Persist, 5 provider (`openai`, `gemini`, `google-cloud`, `elevenlabs`, `cartesia`).
|
||
- `lib/llmProvider.ts` — analog für LLM (`auto`, `openrouter-sonnet`, …).
|
||
|
||
### Was komplett fehlt im rebreak-native
|
||
|
||
- Profile-Edit-Logik (nickname-Form, Avatar-Picker mit HERO_AVATARS + Photo-Upload)
|
||
- StreakTab-Equivalent als RN-Component
|
||
- Devices-Section (read aus `/api/devices`)
|
||
- Community-Domains-Section
|
||
- Stripe-Portal-Button
|
||
- Theme/Language-Switcher mit echtem State (i18n + ColorMode)
|
||
- Lyra-Voice-Picker (existiert nirgends)
|
||
- Debug-Section (TtsProviderToggle + LlmProviderToggle aktuell hardcoded auf urge.tsx)
|
||
- Profile-View für fremde User (`/app/profile/[userId]` Nuxt → `/profile/[userId].tsx` RN)
|
||
|
||
---
|
||
|
||
## 2. Header-Dropdown-Menu Architektur
|
||
|
||
Bereits im File: `apps/rebreak-native/components/AppHeader.tsx:65–76`.
|
||
|
||
Aktuell:
|
||
```
|
||
[SOS — heart, prominent]
|
||
─────
|
||
person-outline "Profil bearbeiten" → /settings
|
||
settings-outline "Einstellungen" → /settings
|
||
─────
|
||
log-out-outline "Abmelden"
|
||
```
|
||
|
||
**Empfohlene Items nach Migration:**
|
||
- SOS (bleibt prominent)
|
||
- Profil bearbeiten → `/settings?tab=profile`
|
||
- Einstellungen → `/settings?tab=settings`
|
||
- Streak → `/settings?tab=streak`
|
||
- (Legend-only) Lyra-Voice → `/settings?tab=settings#lyra-voice`
|
||
- (Dev-Builds only / `__DEV__`) Debug → `/settings?tab=debug`
|
||
- Logout
|
||
|
||
→ Settings-Page übernimmt die alte Nuxt-3-Tab-Struktur **plus** Debug-Tab (gated auf `__DEV__` oder Internal-User).
|
||
|
||
Kein neues Header-Element nötig — Dropdown ist da, nur die `tab`-Query-Param-Logik in `settings.tsx` ergänzen + Items ergänzen.
|
||
|
||
---
|
||
|
||
## 3. Settings-Page Struktur (Migration-Ziel)
|
||
|
||
`app/settings.tsx` umbauen zu Tab-Layout (z.B. via simple Tab-Bar — kein UTabs in RN, custom mit drei `Pressable`-Buttons reicht).
|
||
|
||
### Tab 1 — Streak (MVP)
|
||
- StreakBadge + currentDays/longestDays (read aus `/api/streak`)
|
||
- Heatmap der letzten Wochen (StreakEvent-Liste)
|
||
- Reset-Button mit Confirm-Modal
|
||
|
||
### Tab 2 — Profil (MVP)
|
||
- Avatar (HERO_AVATARS preset grid + Photo-Upload via `expo-image-picker`)
|
||
- Nickname-Input (Save → `PATCH /api/auth/me { nickname }`)
|
||
- Username (read-only display, `@username`)
|
||
- Member-Since-Date
|
||
|
||
### Tab 3 — Einstellungen (MVP)
|
||
- **Appearance** — system/light/dark (AsyncStorage + theme-Provider) — *requires theme-store create*
|
||
- **Language** — de/en (i18n.changeLanguage + AsyncStorage persist)
|
||
- **Push-Notifications** Toggle (existiert schon im Stub)
|
||
- **Streak-Reminders** Toggle
|
||
- **Devices** (read aus `/api/devices`) — Liste, kein Delete (cooldown noch nicht implementiert)
|
||
- **Subscription** — Plan-Status + "Manage" → opens Stripe-Portal-URL via `Linking.openURL(url)`
|
||
- **Logout**
|
||
- **Delete Account** (Danger-Zone)
|
||
|
||
### Tab 4 — Lyra (Legend-only)
|
||
- Voice-Picker (default: Alexandra ElevenLabs / Sonic Cartesia)
|
||
- Speed-Slider? (ElevenLabs `voice_settings.style` o.ä.)
|
||
- Preview-Button (sample-text TTS)
|
||
|
||
### Tab 5 — Debug (gated `__DEV__` || internal-user)
|
||
- TtsProviderToggle (verschoben aus urge.tsx)
|
||
- LlmProviderToggle (verschoben aus urge.tsx)
|
||
- Bench-Anzeige (letzte BenchSession-Werte aus `lib/sosTtsBenchmark`)
|
||
- Reset-AsyncStorage-Button
|
||
|
||
### Followup (NICHT MVP)
|
||
- Community-Domains-Liste (kann zur Blocker-Page wandern)
|
||
- Hilfe & FAQ — Webview oder externe URL erstmal
|
||
- Family-Controls Spike — Native-only, kein UI nötig
|
||
- Profile-View `/profile/[userId].tsx` — Community-Section, kommt mit Community-Migration
|
||
|
||
---
|
||
|
||
## 4. iOS-Keyboard-Fix für SOS-Page
|
||
|
||
### Pattern aus `components/PostCommentsSheet.tsx`
|
||
|
||
Funktionierende Bestandteile (line 126–139, 240, 367):
|
||
|
||
1. **Listener** auf `keyboardWillShow`/`keyboardWillHide` (iOS) bzw. `keyboardDidShow`/`keyboardDidHide` (Android) → `keyboardHeight`-State.
|
||
2. **Container-Padding bottom = `Platform.OS === 'ios' ? keyboardHeight : 0`** auf der gesamten inneren Inhalts-View. (Android nutzt `windowSoftInputMode=adjustResize` und braucht kein Padding.)
|
||
3. **Input-Bar paddingBottom = `keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom)`** — schlankes Padding bei offener Tastatur, sonst Safe-Area.
|
||
4. `keyboardShouldPersistTaps="handled"` auf der scrollbaren Liste.
|
||
|
||
### Was urge.tsx aktuell macht (line 1144)
|
||
|
||
```tsx
|
||
<KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : 'height'} keyboardVerticalOffset={0}>
|
||
```
|
||
|
||
→ funktioniert in Modals manchmal, aber bei Full-Screen-Pages wie urge.tsx (kein Modal!) overlapped der Input bei iOS, weil `behavior="padding"` kämpft mit der SafeAreaView und dem festen `paddingTop: insets.top`. User-Bug ist reproduziert.
|
||
|
||
### Vorgeschlagener Fix für `app/urge.tsx`
|
||
|
||
**Option A (empfohlen, 1:1 PostCommentsSheet-Pattern):**
|
||
- `KeyboardAvoidingView` **entfernen**.
|
||
- `keyboardHeight`-State (existiert schon ab line 92, line 198–199 listener → behalten).
|
||
- Auf den äußeren Container (oder den Wrapper um `FlatList + chips + inputBar`) `paddingBottom: Platform.OS === 'ios' ? keyboardHeight : 0` anwenden.
|
||
- Input-Bar (line 1230): `paddingBottom: keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom)` ist schon korrekt — bleibt.
|
||
- `FlatList`: `keyboardShouldPersistTaps="handled"` hinzufügen (verhindert dass tap-on-chip die Tastatur wegklickt).
|
||
|
||
**Option B (minimaler Eingriff):**
|
||
- `KeyboardAvoidingView`-`keyboardVerticalOffset` auf `insets.top + topBarHeight` setzen statt `0` — hilft bei vielen Layouts, aber fragiler.
|
||
|
||
→ **A** ist konsistent mit dem bewährten Pattern und schon benutzt für PostComment. Soll der Default sein.
|
||
|
||
### Files anzupassen
|
||
|
||
- `apps/rebreak-native/app/urge.tsx` (lines 1144, 1249, ggf. um den top-bar-Container herum)
|
||
|
||
---
|
||
|
||
## 5. Lyra-Voice-Feature
|
||
|
||
### Status backend
|
||
|
||
`speak-elevenlabs.post.ts:28` und `speak-cartesia.post.ts:23` lesen voiceId NUR aus `runtimeConfig.elevenlabsVoiceId` / `cartesiaVoiceId` oder aus `process.env`. **Kein body-param `voiceId` aktuell.**
|
||
|
||
Aktuell ist die Voice systemweit fix.
|
||
|
||
### Frage: DB-Schema-Change nötig?
|
||
|
||
**JA** — aber minimal. Variante (a) ist **empfohlen**:
|
||
|
||
**(a) Profile-Field erweitern (1 Spalte):**
|
||
```prisma
|
||
model Profile {
|
||
...
|
||
lyraVoiceId String? @map("lyra_voice_id") // null = system-default
|
||
...
|
||
}
|
||
```
|
||
+ Migration: `ALTER TABLE rebreak.profiles ADD COLUMN lyra_voice_id text NULL;`
|
||
+ `me.patch.ts` um `lyraVoiceId` erweitern (gated: nur Legend-User dürfen setzen).
|
||
+ `me.get.ts` returnt `lyraVoiceId` mit.
|
||
|
||
**(b) Separates `UserPreference`-Modell (overkill für jetzt):**
|
||
Nur lohnenswert wenn weitere Prefs (theme, language-override, push-prefs) bald hinzu kommen. Dann lieber zentral.
|
||
|
||
→ Empfehlung: **(a)** für Phase 2, **(b)** parken bis 3+ Prefs gleichzeitig nötig sind.
|
||
|
||
### API-Changes
|
||
|
||
`speak-elevenlabs.post.ts` + `speak-cartesia.post.ts`:
|
||
- Body um optional `voiceId?: string` erweitern.
|
||
- VoiceId-Resolve-Order: body-param → user.lyraVoiceId (DB) → runtimeConfig → env → FALLBACK.
|
||
- Tier-check: wenn body-param gesetzt aber user nicht Legend → 403 oder silent-ignore + use default (silent-ignore safer).
|
||
|
||
### Frontend-Flow
|
||
|
||
1. `useMe()` returned bereits Profile incl. zukünftiges `lyraVoiceId`.
|
||
2. Settings-Tab (Lyra) — Voice-Liste hardcoded zur First-Iteration:
|
||
- ElevenLabs: Alexandra (`kdmDKE6EkgrWrrykO9Qt`), Rachel, …
|
||
- Cartesia: Default-DE (`b9de4a89-2257-424b-94c2-db18ba68c81a`), …
|
||
3. Save → `PATCH /api/auth/me { lyraVoiceId }`.
|
||
4. `lib/sosTtsQueue.ts:208` — Body um `voiceId: useMe().lyraVoiceId` erweitern.
|
||
|
||
### Voices initial (nice-to-have hardcoded)
|
||
|
||
- ElevenLabs: 3–5 deutsche female voices auswählen, in `lib/lyraVoices.ts` als statische Liste.
|
||
- Cartesia: 2–3 deutsche stimmen.
|
||
- Per Provider (TTS-Provider != Voice-Provider), Voice-Picker zeigt nur Voices passend zum gewählten TTS-Provider — oder "auto" mit per-Provider-Default-Map.
|
||
|
||
→ Erste Iteration: nur ElevenLabs-Voices anbieten (häufigster Provider), andere Provider ignorieren `lyraVoiceId`.
|
||
|
||
---
|
||
|
||
## 6. Migration-Reihenfolge (Phase 2/3/4)
|
||
|
||
### Phase 2 — Quick Wins (1–2 days)
|
||
1. **iOS-Keyboard-Fix urge.tsx** — kein Server-Change, isolierter UI-Fix → Smoke-Test auf iOS-Device.
|
||
2. **Debug-Tab in settings.tsx** — TtsProviderToggle + LlmProviderToggle dort einbauen, aus urge.tsx entfernen.
|
||
3. **Header-Dropdown** — Items "Streak" + "Debug" (gated `__DEV__`) ergänzen, settings.tsx Tab-Routing implementieren.
|
||
|
||
### Phase 3 — MVP-Cutover (3–5 days)
|
||
4. **Profil-Tab** — nickname + Avatar-Picker (preset). Photo-Upload via `expo-image-picker` + Crop später (sehr großes File mit `vue-advanced-cropper`-Equivalent in RN: `react-native-image-crop-picker` wäre Native-Module — später).
|
||
5. **Streak-Tab** — useStreak-Hook + Badge.
|
||
6. **Einstellungen-Tab** — Devices-Liste, Theme/Language-Picker, Notification-Toggles (real persisten), Logout (existiert).
|
||
7. **Subscription-Section** — Stripe-Portal-Link via `Linking.openURL`.
|
||
|
||
### Phase 4 — Legend Features (2–3 days)
|
||
8. **DB-Migration** `lyra_voice_id` (Backend-team).
|
||
9. **API-Update** `speak-*.post.ts` body-param + tier-check.
|
||
10. **Voice-Picker UI** im Lyra-Tab.
|
||
11. **sosTtsQueue.ts** body voiceId.
|
||
|
||
### Phase 5 — Followup
|
||
12. Community-Domains-Section (mit Blocker-Migration zusammen).
|
||
13. Profile-View `/profile/[userId].tsx`.
|
||
14. Hilfe/FAQ-Section.
|
||
15. Photo-Upload mit Crop (native module).
|
||
|
||
---
|
||
|
||
## 7. Top-3 Risiken
|
||
|
||
1. **Avatar-Photo-Upload** — Nuxt nutzt `vue-advanced-cropper` (HTML5-Canvas). RN-Alternative: `react-native-image-crop-picker` ist ein Native-Module (Expo-Plugin nötig) → Coordination mit `zied`/`backyard`. **Mitigation:** MVP nur HERO_AVATARS preset, photo-upload als Phase-5.
|
||
2. **Theme-Switch / ColorMode** — Aktuell nutzt rebreak-native NICHT `useColorScheme`-getriebenes Theme. Komplette Color-Token-Refactor wäre nötig (`lib/theme.ts` aktuell hardcoded light). **Mitigation:** MVP "system" disabled, nur Lock auf light. Dark-Mode Phase-5.
|
||
3. **DB-Migration `lyra_voice_id`** — Schema-Change auf production via Prisma migration → wenn Cutover-Blocker noch nicht resolved (siehe `feedback_backend_runtime_config.md`), hängt das. **Mitigation:** Voice-Picker erst bauen wenn Cutover stabil; bis dahin client-side AsyncStorage als Mock-Persist (nur lokal, regelt sich nach Login auf neuem Gerät).
|
||
|
||
---
|
||
|
||
## 8. Empfehlung — erster Schritt in Phase 2
|
||
|
||
**Start: iOS-Keyboard-Fix in `app/urge.tsx`.**
|
||
|
||
Warum:
|
||
- Isoliert (1 File, kein Backend, keine Coordination)
|
||
- Direkt user-sichtbar als Win
|
||
- Pattern (PostCommentsSheet) bereits validiert
|
||
- Kein Risiko für andere Features (kein Schema, kein API-Change)
|
||
- Sets up den Workflow: small PR, smoke-test, merge — vor den größeren Settings-Migrations.
|
||
|
||
Danach: Debug-Tab + TtsProviderToggle/LlmProviderToggle aus urge.tsx in settings.tsx wandern (entfernt visuelle Bench-Bar aus Production-Build, optional gated auf `__DEV__`).
|