chahinebrini d7b15e231a feat(theme): Dark Mode Wave 2 — blocker, mail, chat, community, notifications, all remaining screens
Wave 2 = ALLE app-files die in Wave 1 noch hardcoded waren. Komplette App-weit
theme-aware-Migration jetzt durch. Legacy `import { colors }` flat export
vollständig eliminiert.

Migrated this wave:

Top-level Screens:
- app/urge.tsx (makeStyles factory mit ~20 colors)
- app/room.tsx + dm.tsx + games.tsx
- app/(app)/chat.tsx + mail.tsx + coach.tsx + notifications.tsx
- app/profile/[userId].tsx + profile/edit.tsx (INPUT_STYLE in body moved)
- app/debug.tsx + auth/callback.tsx

Blocker (7):
- AddDomainSheet, CooldownBanner, DeactivationExplainerSheet, DomainGrid,
  ProtectionCard, ProtectionDetailsSheet, ProtectionLockedCard

Mail (3):
- ConnectMailSheet, EditMailAccountSheet, MailEmptyState

Chat (1):
- ChatBubble, ChatInput

Community/Posts/Notifications:
- PostCard, PostCardSkeleton, ComposeCard, PostCommentsSheet
- NotificationsDropdown
- StreakBadge (Nativewind classes durch inline dynamic styles ersetzt)

Reusable Sheets:
- WheelPickerModal, OptionsBottomSheet, DeviceLimitReachedSheet

Urge subsystem (5):
- InlineRatingDrawer, ShareSuccessDrawer, UrgeStats, SosFeedbackModal,
  Breathing

Profile components:
- DigaMissionBanner

Pattern: useColors() hook in component body, makeStyles(colors) factory wo
StyleSheet.create vorher hardcoded war. 11 base-tokens (bg/surface/
surfaceElevated/border/text/textMuted/brandOrange/brandBlue/success/error/
warning) nutzen colors.light vs colors.dark scheme.

Bewusst NICHT migriert (semantic colors):
- DigaMissionBanner amber (#fffbeb, #854d0e) — DiGA-brand, nicht neutral
- Lyra-thinking #3b82f6 in urge.tsx — Lyra-brand-color
- scrollDownBtn #374151 — intentional dark floating-button

TS clean. Test: Settings → Theme → Dark — alle screens sollen jetzt dunkel
werden ohne white-flashes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:51:02 +02:00

288 lines
7.6 KiB
TypeScript

import { useState } from 'react';
import {
Modal,
View,
Text,
TextInput,
Pressable,
StyleSheet,
ActivityIndicator,
Platform,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import { apiFetch } from '../../lib/api';
import { useColors } from '../../lib/theme';
type Props = {
visible: boolean;
onClose: () => void;
onCreated: (room: any) => void;
};
export function CreateRoomSheet({ visible, onClose, onCreated }: Props) {
const { t } = useTranslation();
const colors = useColors();
const styles = makeStyles(colors);
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [isPublic, setIsPublic] = useState(true);
const [joinMode, setJoinMode] = useState<'approval' | 'invite_only'>('approval');
const [creating, setCreating] = useState(false);
function reset() {
setName('');
setDescription('');
setIsPublic(true);
setJoinMode('approval');
}
async function create() {
const trimmed = name.trim();
if (!trimmed || creating) return;
setCreating(true);
try {
const room = await apiFetch<any>('/api/chat/rooms', {
method: 'POST',
body: {
name: trimmed,
description: description.trim() || undefined,
isPublic,
joinMode: isPublic ? 'open' : joinMode,
},
});
onCreated(room);
reset();
onClose();
} catch (err: any) {
console.error('Room erstellen fehlgeschlagen:', err.message);
} finally {
setCreating(false);
}
}
return (
<Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
<Pressable style={styles.backdrop} onPress={onClose}>
<Pressable style={styles.sheet} onPress={() => {}}>
<View style={styles.grabber} />
<Text style={styles.title}>{t('chat.create_group')}</Text>
<TextInput
value={name}
onChangeText={setName}
placeholder={t('chat.room_name')}
placeholderTextColor="#a3a3a3"
style={styles.input}
maxLength={60}
/>
<TextInput
value={description}
onChangeText={setDescription}
placeholder={t('chat.room_description')}
placeholderTextColor="#a3a3a3"
multiline
style={[styles.input, { height: 70, textAlignVertical: 'top' }]}
maxLength={250}
/>
{/* Public toggle */}
<Pressable
style={styles.toggleRow}
onPress={() => setIsPublic((v) => !v)}
>
<Text style={styles.toggleLabel}>{t('chat.public_room')}</Text>
<View style={[styles.toggle, isPublic && styles.toggleOn]}>
<View style={[styles.toggleKnob, isPublic && styles.toggleKnobOn]} />
</View>
</Pressable>
{/* Join mode (private only) */}
{!isPublic && (
<View style={{ marginTop: 8 }}>
<Text style={styles.subLabel}>{t('chat.join_mode')}</Text>
<View style={styles.modeRow}>
{(['approval', 'invite_only'] as const).map((mode) => (
<Pressable
key={mode}
style={[styles.modeBtn, joinMode === mode && styles.modeBtnActive]}
onPress={() => setJoinMode(mode)}
>
<Text
style={[
styles.modeBtnText,
joinMode === mode && styles.modeBtnTextActive,
]}
>
{t(`chat.join_mode_${mode === 'approval' ? 'approval' : 'invite'}`)}
</Text>
</Pressable>
))}
</View>
</View>
)}
{/* Actions */}
<View style={styles.actions}>
<Pressable onPress={onClose} style={styles.cancelBtn}>
<Text style={styles.cancelText}>{t('common.cancel')}</Text>
</Pressable>
<Pressable
onPress={create}
disabled={!name.trim() || creating}
style={[
styles.createBtn,
{ opacity: !name.trim() || creating ? 0.5 : 1 },
]}
>
{creating ? (
<ActivityIndicator size="small" color="#fff" />
) : (
<Text style={styles.createText}>{t('chat.create')}</Text>
)}
</Pressable>
</View>
</Pressable>
</Pressable>
</Modal>
);
}
function makeStyles(colors: ReturnType<typeof useColors>) {
return StyleSheet.create({
backdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'flex-end',
},
sheet: {
backgroundColor: colors.bg,
borderTopLeftRadius: 22,
borderTopRightRadius: 22,
padding: 18,
paddingBottom: Platform.OS === 'ios' ? 32 : 18,
},
grabber: {
width: 36,
height: 4,
borderRadius: 2,
backgroundColor: colors.border,
alignSelf: 'center',
marginBottom: 12,
},
title: {
fontSize: 17,
fontFamily: 'Nunito_700Bold',
color: colors.text,
marginBottom: 14,
},
input: {
backgroundColor: colors.surfaceElevated,
borderRadius: 12,
paddingHorizontal: 14,
paddingVertical: 12,
fontSize: 14,
fontFamily: 'Nunito_400Regular',
color: colors.text,
marginBottom: 10,
},
toggleRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 6,
marginTop: 4,
},
toggleLabel: {
fontSize: 14,
fontFamily: 'Nunito_600SemiBold',
color: colors.text,
},
toggle: {
width: 46,
height: 28,
borderRadius: 14,
backgroundColor: colors.surfaceElevated,
padding: 2,
justifyContent: 'center',
},
toggleOn: {
backgroundColor: '#007AFF',
},
toggleKnob: {
width: 24,
height: 24,
borderRadius: 12,
backgroundColor: colors.bg,
shadowColor: '#000',
shadowOpacity: 0.15,
shadowRadius: 2,
shadowOffset: { width: 0, height: 1 },
elevation: 2,
},
toggleKnobOn: {
transform: [{ translateX: 18 }],
},
subLabel: {
fontSize: 12,
fontFamily: 'Nunito_600SemiBold',
color: colors.textMuted,
marginBottom: 6,
},
modeRow: {
flexDirection: 'row',
},
modeBtn: {
flex: 1,
paddingVertical: 8,
borderRadius: 10,
borderWidth: 1,
borderColor: colors.border,
alignItems: 'center',
marginRight: 6,
},
modeBtnActive: {
backgroundColor: colors.surface,
borderColor: '#007AFF',
},
modeBtnText: {
fontSize: 12,
fontFamily: 'Nunito_600SemiBold',
color: colors.textMuted,
},
modeBtnTextActive: {
color: '#007AFF',
},
actions: {
flexDirection: 'row',
marginTop: 20,
},
cancelBtn: {
flex: 1,
backgroundColor: colors.surfaceElevated,
paddingVertical: 12,
borderRadius: 12,
alignItems: 'center',
marginRight: 6,
},
cancelText: {
fontSize: 14,
fontFamily: 'Nunito_600SemiBold',
color: colors.text,
},
createBtn: {
flex: 1,
backgroundColor: '#007AFF',
paddingVertical: 12,
borderRadius: 12,
alignItems: 'center',
marginLeft: 6,
},
createText: {
fontSize: 14,
fontFamily: 'Nunito_700Bold',
color: '#fff',
},
});
}