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>
This commit is contained in:
parent
1abd101d53
commit
d7b15e231a
@ -17,7 +17,7 @@ import { apiFetch } from '../../lib/api';
|
|||||||
import { AppHeader } from '../../components/AppHeader';
|
import { AppHeader } from '../../components/AppHeader';
|
||||||
import { RoomCard, type Room } from '../../components/chat/RoomCard';
|
import { RoomCard, type Room } from '../../components/chat/RoomCard';
|
||||||
import { CreateRoomSheet } from '../../components/chat/CreateRoomSheet';
|
import { CreateRoomSheet } from '../../components/chat/CreateRoomSheet';
|
||||||
import { colors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
type DmConversation = {
|
type DmConversation = {
|
||||||
partnerId: string;
|
partnerId: string;
|
||||||
@ -39,6 +39,8 @@ function formatTime(ts: string, justNowLabel: string): string {
|
|||||||
|
|
||||||
function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }) {
|
function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
|
const styles = makeStyles(colors);
|
||||||
const hasUnread = conv.unreadCount > 0;
|
const hasUnread = conv.unreadCount > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -95,6 +97,8 @@ function DmItem({ conv, onPress }: { conv: DmConversation; onPress: () => void }
|
|||||||
export default function ChatScreen() {
|
export default function ChatScreen() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const colors = useColors();
|
||||||
|
const styles = makeStyles(colors);
|
||||||
const [tab, setTab] = useState<'groups' | 'direct'>('groups');
|
const [tab, setTab] = useState<'groups' | 'direct'>('groups');
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
const [createOpen, setCreateOpen] = useState(false);
|
||||||
|
|
||||||
@ -199,13 +203,13 @@ export default function ChatScreen() {
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refetchingRooms}
|
refreshing={refetchingRooms}
|
||||||
onRefresh={refetchRooms}
|
onRefresh={refetchRooms}
|
||||||
tintColor={colors.brandOrange}
|
tintColor="#007AFF"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
loadingRooms ? (
|
loadingRooms ? (
|
||||||
<View style={styles.emptyBox}>
|
<View style={styles.emptyBox}>
|
||||||
<ActivityIndicator color={colors.brandOrange} />
|
<ActivityIndicator color="#007AFF" />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.emptyBox}>
|
<View style={styles.emptyBox}>
|
||||||
@ -225,13 +229,13 @@ export default function ChatScreen() {
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={refetchingDms}
|
refreshing={refetchingDms}
|
||||||
onRefresh={refetchDms}
|
onRefresh={refetchDms}
|
||||||
tintColor={colors.brandOrange}
|
tintColor="#007AFF"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
loadingDms ? (
|
loadingDms ? (
|
||||||
<View style={styles.emptyBox}>
|
<View style={styles.emptyBox}>
|
||||||
<ActivityIndicator color={colors.brandOrange} />
|
<ActivityIndicator color="#007AFF" />
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.emptyBox}>
|
<View style={styles.emptyBox}>
|
||||||
@ -257,154 +261,155 @@ export default function ChatScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function makeStyles(colors: ReturnType<typeof useColors>) {
|
||||||
container: { flex: 1, backgroundColor: '#fafafa' },
|
return StyleSheet.create({
|
||||||
headerSection: {
|
container: { flex: 1, backgroundColor: colors.bg },
|
||||||
paddingHorizontal: 16,
|
headerSection: {
|
||||||
paddingTop: 14,
|
paddingHorizontal: 16,
|
||||||
paddingBottom: 10,
|
paddingTop: 14,
|
||||||
backgroundColor: '#fff',
|
paddingBottom: 10,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
backgroundColor: colors.bg,
|
||||||
borderBottomColor: '#e5e5e5',
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
},
|
borderBottomColor: colors.border,
|
||||||
titleRow: {
|
},
|
||||||
flexDirection: 'row',
|
titleRow: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'space-between',
|
||||||
title: {
|
},
|
||||||
fontSize: 22,
|
title: {
|
||||||
fontFamily: 'Nunito_800ExtraBold',
|
fontSize: 22,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_800ExtraBold',
|
||||||
},
|
color: colors.text,
|
||||||
createBtn: {
|
},
|
||||||
width: 34,
|
createBtn: {
|
||||||
height: 34,
|
width: 34,
|
||||||
borderRadius: 17,
|
height: 34,
|
||||||
backgroundColor: '#007AFF',
|
borderRadius: 17,
|
||||||
alignItems: 'center',
|
backgroundColor: '#007AFF',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
tabs: {
|
},
|
||||||
flexDirection: 'row',
|
tabs: {
|
||||||
marginTop: 12,
|
flexDirection: 'row',
|
||||||
backgroundColor: '#f5f5f5',
|
marginTop: 12,
|
||||||
borderRadius: 10,
|
backgroundColor: colors.surfaceElevated,
|
||||||
padding: 3,
|
borderRadius: 10,
|
||||||
},
|
padding: 3,
|
||||||
tab: {
|
},
|
||||||
flex: 1,
|
tab: {
|
||||||
flexDirection: 'row',
|
flex: 1,
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 7,
|
justifyContent: 'center',
|
||||||
borderRadius: 8,
|
paddingVertical: 7,
|
||||||
},
|
borderRadius: 8,
|
||||||
tabActive: {
|
},
|
||||||
backgroundColor: '#fff',
|
tabActive: {
|
||||||
shadowColor: '#000',
|
backgroundColor: colors.surface,
|
||||||
shadowOpacity: 0.05,
|
shadowColor: '#000',
|
||||||
shadowRadius: 2,
|
shadowOpacity: 0.05,
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowRadius: 2,
|
||||||
},
|
shadowOffset: { width: 0, height: 1 },
|
||||||
tabText: {
|
},
|
||||||
fontSize: 12,
|
tabText: {
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontSize: 12,
|
||||||
color: '#737373',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
marginLeft: 5,
|
color: colors.textMuted,
|
||||||
},
|
marginLeft: 5,
|
||||||
tabTextActive: {
|
},
|
||||||
color: '#007AFF',
|
tabTextActive: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
color: '#007AFF',
|
||||||
},
|
fontFamily: 'Nunito_700Bold',
|
||||||
tabBadge: {
|
},
|
||||||
minWidth: 16,
|
tabBadge: {
|
||||||
height: 16,
|
minWidth: 16,
|
||||||
borderRadius: 8,
|
height: 16,
|
||||||
backgroundColor: '#007AFF',
|
borderRadius: 8,
|
||||||
paddingHorizontal: 4,
|
backgroundColor: '#007AFF',
|
||||||
alignItems: 'center',
|
paddingHorizontal: 4,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
marginLeft: 5,
|
justifyContent: 'center',
|
||||||
},
|
marginLeft: 5,
|
||||||
tabBadgeText: {
|
},
|
||||||
fontSize: 9,
|
tabBadgeText: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 9,
|
||||||
color: '#fff',
|
fontFamily: 'Nunito_700Bold',
|
||||||
},
|
color: '#fff',
|
||||||
emptyBox: {
|
},
|
||||||
alignItems: 'center',
|
emptyBox: {
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 60,
|
justifyContent: 'center',
|
||||||
paddingHorizontal: 32,
|
paddingVertical: 60,
|
||||||
},
|
paddingHorizontal: 32,
|
||||||
emptyText: {
|
},
|
||||||
fontSize: 13,
|
emptyText: {
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontSize: 13,
|
||||||
color: '#a3a3a3',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
marginTop: 12,
|
color: colors.textMuted,
|
||||||
},
|
marginTop: 12,
|
||||||
// DM row styles
|
},
|
||||||
dmRow: {
|
dmRow: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
paddingVertical: 11,
|
paddingVertical: 11,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
borderBottomColor: '#f5f5f5',
|
borderBottomColor: colors.border,
|
||||||
},
|
},
|
||||||
dmAvatar: {
|
dmAvatar: {
|
||||||
width: 42,
|
width: 42,
|
||||||
height: 42,
|
height: 42,
|
||||||
borderRadius: 21,
|
borderRadius: 21,
|
||||||
backgroundColor: '#e5e5e5',
|
backgroundColor: colors.surfaceElevated,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
marginRight: 10,
|
marginRight: 10,
|
||||||
},
|
},
|
||||||
dmAvatarImg: { width: 42, height: 42 },
|
dmAvatarImg: { width: 42, height: 42 },
|
||||||
dmAvatarInitials: {
|
dmAvatarInitials: {
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontFamily: 'Nunito_700Bold',
|
||||||
color: '#525252',
|
color: colors.textMuted,
|
||||||
},
|
},
|
||||||
dmInfo: { flex: 1, minWidth: 0 },
|
dmInfo: { flex: 1, minWidth: 0 },
|
||||||
dmHeaderRow: {
|
dmHeaderRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
},
|
},
|
||||||
dmName: {
|
dmName: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontFamily: 'Nunito_700Bold',
|
||||||
color: '#171717',
|
color: colors.text,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
marginRight: 6,
|
marginRight: 6,
|
||||||
},
|
},
|
||||||
dmTime: { fontSize: 11, fontFamily: 'Nunito_600SemiBold' },
|
dmTime: { fontSize: 11, fontFamily: 'Nunito_600SemiBold' },
|
||||||
dmBottomRow: {
|
dmBottomRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
},
|
},
|
||||||
dmLast: { fontSize: 12, flex: 1 },
|
dmLast: { fontSize: 12, flex: 1 },
|
||||||
unreadBadge: {
|
unreadBadge: {
|
||||||
minWidth: 20,
|
minWidth: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
paddingHorizontal: 6,
|
paddingHorizontal: 6,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
backgroundColor: '#007AFF',
|
backgroundColor: '#007AFF',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
},
|
},
|
||||||
unreadBadgeText: {
|
unreadBadgeText: {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontFamily: 'Nunito_700Bold',
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
import { useRouter, useFocusEffect } from 'expo-router';
|
import { useRouter, useFocusEffect } from 'expo-router';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Placeholder-Screen für den Coach-Tab.
|
* Placeholder-Screen für den Coach-Tab.
|
||||||
@ -11,6 +12,7 @@ import { useRouter, useFocusEffect } from 'expo-router';
|
|||||||
*/
|
*/
|
||||||
export default function CoachTabRedirect() {
|
export default function CoachTabRedirect() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const colors = useColors();
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
@ -20,5 +22,5 @@ export default function CoachTabRedirect() {
|
|||||||
}, [router]),
|
}, [router]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return <View style={{ flex: 1, backgroundColor: '#ffffff' }} />;
|
return <View style={{ flex: 1, backgroundColor: colors.bg }} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,10 +20,12 @@ import { SuccessAlert } from '../../components/SuccessAlert';
|
|||||||
import { useMailStatus } from '../../hooks/useMailStatus';
|
import { useMailStatus } from '../../hooks/useMailStatus';
|
||||||
import { useMailDisconnect } from '../../hooks/useMailDisconnect';
|
import { useMailDisconnect } from '../../hooks/useMailDisconnect';
|
||||||
import { useUserPlan } from '../../hooks/useUserPlan';
|
import { useUserPlan } from '../../hooks/useUserPlan';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
export default function MailScreen() {
|
export default function MailScreen() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const tabBarHeight = useBottomTabBarHeight();
|
const tabBarHeight = useBottomTabBarHeight();
|
||||||
|
const colors = useColors();
|
||||||
|
|
||||||
const { plan } = useUserPlan();
|
const { plan } = useUserPlan();
|
||||||
|
|
||||||
@ -72,7 +74,7 @@ export default function MailScreen() {
|
|||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: '#fafafa' }}>
|
<View style={{ flex: 1, backgroundColor: colors.bg }}>
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<ActivityIndicator size="large" color="#007AFF" />
|
<ActivityIndicator size="large" color="#007AFF" />
|
||||||
@ -82,7 +84,7 @@ export default function MailScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: '#fafafa' }}>
|
<View style={{ flex: 1, backgroundColor: colors.bg }}>
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
@ -118,7 +120,7 @@ export default function MailScreen() {
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontFamily: 'Nunito_700Bold',
|
||||||
color: '#737373',
|
color: colors.textMuted,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
letterSpacing: 0.8,
|
letterSpacing: 0.8,
|
||||||
}}
|
}}
|
||||||
@ -129,7 +131,7 @@ export default function MailScreen() {
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#a3a3a3',
|
color: colors.textMuted,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -147,7 +149,7 @@ export default function MailScreen() {
|
|||||||
disabled={limitReached}
|
disabled={limitReached}
|
||||||
android_ripple={{ color: '#0066cc' }}
|
android_ripple={{ color: '#0066cc' }}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: limitReached ? '#e5e5e5' : '#007AFF',
|
backgroundColor: limitReached ? colors.surfaceElevated : '#007AFF',
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
opacity: limitReached ? 0.7 : 1,
|
opacity: limitReached ? 0.7 : 1,
|
||||||
shadowColor: '#007AFF',
|
shadowColor: '#007AFF',
|
||||||
@ -169,14 +171,14 @@ export default function MailScreen() {
|
|||||||
<Ionicons
|
<Ionicons
|
||||||
name="add"
|
name="add"
|
||||||
size={18}
|
size={18}
|
||||||
color={limitReached ? '#737373' : '#fff'}
|
color={limitReached ? colors.textMuted : '#fff'}
|
||||||
style={{ marginRight: 6 }}
|
style={{ marginRight: 6 }}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontFamily: 'Nunito_700Bold',
|
||||||
color: limitReached ? '#737373' : '#fff',
|
color: limitReached ? colors.textMuted : '#fff',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('mail.add_account')}
|
{t('mail.add_account')}
|
||||||
|
|||||||
@ -7,11 +7,12 @@ import { HeroShieldCheck } from '../../components/HeroShieldCheck';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { EmptyState } from '../../components/EmptyState';
|
import { EmptyState } from '../../components/EmptyState';
|
||||||
import { useNotificationStore, type AppNotification } from '../../stores/notifications';
|
import { useNotificationStore, type AppNotification } from '../../stores/notifications';
|
||||||
import { colors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
export default function NotificationsScreen() {
|
export default function NotificationsScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const items = useNotificationStore((s) => s.items);
|
const items = useNotificationStore((s) => s.items);
|
||||||
const loaded = useNotificationStore((s) => s.loaded);
|
const loaded = useNotificationStore((s) => s.loaded);
|
||||||
const load = useNotificationStore((s) => s.load);
|
const load = useNotificationStore((s) => s.load);
|
||||||
@ -28,17 +29,16 @@ export default function NotificationsScreen() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView className="flex-1 bg-white" edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={['top']}>
|
||||||
<View className="flex-row items-center gap-3 px-5 pt-3 pb-3 border-b border-neutral-200">
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 20, paddingTop: 12, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: colors.border }}>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => router.back()}
|
onPress={() => router.back()}
|
||||||
className="w-9 h-9 rounded-full bg-neutral-100 border border-neutral-200 items-center justify-center"
|
style={{ width: 36, height: 36, borderRadius: 18, backgroundColor: colors.surfaceElevated, borderWidth: 1, borderColor: colors.border, alignItems: 'center', justifyContent: 'center' }}
|
||||||
>
|
>
|
||||||
<Ionicons name="arrow-back" size={18} color="#737373" />
|
<Ionicons name="arrow-back" size={18} color={colors.textMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text
|
<Text
|
||||||
className="text-neutral-900 text-lg flex-1"
|
style={{ color: colors.text, fontSize: 18, flex: 1, fontFamily: 'Nunito_700Bold' }}
|
||||||
style={{ fontFamily: 'Nunito_700Bold' }}
|
|
||||||
>
|
>
|
||||||
{t('notifications.title')}
|
{t('notifications.title')}
|
||||||
</Text>
|
</Text>
|
||||||
@ -59,7 +59,7 @@ export default function NotificationsScreen() {
|
|||||||
<RefreshControl
|
<RefreshControl
|
||||||
refreshing={!loaded}
|
refreshing={!loaded}
|
||||||
onRefresh={load}
|
onRefresh={load}
|
||||||
tintColor={colors.brandOrange}
|
tintColor="#007AFF"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
@ -88,6 +88,7 @@ function NotificationRow({
|
|||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const colors = useColors();
|
||||||
const isUnread = !notif.readAt;
|
const isUnread = !notif.readAt;
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -103,8 +104,8 @@ function NotificationRow({
|
|||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#f5f5f5',
|
borderBottomColor: colors.border,
|
||||||
backgroundColor: isUnread ? '#fff7ed' : '#fff',
|
backgroundColor: isUnread ? colors.surface : colors.bg,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Pure-Icon — KEIN bg-Circle (User-Wunsch: kein extra Rand). */}
|
{/* Pure-Icon — KEIN bg-Circle (User-Wunsch: kein extra Rand). */}
|
||||||
@ -117,7 +118,7 @@ function NotificationRow({
|
|||||||
</View>
|
</View>
|
||||||
<View style={{ flex: 1, minWidth: 0, marginRight: 8 }}>
|
<View style={{ flex: 1, minWidth: 0, marginRight: 8 }}>
|
||||||
<Text
|
<Text
|
||||||
style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}
|
style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: colors.text }}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
{notif.actorName}
|
{notif.actorName}
|
||||||
@ -127,7 +128,7 @@ function NotificationRow({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#525252',
|
color: colors.textMuted,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
}}
|
}}
|
||||||
numberOfLines={2}
|
numberOfLines={2}
|
||||||
|
|||||||
@ -15,10 +15,11 @@ import { useEffect } from 'react';
|
|||||||
import { View, ActivityIndicator } from 'react-native';
|
import { View, ActivityIndicator } from 'react-native';
|
||||||
import { useRouter, useLocalSearchParams } from 'expo-router';
|
import { useRouter, useLocalSearchParams } from 'expo-router';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import { colors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
export default function AuthCallback() {
|
export default function AuthCallback() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const colors = useColors();
|
||||||
const params = useLocalSearchParams<{ access_token?: string; refresh_token?: string }>();
|
const params = useLocalSearchParams<{ access_token?: string; refresh_token?: string }>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -50,7 +51,7 @@ export default function AuthCallback() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: '#ffffff' }}>
|
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', backgroundColor: colors.bg }}>
|
||||||
<ActivityIndicator size="large" color={colors.brandOrange} />
|
<ActivityIndicator size="large" color={colors.brandOrange} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -3,10 +3,11 @@ import { View, Text, ScrollView, Pressable } from 'react-native';
|
|||||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { colors } from '../lib/theme';
|
import { useColors } from '../lib/theme';
|
||||||
|
|
||||||
export default function DebugScreen() {
|
export default function DebugScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const colors = useColors();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!__DEV__) {
|
if (!__DEV__) {
|
||||||
@ -15,11 +16,11 @@ export default function DebugScreen() {
|
|||||||
}, [router]);
|
}, [router]);
|
||||||
|
|
||||||
if (!__DEV__) {
|
if (!__DEV__) {
|
||||||
return <View style={{ flex: 1, backgroundColor: '#ffffff' }} />;
|
return <View style={{ flex: 1, backgroundColor: colors.bg }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: '#ffffff' }} edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={['top']}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
@ -48,7 +49,7 @@ export default function DebugScreen() {
|
|||||||
<Ionicons name="chevron-back" size={26} color={colors.text} />
|
<Ionicons name="chevron-back" size={26} color={colors.text} />
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text style={{ fontSize: 20, color: '#0a0a0a', fontFamily: 'Nunito_700Bold' }}>
|
<Text style={{ fontSize: 20, color: colors.text, fontFamily: 'Nunito_700Bold' }}>
|
||||||
Debug
|
Debug
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@ -119,10 +120,11 @@ function DebugStub({
|
|||||||
subtitle: string;
|
subtitle: string;
|
||||||
icon: React.ComponentProps<typeof Ionicons>['name'];
|
icon: React.ComponentProps<typeof Ionicons>['name'];
|
||||||
}) {
|
}) {
|
||||||
|
const colors = useColors();
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#fafafa',
|
backgroundColor: colors.surface,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: 'rgba(0,0,0,0.05)',
|
borderColor: 'rgba(0,0,0,0.05)',
|
||||||
@ -139,19 +141,19 @@ function DebugStub({
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 11,
|
borderRadius: 11,
|
||||||
backgroundColor: '#e5e7eb',
|
backgroundColor: colors.surfaceElevated,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name={icon} size={18} color="#525252" />
|
<Ionicons name={icon} size={18} color={colors.textMuted} />
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={{ fontSize: 14, color: '#0a0a0a', fontFamily: 'Nunito_700Bold' }}>{title}</Text>
|
<Text style={{ fontSize: 14, color: colors.text, fontFamily: 'Nunito_700Bold' }}>{title}</Text>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#737373',
|
color: colors.textMuted,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
marginTop: 3,
|
marginTop: 3,
|
||||||
lineHeight: 17,
|
lineHeight: 17,
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import { supabase } from '../lib/supabase';
|
|||||||
import { ChatBubble, type ChatMsg } from '../components/chat/ChatBubble';
|
import { ChatBubble, type ChatMsg } from '../components/chat/ChatBubble';
|
||||||
import { ChatInput, type SendPayload } from '../components/chat/ChatInput';
|
import { ChatInput, type SendPayload } from '../components/chat/ChatInput';
|
||||||
import { useDmRealtime } from '../hooks/useChatRealtime';
|
import { useDmRealtime } from '../hooks/useChatRealtime';
|
||||||
import { colors } from '../lib/theme';
|
import { useColors } from '../lib/theme';
|
||||||
|
|
||||||
type DmHistoryResponse = {
|
type DmHistoryResponse = {
|
||||||
partner: {
|
partner: {
|
||||||
@ -52,6 +52,8 @@ export default function DmScreen() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const colors = useColors();
|
||||||
|
const styles = makeStyles(colors);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const flatRef = useRef<FlatList>(null);
|
const flatRef = useRef<FlatList>(null);
|
||||||
const [myUserId, setMyUserId] = useState<string | undefined>(undefined);
|
const [myUserId, setMyUserId] = useState<string | undefined>(undefined);
|
||||||
@ -234,7 +236,7 @@ export default function DmScreen() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Pressable style={styles.backBtn} onPress={() => router.back()} hitSlop={8}>
|
<Pressable style={styles.backBtn} onPress={() => router.back()} hitSlop={8}>
|
||||||
<Ionicons name="chevron-back" size={22} color="#0a0a0a" />
|
<Ionicons name="chevron-back" size={22} color={colors.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<View style={styles.headerCenter}>
|
<View style={styles.headerCenter}>
|
||||||
<View style={styles.headerAvatar}>
|
<View style={styles.headerAvatar}>
|
||||||
@ -260,7 +262,7 @@ export default function DmScreen() {
|
|||||||
>
|
>
|
||||||
{isLoading && messages.length === 0 ? (
|
{isLoading && messages.length === 0 ? (
|
||||||
<View style={styles.loadingBox}>
|
<View style={styles.loadingBox}>
|
||||||
<ActivityIndicator color={colors.brandOrange} />
|
<ActivityIndicator color="#007AFF" />
|
||||||
</View>
|
</View>
|
||||||
) : messages.length === 0 ? (
|
) : messages.length === 0 ? (
|
||||||
<View style={styles.loadingBox}>
|
<View style={styles.loadingBox}>
|
||||||
@ -302,64 +304,66 @@ export default function DmScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function makeStyles(colors: ReturnType<typeof useColors>) {
|
||||||
container: { flex: 1, backgroundColor: '#fafafa' },
|
return StyleSheet.create({
|
||||||
header: {
|
container: { flex: 1, backgroundColor: colors.bg },
|
||||||
flexDirection: 'row',
|
header: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 12,
|
justifyContent: 'space-between',
|
||||||
paddingVertical: 10,
|
paddingHorizontal: 12,
|
||||||
backgroundColor: '#fff',
|
paddingVertical: 10,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
backgroundColor: colors.bg,
|
||||||
borderBottomColor: '#e5e5e5',
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
},
|
borderBottomColor: colors.border,
|
||||||
backBtn: {
|
},
|
||||||
width: 36,
|
backBtn: {
|
||||||
height: 36,
|
width: 36,
|
||||||
borderRadius: 12,
|
height: 36,
|
||||||
backgroundColor: '#f5f5f5',
|
borderRadius: 12,
|
||||||
alignItems: 'center',
|
backgroundColor: colors.surfaceElevated,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
headerCenter: {
|
},
|
||||||
flex: 1,
|
headerCenter: {
|
||||||
flexDirection: 'row',
|
flex: 1,
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
marginHorizontal: 8,
|
justifyContent: 'center',
|
||||||
},
|
marginHorizontal: 8,
|
||||||
headerAvatar: {
|
},
|
||||||
width: 32,
|
headerAvatar: {
|
||||||
height: 32,
|
width: 32,
|
||||||
borderRadius: 16,
|
height: 32,
|
||||||
backgroundColor: '#e5e5e5',
|
borderRadius: 16,
|
||||||
alignItems: 'center',
|
backgroundColor: colors.surfaceElevated,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
overflow: 'hidden',
|
justifyContent: 'center',
|
||||||
marginRight: 8,
|
overflow: 'hidden',
|
||||||
},
|
marginRight: 8,
|
||||||
headerAvatarImg: { width: 32, height: 32 },
|
},
|
||||||
headerAvatarInitials: {
|
headerAvatarImg: { width: 32, height: 32 },
|
||||||
fontSize: 11,
|
headerAvatarInitials: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 11,
|
||||||
color: '#737373',
|
fontFamily: 'Nunito_700Bold',
|
||||||
},
|
color: colors.textMuted,
|
||||||
headerName: {
|
},
|
||||||
fontSize: 15,
|
headerName: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 15,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_700Bold',
|
||||||
flexShrink: 1,
|
color: colors.text,
|
||||||
},
|
flexShrink: 1,
|
||||||
loadingBox: {
|
},
|
||||||
flex: 1,
|
loadingBox: {
|
||||||
alignItems: 'center',
|
flex: 1,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
emptyText: {
|
},
|
||||||
fontSize: 13,
|
emptyText: {
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontSize: 13,
|
||||||
color: '#a3a3a3',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
marginTop: 12,
|
color: colors.textMuted,
|
||||||
},
|
marginTop: 12,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import {
|
|||||||
TetrisGame,
|
TetrisGame,
|
||||||
} from '../components/urge/UrgeGames';
|
} from '../components/urge/UrgeGames';
|
||||||
import { GameCard } from '../components/games/GameCard';
|
import { GameCard } from '../components/games/GameCard';
|
||||||
import { colors } from '../lib/theme';
|
import { useColors } from '../lib/theme';
|
||||||
import { apiFetch } from '../lib/api';
|
import { apiFetch } from '../lib/api';
|
||||||
|
|
||||||
type GameStat = { avgStars: number; count: number };
|
type GameStat = { avgStars: number; count: number };
|
||||||
@ -31,6 +31,7 @@ type LastScore = { game: GameType; score: number } | null;
|
|||||||
export default function GamesScreen() {
|
export default function GamesScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const [active, setActive] = useState<GameType | null>(null);
|
const [active, setActive] = useState<GameType | null>(null);
|
||||||
const [lastScore, setLastScore] = useState<LastScore>(null);
|
const [lastScore, setLastScore] = useState<LastScore>(null);
|
||||||
const [gameStats, setGameStats] = useState<GameStats>(EMPTY_STATS);
|
const [gameStats, setGameStats] = useState<GameStats>(EMPTY_STATS);
|
||||||
@ -70,7 +71,7 @@ export default function GamesScreen() {
|
|||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: '#ffffff' }} edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={['top']}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
@ -79,7 +80,7 @@ export default function GamesScreen() {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: 'rgba(0,0,0,0.06)',
|
borderBottomColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -102,7 +103,7 @@ export default function GamesScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
|
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text }}>
|
||||||
{t(GAME_META.find((g) => g.id === active)!.titleKey)}
|
{t(GAME_META.find((g) => g.id === active)!.titleKey)}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ width: 60 }} />
|
<View style={{ width: 60 }} />
|
||||||
@ -127,7 +128,7 @@ export default function GamesScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={{ flex: 1, backgroundColor: '#ffffff' }} edges={['top']}>
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={['top']}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
paddingHorizontal: 12,
|
paddingHorizontal: 12,
|
||||||
@ -137,7 +138,7 @@ export default function GamesScreen() {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: 'rgba(0,0,0,0.06)',
|
borderBottomColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -156,7 +157,7 @@ export default function GamesScreen() {
|
|||||||
<Ionicons name="chevron-back" size={26} color={colors.text} />
|
<Ionicons name="chevron-back" size={26} color={colors.text} />
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text style={{ fontSize: 20, color: '#0a0a0a', fontFamily: 'Nunito_700Bold' }}>
|
<Text style={{ fontSize: 20, color: colors.text, fontFamily: 'Nunito_700Bold' }}>
|
||||||
{t('games.title')}
|
{t('games.title')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@ -169,7 +170,7 @@ export default function GamesScreen() {
|
|||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: '#737373',
|
color: colors.textMuted,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
lineHeight: 19,
|
lineHeight: 19,
|
||||||
marginBottom: 18,
|
marginBottom: 18,
|
||||||
@ -232,7 +233,7 @@ export default function GamesScreen() {
|
|||||||
style={{
|
style={{
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: '#a3a3a3',
|
color: colors.textMuted,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
marginTop: 24,
|
marginTop: 24,
|
||||||
opacity: 0.7,
|
opacity: 0.7,
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { View, Text, ScrollView, Pressable, Image } from 'react-native';
|
|||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { colors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
import { resolveAvatar } from '../../lib/resolveAvatar';
|
import { resolveAvatar } from '../../lib/resolveAvatar';
|
||||||
import type { Plan } from '../../hooks/useUserPlan';
|
import type { Plan } from '../../hooks/useUserPlan';
|
||||||
|
|
||||||
@ -52,13 +52,14 @@ type StatProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function ForeignStat({ value, label }: StatProps) {
|
function ForeignStat({ value, label }: StatProps) {
|
||||||
|
const colors = useColors();
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: colors.bg,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e5e5',
|
borderColor: colors.border,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -85,6 +86,7 @@ function ForeignStat({ value, label }: StatProps) {
|
|||||||
export default function ForeignProfileScreen() {
|
export default function ForeignProfileScreen() {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const colors = useColors();
|
||||||
const { userId } = useLocalSearchParams<{ userId: string }>();
|
const { userId } = useLocalSearchParams<{ userId: string }>();
|
||||||
const [imageFailed, setImageFailed] = useState(false);
|
const [imageFailed, setImageFailed] = useState(false);
|
||||||
const [isFollowing, setIsFollowing] = useState(DUMMY_FOREIGN.isFollowing);
|
const [isFollowing, setIsFollowing] = useState(DUMMY_FOREIGN.isFollowing);
|
||||||
@ -99,13 +101,13 @@ export default function ForeignProfileScreen() {
|
|||||||
const planStyle = planColors[profile.plan];
|
const planStyle = planColors[profile.plan];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: '#ffffff' }}>
|
<View style={{ flex: 1, backgroundColor: colors.bg }}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
paddingTop: insets.top,
|
paddingTop: insets.top,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: colors.bg,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#e5e5e5',
|
borderBottomColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
@ -147,7 +149,7 @@ export default function ForeignProfileScreen() {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: showImage ? '#fafafa' : colors.brandOrange,
|
backgroundColor: showImage ? colors.surface : colors.brandOrange,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showImage ? (
|
{showImage ? (
|
||||||
@ -215,9 +217,9 @@ export default function ForeignProfileScreen() {
|
|||||||
<View style={{
|
<View style={{
|
||||||
paddingVertical: 11,
|
paddingVertical: 11,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
backgroundColor: isFollowing ? '#f5f5f5' : colors.brandOrange,
|
backgroundColor: isFollowing ? colors.surfaceElevated : colors.brandOrange,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: isFollowing ? '#e5e5e5' : colors.brandOrange,
|
borderColor: isFollowing ? colors.border : colors.brandOrange,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}>
|
}}>
|
||||||
<Text
|
<Text
|
||||||
@ -244,9 +246,9 @@ export default function ForeignProfileScreen() {
|
|||||||
<View style={{
|
<View style={{
|
||||||
paddingVertical: 11,
|
paddingVertical: 11,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: colors.bg,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e5e5',
|
borderColor: colors.border,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}>
|
}}>
|
||||||
<Text
|
<Text
|
||||||
@ -292,9 +294,9 @@ export default function ForeignProfileScreen() {
|
|||||||
</Text>
|
</Text>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: colors.bg,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e5e5',
|
borderColor: colors.border,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
padding: 16,
|
padding: 16,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@ -18,29 +18,30 @@ import * as ImagePicker from 'expo-image-picker';
|
|||||||
// TODO(sdk54): migrate to new expo-file-system class-based API — see Task #14
|
// TODO(sdk54): migrate to new expo-file-system class-based API — see Task #14
|
||||||
import * as FileSystem from 'expo-file-system/legacy';
|
import * as FileSystem from 'expo-file-system/legacy';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { colors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
import { HERO_AVATARS, getAvatarUrl } from '../../lib/avatars';
|
import { HERO_AVATARS, getAvatarUrl } from '../../lib/avatars';
|
||||||
import { resolveAvatar } from '../../lib/resolveAvatar';
|
import { resolveAvatar } from '../../lib/resolveAvatar';
|
||||||
import { apiFetch } from '../../lib/api';
|
import { apiFetch } from '../../lib/api';
|
||||||
import { useMe } from '../../hooks/useMe';
|
import { useMe } from '../../hooks/useMe';
|
||||||
|
|
||||||
const INPUT_STYLE = {
|
|
||||||
fontSize: 16,
|
|
||||||
lineHeight: 22,
|
|
||||||
paddingVertical: 14,
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
color: colors.text,
|
|
||||||
fontFamily: 'Nunito_400Regular',
|
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
borderRadius: 12,
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
export default function ProfileEditScreen() {
|
export default function ProfileEditScreen() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const { me, reload } = useMe();
|
const { me, reload } = useMe();
|
||||||
|
|
||||||
|
const INPUT_STYLE = {
|
||||||
|
fontSize: 16,
|
||||||
|
lineHeight: 22,
|
||||||
|
paddingVertical: 14,
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
color: colors.text,
|
||||||
|
fontFamily: 'Nunito_400Regular',
|
||||||
|
backgroundColor: colors.surfaceElevated,
|
||||||
|
borderRadius: 12,
|
||||||
|
} as const;
|
||||||
|
|
||||||
const [nickname, setNickname] = useState(me?.nickname ?? '');
|
const [nickname, setNickname] = useState(me?.nickname ?? '');
|
||||||
const [avatarId, setAvatarId] = useState<string | null>(me?.avatar ?? null);
|
const [avatarId, setAvatarId] = useState<string | null>(me?.avatar ?? null);
|
||||||
const [photoUri, setPhotoUri] = useState<string | null>(null);
|
const [photoUri, setPhotoUri] = useState<string | null>(null);
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import { supabase } from '../lib/supabase';
|
|||||||
import { ChatBubble, type ChatMsg } from '../components/chat/ChatBubble';
|
import { ChatBubble, type ChatMsg } from '../components/chat/ChatBubble';
|
||||||
import { ChatInput, type SendPayload } from '../components/chat/ChatInput';
|
import { ChatInput, type SendPayload } from '../components/chat/ChatInput';
|
||||||
import { useRoomRealtime } from '../hooks/useChatRealtime';
|
import { useRoomRealtime } from '../hooks/useChatRealtime';
|
||||||
import { colors } from '../lib/theme';
|
import { useColors } from '../lib/theme';
|
||||||
|
|
||||||
const GROUP_GAP_MS = 5 * 60 * 1000;
|
const GROUP_GAP_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
@ -64,6 +64,8 @@ export default function RoomScreen() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const colors = useColors();
|
||||||
|
const styles = makeStyles(colors);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const flatRef = useRef<FlatList>(null);
|
const flatRef = useRef<FlatList>(null);
|
||||||
const [myUserId, setMyUserId] = useState<string | undefined>();
|
const [myUserId, setMyUserId] = useState<string | undefined>();
|
||||||
@ -298,7 +300,7 @@ export default function RoomScreen() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Pressable style={styles.iconBtn} onPress={() => router.back()} hitSlop={8}>
|
<Pressable style={styles.iconBtn} onPress={() => router.back()} hitSlop={8}>
|
||||||
<Ionicons name="chevron-back" size={22} color="#0a0a0a" />
|
<Ionicons name="chevron-back" size={22} color={colors.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<View style={styles.headerCenter}>
|
<View style={styles.headerCenter}>
|
||||||
<View style={styles.headerAvatar}>
|
<View style={styles.headerAvatar}>
|
||||||
@ -320,7 +322,7 @@ export default function RoomScreen() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Pressable style={styles.iconBtn} onPress={() => setSettingsOpen(true)} hitSlop={8}>
|
<Pressable style={styles.iconBtn} onPress={() => setSettingsOpen(true)} hitSlop={8}>
|
||||||
<Ionicons name="ellipsis-horizontal" size={20} color="#0a0a0a" />
|
<Ionicons name="ellipsis-horizontal" size={20} color={colors.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
@ -430,6 +432,8 @@ function RoomSettingsModal({
|
|||||||
roomId: string;
|
roomId: string;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
|
const modal = makeModalStyles(colors);
|
||||||
const [pendingRequests, setPendingRequests] = useState<any[]>([]);
|
const [pendingRequests, setPendingRequests] = useState<any[]>([]);
|
||||||
const [loadingReqs, setLoadingReqs] = useState(false);
|
const [loadingReqs, setLoadingReqs] = useState(false);
|
||||||
|
|
||||||
@ -637,221 +641,225 @@ function RoomSettingsModal({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function makeStyles(colors: ReturnType<typeof useColors>) {
|
||||||
container: { flex: 1, backgroundColor: '#fafafa' },
|
return StyleSheet.create({
|
||||||
header: {
|
container: { flex: 1, backgroundColor: colors.bg },
|
||||||
flexDirection: 'row',
|
header: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 12,
|
alignItems: 'center',
|
||||||
paddingVertical: 10,
|
paddingHorizontal: 12,
|
||||||
backgroundColor: '#fff',
|
paddingVertical: 10,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
backgroundColor: colors.bg,
|
||||||
borderBottomColor: '#e5e5e5',
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
},
|
borderBottomColor: colors.border,
|
||||||
iconBtn: {
|
},
|
||||||
width: 36,
|
iconBtn: {
|
||||||
height: 36,
|
width: 36,
|
||||||
borderRadius: 12,
|
height: 36,
|
||||||
backgroundColor: '#f5f5f5',
|
borderRadius: 12,
|
||||||
alignItems: 'center',
|
backgroundColor: colors.surfaceElevated,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
headerCenter: {
|
},
|
||||||
flex: 1,
|
headerCenter: {
|
||||||
flexDirection: 'row',
|
flex: 1,
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
marginHorizontal: 8,
|
alignItems: 'center',
|
||||||
},
|
marginHorizontal: 8,
|
||||||
headerAvatar: {
|
},
|
||||||
width: 36,
|
headerAvatar: {
|
||||||
height: 36,
|
width: 36,
|
||||||
borderRadius: 18,
|
height: 36,
|
||||||
backgroundColor: '#e5e5e5',
|
borderRadius: 18,
|
||||||
alignItems: 'center',
|
backgroundColor: colors.surfaceElevated,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
overflow: 'hidden',
|
justifyContent: 'center',
|
||||||
marginRight: 8,
|
overflow: 'hidden',
|
||||||
},
|
marginRight: 8,
|
||||||
headerAvatarImg: { width: 36, height: 36 },
|
},
|
||||||
headerAvatarInitials: {
|
headerAvatarImg: { width: 36, height: 36 },
|
||||||
fontSize: 12,
|
headerAvatarInitials: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 12,
|
||||||
color: '#737373',
|
fontFamily: 'Nunito_700Bold',
|
||||||
},
|
color: colors.textMuted,
|
||||||
headerName: {
|
},
|
||||||
fontSize: 15,
|
headerName: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 15,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_700Bold',
|
||||||
},
|
color: colors.text,
|
||||||
headerSub: {
|
},
|
||||||
fontSize: 11,
|
headerSub: {
|
||||||
fontFamily: 'Nunito_500Medium',
|
fontSize: 11,
|
||||||
color: '#737373',
|
fontFamily: 'Nunito_500Medium',
|
||||||
marginTop: 1,
|
color: colors.textMuted,
|
||||||
},
|
marginTop: 1,
|
||||||
loadingBox: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
},
|
||||||
joinBox: {
|
loadingBox: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
||||||
flex: 1,
|
joinBox: {
|
||||||
alignItems: 'center',
|
flex: 1,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 32,
|
justifyContent: 'center',
|
||||||
},
|
paddingHorizontal: 32,
|
||||||
joinTitle: {
|
},
|
||||||
fontSize: 20,
|
joinTitle: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 20,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_700Bold',
|
||||||
marginTop: 14,
|
color: colors.text,
|
||||||
},
|
marginTop: 14,
|
||||||
joinDesc: {
|
},
|
||||||
fontSize: 13,
|
joinDesc: {
|
||||||
fontFamily: 'Nunito_500Medium',
|
fontSize: 13,
|
||||||
color: '#737373',
|
fontFamily: 'Nunito_500Medium',
|
||||||
marginTop: 6,
|
color: colors.textMuted,
|
||||||
textAlign: 'center',
|
marginTop: 6,
|
||||||
},
|
textAlign: 'center',
|
||||||
joinHint: {
|
},
|
||||||
fontSize: 12,
|
joinHint: {
|
||||||
fontFamily: 'Nunito_500Medium',
|
fontSize: 12,
|
||||||
color: '#a3a3a3',
|
fontFamily: 'Nunito_500Medium',
|
||||||
marginTop: 18,
|
color: colors.textMuted,
|
||||||
textAlign: 'center',
|
marginTop: 18,
|
||||||
},
|
textAlign: 'center',
|
||||||
joinBtn: {
|
},
|
||||||
marginTop: 16,
|
joinBtn: {
|
||||||
backgroundColor: '#007AFF',
|
marginTop: 16,
|
||||||
paddingHorizontal: 32,
|
backgroundColor: '#007AFF',
|
||||||
paddingVertical: 12,
|
paddingHorizontal: 32,
|
||||||
borderRadius: 12,
|
paddingVertical: 12,
|
||||||
minWidth: 140,
|
borderRadius: 12,
|
||||||
alignItems: 'center',
|
minWidth: 140,
|
||||||
},
|
alignItems: 'center',
|
||||||
joinBtnText: {
|
},
|
||||||
color: '#fff',
|
joinBtnText: {
|
||||||
fontSize: 14,
|
color: '#fff',
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 14,
|
||||||
},
|
fontFamily: 'Nunito_700Bold',
|
||||||
pendingBadge: {
|
},
|
||||||
flexDirection: 'row',
|
pendingBadge: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
backgroundColor: '#fef3c7',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 14,
|
backgroundColor: '#fef3c7',
|
||||||
paddingVertical: 8,
|
paddingHorizontal: 14,
|
||||||
borderRadius: 20,
|
paddingVertical: 8,
|
||||||
marginTop: 16,
|
borderRadius: 20,
|
||||||
},
|
marginTop: 16,
|
||||||
pendingText: {
|
},
|
||||||
color: '#92400e',
|
pendingText: {
|
||||||
fontSize: 12,
|
color: '#92400e',
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 12,
|
||||||
marginLeft: 6,
|
fontFamily: 'Nunito_700Bold',
|
||||||
},
|
marginLeft: 6,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const modal = StyleSheet.create({
|
function makeModalStyles(colors: ReturnType<typeof useColors>) {
|
||||||
container: { flex: 1, backgroundColor: '#fafafa' },
|
return StyleSheet.create({
|
||||||
header: {
|
container: { flex: 1, backgroundColor: colors.bg },
|
||||||
flexDirection: 'row',
|
header: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
alignItems: 'center',
|
||||||
paddingHorizontal: 16,
|
justifyContent: 'space-between',
|
||||||
paddingVertical: 12,
|
paddingHorizontal: 16,
|
||||||
backgroundColor: '#fff',
|
paddingVertical: 12,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
backgroundColor: colors.bg,
|
||||||
borderBottomColor: '#e5e5e5',
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
},
|
borderBottomColor: colors.border,
|
||||||
title: { fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#171717' },
|
},
|
||||||
section: {
|
title: { fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text },
|
||||||
backgroundColor: '#fff',
|
section: {
|
||||||
borderRadius: 12,
|
backgroundColor: colors.surface,
|
||||||
padding: 14,
|
borderRadius: 12,
|
||||||
marginBottom: 12,
|
padding: 14,
|
||||||
},
|
marginBottom: 12,
|
||||||
sectionTitle: {
|
},
|
||||||
fontSize: 12,
|
sectionTitle: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 12,
|
||||||
color: '#737373',
|
fontFamily: 'Nunito_700Bold',
|
||||||
textTransform: 'uppercase',
|
color: colors.textMuted,
|
||||||
marginBottom: 10,
|
textTransform: 'uppercase',
|
||||||
letterSpacing: 0.5,
|
marginBottom: 10,
|
||||||
},
|
letterSpacing: 0.5,
|
||||||
avatarWrap: { alignSelf: 'center', marginBottom: 10 },
|
},
|
||||||
avatar: { width: 80, height: 80, borderRadius: 40 },
|
avatarWrap: { alignSelf: 'center', marginBottom: 10 },
|
||||||
avatarPlaceholder: {
|
avatar: { width: 80, height: 80, borderRadius: 40 },
|
||||||
backgroundColor: '#e5e5e5',
|
avatarPlaceholder: {
|
||||||
alignItems: 'center',
|
backgroundColor: colors.surfaceElevated,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
avatarEdit: {
|
},
|
||||||
position: 'absolute',
|
avatarEdit: {
|
||||||
right: -2,
|
position: 'absolute',
|
||||||
bottom: -2,
|
right: -2,
|
||||||
width: 28,
|
bottom: -2,
|
||||||
height: 28,
|
width: 28,
|
||||||
borderRadius: 14,
|
height: 28,
|
||||||
backgroundColor: '#007AFF',
|
borderRadius: 14,
|
||||||
borderWidth: 3,
|
backgroundColor: '#007AFF',
|
||||||
borderColor: '#fff',
|
borderWidth: 3,
|
||||||
alignItems: 'center',
|
borderColor: colors.bg,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
},
|
justifyContent: 'center',
|
||||||
roomName: {
|
},
|
||||||
fontSize: 17,
|
roomName: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 17,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_700Bold',
|
||||||
textAlign: 'center',
|
color: colors.text,
|
||||||
},
|
textAlign: 'center',
|
||||||
roomDesc: {
|
},
|
||||||
fontSize: 12,
|
roomDesc: {
|
||||||
fontFamily: 'Nunito_500Medium',
|
fontSize: 12,
|
||||||
color: '#737373',
|
fontFamily: 'Nunito_500Medium',
|
||||||
textAlign: 'center',
|
color: colors.textMuted,
|
||||||
marginTop: 4,
|
textAlign: 'center',
|
||||||
},
|
marginTop: 4,
|
||||||
memberRow: {
|
},
|
||||||
flexDirection: 'row',
|
memberRow: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingVertical: 8,
|
alignItems: 'center',
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
paddingVertical: 8,
|
||||||
borderBottomColor: '#f5f5f5',
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
},
|
borderBottomColor: colors.border,
|
||||||
memberAvatar: {
|
},
|
||||||
width: 32,
|
memberAvatar: {
|
||||||
height: 32,
|
width: 32,
|
||||||
borderRadius: 16,
|
height: 32,
|
||||||
backgroundColor: '#e5e5e5',
|
borderRadius: 16,
|
||||||
alignItems: 'center',
|
backgroundColor: colors.surfaceElevated,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
overflow: 'hidden',
|
justifyContent: 'center',
|
||||||
marginRight: 10,
|
overflow: 'hidden',
|
||||||
},
|
marginRight: 10,
|
||||||
memberAvatarImg: { width: 32, height: 32 },
|
},
|
||||||
memberInitials: {
|
memberAvatarImg: { width: 32, height: 32 },
|
||||||
fontSize: 11,
|
memberInitials: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 11,
|
||||||
color: '#737373',
|
fontFamily: 'Nunito_700Bold',
|
||||||
},
|
color: colors.textMuted,
|
||||||
memberName: { fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: '#171717' },
|
},
|
||||||
memberRole: { fontSize: 11, color: '#a3a3a3', marginTop: 1, textTransform: 'capitalize' },
|
memberName: { fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: colors.text },
|
||||||
actionBtn: {
|
memberRole: { fontSize: 11, color: colors.textMuted, marginTop: 1, textTransform: 'capitalize' },
|
||||||
paddingHorizontal: 10,
|
actionBtn: {
|
||||||
paddingVertical: 5,
|
paddingHorizontal: 10,
|
||||||
borderRadius: 6,
|
paddingVertical: 5,
|
||||||
},
|
borderRadius: 6,
|
||||||
actionText: { fontSize: 11, fontFamily: 'Nunito_700Bold' },
|
},
|
||||||
emptyText: { fontSize: 12, color: '#a3a3a3' },
|
actionText: { fontSize: 11, fontFamily: 'Nunito_700Bold' },
|
||||||
leaveBtn: {
|
emptyText: { fontSize: 12, color: colors.textMuted },
|
||||||
flexDirection: 'row',
|
leaveBtn: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#fee2e2',
|
justifyContent: 'center',
|
||||||
paddingVertical: 12,
|
backgroundColor: '#fee2e2',
|
||||||
borderRadius: 10,
|
paddingVertical: 12,
|
||||||
marginTop: 8,
|
borderRadius: 10,
|
||||||
},
|
marginTop: 8,
|
||||||
leaveText: {
|
},
|
||||||
color: '#991b1b',
|
leaveText: {
|
||||||
fontSize: 13,
|
color: '#991b1b',
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 13,
|
||||||
marginLeft: 6,
|
fontFamily: 'Nunito_700Bold',
|
||||||
},
|
marginLeft: 6,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { RiveAvatar } from '../components/RiveAvatar';
|
import { RiveAvatar } from '../components/RiveAvatar';
|
||||||
import { apiFetch } from '../lib/api';
|
import { apiFetch } from '../lib/api';
|
||||||
import { supabase } from '../lib/supabase';
|
import { supabase } from '../lib/supabase';
|
||||||
import { colors } from '../lib/theme';
|
import { useColors } from '../lib/theme';
|
||||||
import {
|
import {
|
||||||
type GameType, GAME_META, MemoryGame, TicTacToeGame, SnakeGame, TetrisGame,
|
type GameType, GAME_META, MemoryGame, TicTacToeGame, SnakeGame, TetrisGame,
|
||||||
} from '../components/urge/UrgeGames';
|
} from '../components/urge/UrgeGames';
|
||||||
@ -41,6 +41,8 @@ export default function SOSScreen() {
|
|||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const colors = useColors();
|
||||||
|
const st = makeStyles(colors);
|
||||||
const flatRef = useRef<FlatList>(null);
|
const flatRef = useRef<FlatList>(null);
|
||||||
|
|
||||||
const [messages, setMessages] = useState<SosMsg[]>([]);
|
const [messages, setMessages] = useState<SosMsg[]>([]);
|
||||||
@ -1089,7 +1091,7 @@ export default function SOSScreen() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View style={[st.topBar, { top: insets.top + 6 }]}>
|
<View style={[st.topBar, { top: insets.top + 6 }]}>
|
||||||
<Pressable style={st.actionBtn} onPress={attemptExit} hitSlop={12}>
|
<Pressable style={st.actionBtn} onPress={attemptExit} hitSlop={12}>
|
||||||
<Ionicons name="close" size={22} color="#374151" />
|
<Ionicons name="close" size={22} color={colors.textMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<View style={st.avatarCenter}>
|
<View style={st.avatarCenter}>
|
||||||
<RiveAvatar emotion={emotion} size="md" />
|
<RiveAvatar emotion={emotion} size="md" />
|
||||||
@ -1286,39 +1288,41 @@ export default function SOSScreen() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const st = StyleSheet.create({
|
function makeStyles(colors: ReturnType<typeof useColors>) {
|
||||||
container: { flex: 1, backgroundColor: '#ffffff' },
|
return StyleSheet.create({
|
||||||
topBar: { position: 'absolute', left: 0, right: 0, zIndex: 10, flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', paddingHorizontal: 12 },
|
container: { flex: 1, backgroundColor: colors.bg },
|
||||||
topBarBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9, backgroundColor: '#ffffff' },
|
topBar: { position: 'absolute', left: 0, right: 0, zIndex: 10, flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', paddingHorizontal: 12 },
|
||||||
actionBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.92)', alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 6, elevation: 4 },
|
topBarBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 9, backgroundColor: colors.bg },
|
||||||
avatarCenter: { flex: 1, alignItems: 'center', gap: 4 },
|
actionBtn: { width: 40, height: 40, borderRadius: 20, backgroundColor: colors.surface, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 6, elevation: 4 },
|
||||||
avatarMeta: { alignItems: 'center', gap: 2 },
|
avatarCenter: { flex: 1, alignItems: 'center', gap: 4 },
|
||||||
avatarName: { fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' },
|
avatarMeta: { alignItems: 'center', gap: 2 },
|
||||||
speakingRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
avatarName: { fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text },
|
||||||
stopBtn: { width: 18, height: 18, borderRadius: 9, backgroundColor: '#f5f5f5', alignItems: 'center', justifyContent: 'center' },
|
speakingRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
|
||||||
listContent: { paddingHorizontal: 12, paddingBottom: 4 },
|
stopBtn: { width: 18, height: 18, borderRadius: 9, backgroundColor: colors.surfaceElevated, alignItems: 'center', justifyContent: 'center' },
|
||||||
scrollDownBtn: { position: 'absolute', bottom: 8, right: 16, width: 36, height: 36, borderRadius: 18, backgroundColor: '#374151', alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4, elevation: 4 },
|
listContent: { paddingHorizontal: 12, paddingBottom: 4 },
|
||||||
chip: {
|
scrollDownBtn: { position: 'absolute', bottom: 8, right: 16, width: 36, height: 36, borderRadius: 18, backgroundColor: '#374151', alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.15, shadowRadius: 4, elevation: 4 },
|
||||||
borderRadius: 14,
|
chip: {
|
||||||
borderWidth: 1.5,
|
borderRadius: 14,
|
||||||
borderColor: '#9ca3af', // sichtbarer Ring (medium-grau gegen weiß)
|
borderWidth: 1.5,
|
||||||
backgroundColor: '#ffffff',
|
borderColor: colors.border,
|
||||||
paddingHorizontal: 16,
|
backgroundColor: colors.bg,
|
||||||
paddingVertical: 11,
|
paddingHorizontal: 16,
|
||||||
shadowColor: '#000',
|
paddingVertical: 11,
|
||||||
shadowOffset: { width: 0, height: 2 },
|
shadowColor: '#000',
|
||||||
shadowOpacity: 0.12,
|
shadowOffset: { width: 0, height: 2 },
|
||||||
shadowRadius: 6,
|
shadowOpacity: 0.12,
|
||||||
elevation: 3,
|
shadowRadius: 6,
|
||||||
},
|
elevation: 3,
|
||||||
chipPressed: {
|
},
|
||||||
backgroundColor: '#f3f4f6',
|
chipPressed: {
|
||||||
borderColor: '#6b7280', // dunkler beim Press → spürbares Feedback
|
backgroundColor: colors.surfaceElevated,
|
||||||
transform: [{ scale: 0.97 }],
|
borderColor: colors.textMuted,
|
||||||
shadowOpacity: 0.05,
|
transform: [{ scale: 0.97 }],
|
||||||
},
|
shadowOpacity: 0.05,
|
||||||
chipText: { fontFamily: 'Nunito_600SemiBold', fontSize: 14, color: '#334155' },
|
},
|
||||||
inputBar: { flexDirection: 'row', alignItems: 'flex-end', paddingHorizontal: 12, paddingTop: 8, borderTopWidth: 1, borderTopColor: '#f3f4f6', backgroundColor: '#fff', gap: 8 },
|
chipText: { fontFamily: 'Nunito_600SemiBold', fontSize: 14, color: colors.textMuted },
|
||||||
textInput: { flex: 1, minHeight: 40, maxHeight: 120, backgroundColor: '#f3f4f6', borderRadius: 20, paddingHorizontal: 14, paddingVertical: 10, fontSize: 15, fontFamily: 'Nunito_400Regular', color: '#111827' },
|
inputBar: { flexDirection: 'row', alignItems: 'flex-end', paddingHorizontal: 12, paddingTop: 8, borderTopWidth: 1, borderTopColor: colors.border, backgroundColor: colors.bg, gap: 8 },
|
||||||
sendBtn: { width: 38, height: 38, borderRadius: 19, backgroundColor: '#007AFF', alignItems: 'center', justifyContent: 'center' },
|
textInput: { flex: 1, minHeight: 40, maxHeight: 120, backgroundColor: colors.surfaceElevated, borderRadius: 20, paddingHorizontal: 14, paddingVertical: 10, fontSize: 15, fontFamily: 'Nunito_400Regular', color: colors.text },
|
||||||
});
|
sendBtn: { width: 38, height: 38, borderRadius: 19, backgroundColor: '#007AFF', alignItems: 'center', justifyContent: 'center' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ import * as ImagePicker from 'expo-image-picker';
|
|||||||
import { apiFetch } from '../lib/api';
|
import { apiFetch } from '../lib/api';
|
||||||
import { resolveAvatar } from '../lib/resolveAvatar';
|
import { resolveAvatar } from '../lib/resolveAvatar';
|
||||||
import { useAuthStore } from '../stores/auth';
|
import { useAuthStore } from '../stores/auth';
|
||||||
import { colors } from '../lib/theme';
|
import { useColors } from '../lib/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onPosted?: () => void;
|
onPosted?: () => void;
|
||||||
@ -25,6 +25,7 @@ type Props = {
|
|||||||
|
|
||||||
export function ComposeCard({ onPosted }: Props) {
|
export function ComposeCard({ onPosted }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const { user } = useAuthStore();
|
const { user } = useAuthStore();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const inputRef = useRef<TextInput>(null);
|
const inputRef = useRef<TextInput>(null);
|
||||||
@ -101,7 +102,7 @@ export function ComposeCard({ onPosted }: Props) {
|
|||||||
const showActions = focused || content.length > 0;
|
const showActions = focused || content.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-white border border-neutral-200 rounded-2xl p-4 mb-4">
|
<View style={{ backgroundColor: colors.bg, borderWidth: 1, borderColor: colors.border, borderRadius: 16, padding: 16, marginBottom: 16 }}>
|
||||||
<View className="flex-row items-start gap-3">
|
<View className="flex-row items-start gap-3">
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: avatarUrl }}
|
source={{ uri: avatarUrl }}
|
||||||
@ -114,10 +115,10 @@ export function ComposeCard({ onPosted }: Props) {
|
|||||||
onChangeText={setContent}
|
onChangeText={setContent}
|
||||||
onFocus={() => setFocused(true)}
|
onFocus={() => setFocused(true)}
|
||||||
placeholder={t('community.compose_placeholder')}
|
placeholder={t('community.compose_placeholder')}
|
||||||
placeholderTextColor="#a3a3a3"
|
placeholderTextColor={colors.textMuted}
|
||||||
multiline
|
multiline
|
||||||
className="text-sm text-neutral-900 leading-5 min-h-[40px]"
|
className="text-sm leading-5 min-h-[40px]"
|
||||||
style={{ textAlignVertical: 'top', fontFamily: 'Nunito_400Regular' }}
|
style={{ textAlignVertical: 'top', fontFamily: 'Nunito_400Regular', color: colors.text }}
|
||||||
/>
|
/>
|
||||||
{imageUri && (
|
{imageUri && (
|
||||||
<View className="relative mt-2">
|
<View className="relative mt-2">
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react';
|
|||||||
import { TrueSheet, type SheetDetent } from '@lodev09/react-native-true-sheet';
|
import { TrueSheet, type SheetDetent } from '@lodev09/react-native-true-sheet';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { colors } from '../lib/theme';
|
import { useColors } from '../lib/theme';
|
||||||
import { apiFetch } from '../lib/api';
|
import { apiFetch } from '../lib/api';
|
||||||
import { useDeviceLimitStore, type DeviceLimitDevice } from '../stores/deviceLimit';
|
import { useDeviceLimitStore, type DeviceLimitDevice } from '../stores/deviceLimit';
|
||||||
|
|
||||||
@ -40,6 +40,7 @@ function DeviceLimitRow({
|
|||||||
onRemove: (id: string) => void;
|
onRemove: (id: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@ -119,6 +120,7 @@ function DeviceLimitRow({
|
|||||||
|
|
||||||
export function DeviceLimitReachedSheet() {
|
export function DeviceLimitReachedSheet() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const sheetRef = useRef<TrueSheet>(null);
|
const sheetRef = useRef<TrueSheet>(null);
|
||||||
const { visible, devices, max, plan, hide, removeDevice } = useDeviceLimitStore();
|
const { visible, devices, max, plan, hide, removeDevice } = useDeviceLimitStore();
|
||||||
const [removingId, setRemovingId] = useState<string | null>(null);
|
const [removingId, setRemovingId] = useState<string | null>(null);
|
||||||
@ -195,7 +197,7 @@ export function DeviceLimitReachedSheet() {
|
|||||||
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: colors.bg,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useNotificationStore, type AppNotification } from '../stores/notifications';
|
import { useNotificationStore, type AppNotification } from '../stores/notifications';
|
||||||
import { resolveAvatar } from '../lib/resolveAvatar';
|
import { resolveAvatar } from '../lib/resolveAvatar';
|
||||||
import { HeroShieldCheck } from './HeroShieldCheck';
|
import { HeroShieldCheck } from './HeroShieldCheck';
|
||||||
|
import { useColors } from '../lib/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -16,6 +17,7 @@ type Props = {
|
|||||||
|
|
||||||
export function NotificationsDropdown({ visible, onClose, topOffset }: Props) {
|
export function NotificationsDropdown({ visible, onClose, topOffset }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const items = useNotificationStore((s) => s.items);
|
const items = useNotificationStore((s) => s.items);
|
||||||
const loaded = useNotificationStore((s) => s.loaded);
|
const loaded = useNotificationStore((s) => s.loaded);
|
||||||
@ -71,7 +73,7 @@ export function NotificationsDropdown({ visible, onClose, topOffset }: Props) {
|
|||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: topOffset + 6,
|
top: topOffset + 6,
|
||||||
right: 12,
|
right: 12,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: colors.bg,
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
shadowOffset: { width: 0, height: 8 },
|
shadowOffset: { width: 0, height: 8 },
|
||||||
@ -93,11 +95,11 @@ export function NotificationsDropdown({ visible, onClose, topOffset }: Props) {
|
|||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#f0f0f0',
|
borderBottomColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={{ flex: 1, fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}
|
style={{ flex: 1, fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text }}
|
||||||
>
|
>
|
||||||
{t('notifications.title')}
|
{t('notifications.title')}
|
||||||
</Text>
|
</Text>
|
||||||
@ -114,13 +116,13 @@ export function NotificationsDropdown({ visible, onClose, topOffset }: Props) {
|
|||||||
|
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<View style={{ paddingVertical: 32, alignItems: 'center' }}>
|
<View style={{ paddingVertical: 32, alignItems: 'center' }}>
|
||||||
<Ionicons name="notifications-off-outline" size={28} color="#a3a3a3" />
|
<Ionicons name="notifications-off-outline" size={28} color={colors.textMuted} />
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
color: '#525252',
|
color: colors.textMuted,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('notifications.empty_title')}
|
{t('notifications.empty_title')}
|
||||||
@ -130,7 +132,7 @@ export function NotificationsDropdown({ visible, onClose, topOffset }: Props) {
|
|||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#a3a3a3',
|
color: colors.textMuted,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
paddingHorizontal: 20,
|
paddingHorizontal: 20,
|
||||||
}}
|
}}
|
||||||
@ -221,6 +223,7 @@ function NotificationRow({
|
|||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
t: (k: string, opts?: any) => string;
|
t: (k: string, opts?: any) => string;
|
||||||
}) {
|
}) {
|
||||||
|
const colors = useColors();
|
||||||
const isUnread = !notif.readAt;
|
const isUnread = !notif.readAt;
|
||||||
const { icon, color, bg } = notifIcon(notif.type);
|
const { icon, color, bg } = notifIcon(notif.type);
|
||||||
const isSocial =
|
const isSocial =
|
||||||
@ -249,8 +252,8 @@ function NotificationRow({
|
|||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
paddingVertical: 11,
|
paddingVertical: 11,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#f5f5f5',
|
borderBottomColor: colors.border,
|
||||||
backgroundColor: isUnread ? '#fff7ed' : '#ffffff',
|
backgroundColor: isUnread ? colors.surface : colors.bg,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Avatar-Logik:
|
{/* Avatar-Logik:
|
||||||
@ -297,7 +300,7 @@ function NotificationRow({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
color: '#0a0a0a',
|
color: colors.text,
|
||||||
lineHeight: 16,
|
lineHeight: 16,
|
||||||
}}
|
}}
|
||||||
numberOfLines={2}
|
numberOfLines={2}
|
||||||
@ -308,7 +311,7 @@ function NotificationRow({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#a3a3a3',
|
color: colors.textMuted,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
Easing,
|
Easing,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||||
import { colors } from '../lib/theme';
|
import { useColors } from '../lib/theme';
|
||||||
|
|
||||||
type Option<T> = {
|
type Option<T> = {
|
||||||
value: T;
|
value: T;
|
||||||
@ -51,6 +51,7 @@ export function OptionsBottomSheet<T extends string | number>({
|
|||||||
onClose,
|
onClose,
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
|
const colors = useColors();
|
||||||
const translateY = useRef(new Animated.Value(400)).current;
|
const translateY = useRef(new Animated.Value(400)).current;
|
||||||
const backdropOpacity = useRef(new Animated.Value(0)).current;
|
const backdropOpacity = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { formatRelativeTime } from '../lib/formatTime';
|
|||||||
import { useCommunityStore, type CommunityPost } from '../stores/community';
|
import { useCommunityStore, type CommunityPost } from '../stores/community';
|
||||||
import { RiveAvatar } from './RiveAvatar';
|
import { RiveAvatar } from './RiveAvatar';
|
||||||
import { HeroShieldCheck } from './HeroShieldCheck';
|
import { HeroShieldCheck } from './HeroShieldCheck';
|
||||||
|
import { useColors } from '../lib/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
post: CommunityPost;
|
post: CommunityPost;
|
||||||
@ -17,6 +18,7 @@ type Props = {
|
|||||||
|
|
||||||
function PostCardImpl({ post, onCommentPress }: Props) {
|
function PostCardImpl({ post, onCommentPress }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
// Granular selectors — subscribing to the whole store would re-render every
|
// Granular selectors — subscribing to the whole store would re-render every
|
||||||
// PostCard whenever any user likes any post (optimisticLikes mutates).
|
// PostCard whenever any user likes any post (optimisticLikes mutates).
|
||||||
@ -162,7 +164,7 @@ function PostCardImpl({ post, onCommentPress }: Props) {
|
|||||||
}, [isLiking, localLike, localCount, post.id, post.userLike, post.likesCount, applyOptimisticLike, clearOptimisticLike, revertOptimisticLike, queryClient, triggerHeartPop]);
|
}, [isLiking, localLike, localCount, post.id, post.userLike, post.likesCount, applyOptimisticLike, clearOptimisticLike, revertOptimisticLike, queryClient, triggerHeartPop]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View className="bg-white border border-neutral-200 rounded-2xl p-3 mb-3">
|
<View style={{ backgroundColor: colors.bg, borderWidth: 1, borderColor: colors.border, borderRadius: 16, padding: 12, marginBottom: 12 }}>
|
||||||
{/* Repost header */}
|
{/* Repost header */}
|
||||||
{post.repostOf && (
|
{post.repostOf && (
|
||||||
<View className="flex-row items-center gap-1.5 mb-3">
|
<View className="flex-row items-center gap-1.5 mb-3">
|
||||||
@ -194,35 +196,35 @@ function PostCardImpl({ post, onCommentPress }: Props) {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
<View className="flex-1 min-w-0">
|
<View className="flex-1 min-w-0">
|
||||||
<Text className="text-sm text-neutral-900" numberOfLines={1} style={{ fontFamily: 'Nunito_600SemiBold' }}>
|
<Text style={{ fontSize: 14, color: colors.text, fontFamily: 'Nunito_600SemiBold' }} numberOfLines={1}>
|
||||||
{authorLabel}
|
{authorLabel}
|
||||||
</Text>
|
</Text>
|
||||||
{authorDescription !== undefined && (
|
{authorDescription !== undefined && (
|
||||||
<Text className="text-xs text-neutral-400" style={{ fontFamily: 'Nunito_400Regular' }}>{authorDescription}</Text>
|
<Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_400Regular' }}>{authorDescription}</Text>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Text className="text-xs text-neutral-400 shrink-0 ml-2 mt-0.5" style={{ fontFamily: 'Nunito_400Regular' }}>
|
<Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_400Regular', flexShrink: 0, marginLeft: 8, marginTop: 2 }}>
|
||||||
{formatRelativeTime(post.createdAt)}
|
{formatRelativeTime(post.createdAt)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Content — hidden for domain_vote (replaced by poll below) */}
|
{/* Content — hidden for domain_vote (replaced by poll below) */}
|
||||||
{!!displayContent && post.category !== 'domain_vote' && (
|
{!!displayContent && post.category !== 'domain_vote' && (
|
||||||
<Text className="text-sm text-neutral-800 leading-relaxed mb-0" style={{ fontFamily: 'Nunito_400Regular' }}>
|
<Text style={{ fontSize: 14, color: colors.textMuted, fontFamily: 'Nunito_400Regular', lineHeight: 21 }}>
|
||||||
{displayContent}
|
{displayContent}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* domain_approved: favicon + domain name + shield badge */}
|
{/* domain_approved: favicon + domain name + shield badge */}
|
||||||
{post.category === 'domain_approved' && !!approvedDomain && (
|
{post.category === 'domain_approved' && !!approvedDomain && (
|
||||||
<View className="flex-row items-center gap-2.5 mt-3 rounded-xl border border-neutral-200 bg-neutral-50 px-3 py-2">
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 10, marginTop: 12, borderRadius: 12, borderWidth: 1, borderColor: colors.border, backgroundColor: colors.surface, paddingHorizontal: 12, paddingVertical: 8 }}>
|
||||||
<DomainFavicon domain={approvedDomain} size={24} />
|
<DomainFavicon domain={approvedDomain} size={24} />
|
||||||
<View className="flex-1 min-w-0">
|
<View style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text className="text-xs font-semibold text-neutral-900 truncate" style={{ fontFamily: 'Nunito_700Bold' }}>
|
<Text style={{ fontSize: 12, color: colors.text, fontFamily: 'Nunito_700Bold' }} numberOfLines={1}>
|
||||||
{approvedDomain}
|
{approvedDomain}
|
||||||
</Text>
|
</Text>
|
||||||
<Text className="text-xs text-neutral-400" style={{ fontFamily: 'Nunito_400Regular' }}>
|
<Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_400Regular' }}>
|
||||||
{t('community.domain_added_to_blocklist')}
|
{t('community.domain_added_to_blocklist')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -1,18 +1,20 @@
|
|||||||
import { View } from 'react-native';
|
import { View } from 'react-native';
|
||||||
|
import { useColors } from '../lib/theme';
|
||||||
|
|
||||||
export function PostCardSkeleton() {
|
export function PostCardSkeleton() {
|
||||||
|
const colors = useColors();
|
||||||
return (
|
return (
|
||||||
<View className="bg-white border border-neutral-200 rounded-2xl p-4 mb-3">
|
<View style={{ backgroundColor: colors.bg, borderWidth: 1, borderColor: colors.border, borderRadius: 16, padding: 16, marginBottom: 12 }}>
|
||||||
<View className="flex-row items-center gap-3 mb-3">
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 12 }}>
|
||||||
<View className="w-9 h-9 rounded-full bg-neutral-200" />
|
<View style={{ width: 36, height: 36, borderRadius: 18, backgroundColor: colors.surfaceElevated }} />
|
||||||
<View className="flex-1 gap-1.5">
|
<View style={{ flex: 1, gap: 6 }}>
|
||||||
<View className="h-3 bg-neutral-200 rounded w-1/3" />
|
<View style={{ height: 12, backgroundColor: colors.surfaceElevated, borderRadius: 6, width: '33%' }} />
|
||||||
<View className="h-2.5 bg-neutral-100 rounded w-1/4" />
|
<View style={{ height: 10, backgroundColor: colors.surface, borderRadius: 6, width: '25%' }} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className="h-3 bg-neutral-200 rounded w-full mb-2" />
|
<View style={{ height: 12, backgroundColor: colors.surfaceElevated, borderRadius: 6, width: '100%', marginBottom: 8 }} />
|
||||||
<View className="h-3 bg-neutral-200 rounded w-3/4 mb-2" />
|
<View style={{ height: 12, backgroundColor: colors.surfaceElevated, borderRadius: 6, width: '75%', marginBottom: 8 }} />
|
||||||
<View className="h-3 bg-neutral-100 rounded w-1/2" />
|
<View style={{ height: 12, backgroundColor: colors.surface, borderRadius: 6, width: '50%' }} />
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { apiFetch } from '../lib/api';
|
import { apiFetch } from '../lib/api';
|
||||||
import { formatRelativeTime } from '../lib/formatTime';
|
import { formatRelativeTime } from '../lib/formatTime';
|
||||||
import { colors } from '../lib/theme';
|
import { useColors } from '../lib/theme';
|
||||||
import type { CommunityComment } from '../stores/community';
|
import type { CommunityComment } from '../stores/community';
|
||||||
|
|
||||||
const EMOJIS = ['❤️', '🙌', '🔥', '👏', '😢', '😍', '😮', '😂'];
|
const EMOJIS = ['❤️', '🙌', '🔥', '👏', '😢', '😍', '😮', '😂'];
|
||||||
@ -33,6 +33,7 @@ type Props = {
|
|||||||
|
|
||||||
export function PostCommentsSheet({ postId, visible, onClose }: Props) {
|
export function PostCommentsSheet({ postId, visible, onClose }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const inputRef = useRef<TextInput>(null);
|
const inputRef = useRef<TextInput>(null);
|
||||||
@ -230,7 +231,7 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
|
|||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: colors.bg,
|
||||||
borderTopLeftRadius: 24,
|
borderTopLeftRadius: 24,
|
||||||
borderTopRightRadius: 24,
|
borderTopRightRadius: 24,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@ -255,7 +256,7 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 5,
|
height: 5,
|
||||||
borderRadius: 3,
|
borderRadius: 3,
|
||||||
backgroundColor: '#d4d4d8',
|
backgroundColor: colors.border,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@ -268,10 +269,10 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
|
|||||||
paddingTop: 6,
|
paddingTop: 6,
|
||||||
paddingBottom: 12,
|
paddingBottom: 12,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#e5e5e5',
|
borderBottomColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
|
||||||
{t('community.comments_title')}
|
{t('community.comments_title')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@ -323,7 +324,7 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
|
|||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: '#f5f5f5',
|
borderTopColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{EMOJIS.map((e) => (
|
{EMOJIS.map((e) => (
|
||||||
@ -342,10 +343,10 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
|
|||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 8,
|
paddingVertical: 8,
|
||||||
backgroundColor: '#fafafa',
|
backgroundColor: colors.surface,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontSize: 12, color: '#737373', fontFamily: 'Nunito_400Regular' }}>
|
<Text style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_400Regular' }}>
|
||||||
{t('community.reply_to')}{' '}
|
{t('community.reply_to')}{' '}
|
||||||
<Text style={{ fontFamily: 'Nunito_700Bold' }}>@{replyTarget.nickname}</Text>
|
<Text style={{ fontFamily: 'Nunito_700Bold' }}>@{replyTarget.nickname}</Text>
|
||||||
</Text>
|
</Text>
|
||||||
@ -366,7 +367,7 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
|
|||||||
// sonst Safe-Area
|
// sonst Safe-Area
|
||||||
paddingBottom: keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom),
|
paddingBottom: keyboardHeight > 0 ? 8 : Math.max(12, insets.bottom),
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: '#e5e5e5',
|
borderTopColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
@ -374,18 +375,18 @@ export function PostCommentsSheet({ postId, visible, onClose }: Props) {
|
|||||||
value={text}
|
value={text}
|
||||||
onChangeText={setText}
|
onChangeText={setText}
|
||||||
placeholder={t('community.comment_placeholder')}
|
placeholder={t('community.comment_placeholder')}
|
||||||
placeholderTextColor="#a3a3a3"
|
placeholderTextColor={colors.textMuted}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#fafafa',
|
backgroundColor: colors.surface,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e5e5',
|
borderColor: colors.border,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#0a0a0a',
|
color: colors.text,
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
}}
|
}}
|
||||||
returnKeyType="send"
|
returnKeyType="send"
|
||||||
@ -430,6 +431,7 @@ type CommentRowProps = {
|
|||||||
|
|
||||||
function CommentRow({ comment, isReply = false, onReply, onLike }: CommentRowProps) {
|
function CommentRow({ comment, isReply = false, onReply, onLike }: CommentRowProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const heartScale = useRef(new Animated.Value(1)).current;
|
const heartScale = useRef(new Animated.Value(1)).current;
|
||||||
const handleLikeWithPop = useCallback(() => {
|
const handleLikeWithPop = useCallback(() => {
|
||||||
heartScale.setValue(1);
|
heartScale.setValue(1);
|
||||||
@ -447,26 +449,26 @@ function CommentRow({ comment, isReply = false, onReply, onLike }: CommentRowPro
|
|||||||
width: isReply ? 24 : 32,
|
width: isReply ? 24 : 32,
|
||||||
height: isReply ? 24 : 32,
|
height: isReply ? 24 : 32,
|
||||||
borderRadius: isReply ? 12 : 16,
|
borderRadius: isReply ? 12 : 16,
|
||||||
backgroundColor: '#e5e5e5',
|
backgroundColor: colors.surfaceElevated,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontSize: isReply ? 9 : 11, fontFamily: 'Nunito_700Bold', color: '#737373' }}>
|
<Text style={{ fontSize: isReply ? 9 : 11, fontFamily: 'Nunito_700Bold', color: colors.textMuted }}>
|
||||||
{(comment.authorNickname ?? 'AN').slice(0, 2).toUpperCase()}
|
{(comment.authorNickname ?? 'AN').slice(0, 2).toUpperCase()}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={{ flex: 1, minWidth: 0 }}>
|
<View style={{ flex: 1, minWidth: 0 }}>
|
||||||
<Text style={{ fontSize: 12, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
|
<Text style={{ fontSize: 12, fontFamily: 'Nunito_700Bold', color: colors.text }}>
|
||||||
{comment.authorNickname ?? t('community.anonymous_label')}
|
{comment.authorNickname ?? t('community.anonymous_label')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#404040',
|
color: colors.textMuted,
|
||||||
lineHeight: 20,
|
lineHeight: 20,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
}}
|
}}
|
||||||
@ -474,12 +476,12 @@ function CommentRow({ comment, isReply = false, onReply, onLike }: CommentRowPro
|
|||||||
{comment.content}
|
{comment.content}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 16, marginTop: 6 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 16, marginTop: 6 }}>
|
||||||
<Text style={{ fontSize: 10, color: '#a3a3a3', fontFamily: 'Nunito_400Regular' }}>
|
<Text style={{ fontSize: 10, color: colors.textMuted, fontFamily: 'Nunito_400Regular' }}>
|
||||||
{formatRelativeTime(comment.createdAt)}
|
{formatRelativeTime(comment.createdAt)}
|
||||||
</Text>
|
</Text>
|
||||||
{!isReply && onReply && (
|
{!isReply && onReply && (
|
||||||
<Pressable onPress={onReply}>
|
<Pressable onPress={onReply}>
|
||||||
<Text style={{ fontSize: 11, color: '#a3a3a3', fontFamily: 'Nunito_600SemiBold' }}>
|
<Text style={{ fontSize: 11, color: colors.textMuted, fontFamily: 'Nunito_600SemiBold' }}>
|
||||||
{t('community.reply')}
|
{t('community.reply')}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { View, Text } from 'react-native';
|
import { View, Text } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { colors } from '../lib/theme';
|
import { useColors } from '../lib/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
days: number;
|
days: number;
|
||||||
@ -16,14 +16,18 @@ const sizeMap = {
|
|||||||
|
|
||||||
export function StreakBadge({ days, size = 'md' }: Props) {
|
export function StreakBadge({ days, size = 'md' }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const s = sizeMap[size];
|
const s = sizeMap[size];
|
||||||
return (
|
return (
|
||||||
<View className={`items-center bg-white border border-neutral-200 rounded-3xl ${s.padding}`}>
|
<View
|
||||||
|
className={`items-center rounded-3xl ${s.padding}`}
|
||||||
|
style={{ backgroundColor: colors.bg, borderWidth: 1, borderColor: colors.border }}
|
||||||
|
>
|
||||||
<View className="flex-row items-center gap-2 mb-1">
|
<View className="flex-row items-center gap-2 mb-1">
|
||||||
<Ionicons name="flame" size={s.icon} color={colors.brandOrange} />
|
<Ionicons name="flame" size={s.icon} color={colors.brandOrange} />
|
||||||
<Text className={`${s.number} text-neutral-900 tabular-nums`} style={{ fontFamily: 'Nunito_800ExtraBold' }}>{days}</Text>
|
<Text className={`${s.number} tabular-nums`} style={{ fontFamily: 'Nunito_800ExtraBold', color: colors.text }}>{days}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text className={`${s.label} text-neutral-500 tracking-wide uppercase`} style={{ fontFamily: 'Nunito_600SemiBold' }}>
|
<Text className={`${s.label} tracking-wide uppercase`} style={{ fontFamily: 'Nunito_600SemiBold', color: colors.textMuted }}>
|
||||||
{days === 1 ? t('streak.label_one') : t('streak.label_other')} {t('streak.label_suffix')}
|
{days === 1 ? t('streak.label_one') : t('streak.label_other')} {t('streak.label_suffix')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Modal, View, Text, Pressable } from 'react-native';
|
import { Modal, View, Text, Pressable } from 'react-native';
|
||||||
import { Picker } from '@react-native-picker/picker';
|
import { Picker } from '@react-native-picker/picker';
|
||||||
import { colors } from '../lib/theme';
|
import { useColors } from '../lib/theme';
|
||||||
|
|
||||||
type Option<T> = { value: T; label: string };
|
type Option<T> = { value: T; label: string };
|
||||||
|
|
||||||
@ -35,6 +35,7 @@ export function WheelPickerModal<T extends string | number>({
|
|||||||
onSelect,
|
onSelect,
|
||||||
onClose,
|
onClose,
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
|
const colors = useColors();
|
||||||
// Tracks the wheel's current selection (separate from confirmed value).
|
// Tracks the wheel's current selection (separate from confirmed value).
|
||||||
// Initialized from `value` prop on each open.
|
// Initialized from `value` prop on each open.
|
||||||
const [tempValue, setTempValue] = useState<T | null>(value);
|
const [tempValue, setTempValue] = useState<T | null>(value);
|
||||||
@ -72,7 +73,7 @@ export function WheelPickerModal<T extends string | number>({
|
|||||||
<Pressable onPress={() => {}}>
|
<Pressable onPress={() => {}}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: colors.bg,
|
||||||
borderTopLeftRadius: 18,
|
borderTopLeftRadius: 18,
|
||||||
borderTopRightRadius: 18,
|
borderTopRightRadius: 18,
|
||||||
paddingBottom: 24,
|
paddingBottom: 24,
|
||||||
@ -87,7 +88,7 @@ export function WheelPickerModal<T extends string | number>({
|
|||||||
paddingHorizontal: 16,
|
paddingHorizontal: 16,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#e5e5e5',
|
borderBottomColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pressable onPress={onClose} hitSlop={10}>
|
<Pressable onPress={onClose} hitSlop={10}>
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import {
|
|||||||
normalizeDomain,
|
normalizeDomain,
|
||||||
type Tier,
|
type Tier,
|
||||||
} from '../../hooks/useCustomDomains';
|
} from '../../hooks/useCustomDomains';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
||||||
const SHEET_HEIGHT = SCREEN_HEIGHT * 0.65; // wie bei PostCommentsSheet — 65% der Screen-Höhe
|
const SHEET_HEIGHT = SCREEN_HEIGHT * 0.65; // wie bei PostCommentsSheet — 65% der Screen-Höhe
|
||||||
@ -34,6 +35,7 @@ type Props = {
|
|||||||
|
|
||||||
export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [confirmPermanent, setConfirmPermanent] = useState(false);
|
const [confirmPermanent, setConfirmPermanent] = useState(false);
|
||||||
@ -122,7 +124,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
height: SHEET_HEIGHT,
|
height: SHEET_HEIGHT,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
borderTopLeftRadius: 20,
|
borderTopLeftRadius: 20,
|
||||||
borderTopRightRadius: 20,
|
borderTopRightRadius: 20,
|
||||||
transform: [{ translateY }],
|
transform: [{ translateY }],
|
||||||
@ -134,7 +136,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
>
|
>
|
||||||
{/* Drag-handle */}
|
{/* Drag-handle */}
|
||||||
<View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 4 }}>
|
<View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 4 }}>
|
||||||
<View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: '#d4d4d4' }} />
|
<View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: colors.border }} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -147,15 +149,15 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
paddingTop: 6,
|
paddingTop: 6,
|
||||||
paddingBottom: 12,
|
paddingBottom: 12,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#f0f0f0',
|
borderBottomColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pressable onPress={close} hitSlop={10}>
|
<Pressable onPress={close} hitSlop={10}>
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: '#525252' }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
|
||||||
{t('blocker.add_sheet_title')}
|
{t('blocker.add_sheet_title')}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ width: 60 }} />
|
<View style={{ width: 60 }} />
|
||||||
@ -168,7 +170,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
color: '#525252',
|
color: colors.textMuted,
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -181,7 +183,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
setError(null);
|
setError(null);
|
||||||
}}
|
}}
|
||||||
placeholder={t('blocker.add_sheet_placeholder')}
|
placeholder={t('blocker.add_sheet_placeholder')}
|
||||||
placeholderTextColor="#a3a3a3"
|
placeholderTextColor={colors.textMuted}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
autoFocus
|
autoFocus
|
||||||
@ -189,13 +191,13 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
onSubmitEditing={handleAdd}
|
onSubmitEditing={handleAdd}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: colors.surfaceElevated,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#0a0a0a',
|
color: colors.text,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{input && !valid && (
|
{input && !valid && (
|
||||||
@ -220,7 +222,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 10,
|
gap: 10,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: colors.surfaceElevated,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -235,7 +237,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
color: '#0a0a0a',
|
color: colors.text,
|
||||||
}}
|
}}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
@ -289,8 +291,8 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
height: 22,
|
height: 22,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
borderColor: confirmPermanent ? '#16a34a' : '#d4d4d4',
|
borderColor: confirmPermanent ? colors.success : colors.border,
|
||||||
backgroundColor: confirmPermanent ? '#16a34a' : '#fff',
|
backgroundColor: confirmPermanent ? colors.success : colors.bg,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
marginTop: 1,
|
marginTop: 1,
|
||||||
@ -303,7 +305,7 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#0a0a0a',
|
color: colors.text,
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { View, Text, Pressable, ActivityIndicator } from 'react-native';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
remainingFormatted: string; // "23:59:42"
|
remainingFormatted: string; // "23:59:42"
|
||||||
@ -10,6 +11,7 @@ type Props = {
|
|||||||
|
|
||||||
export function CooldownBanner({ remainingFormatted, onCancel }: Props) {
|
export function CooldownBanner({ remainingFormatted, onCancel }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const [cancelling, setCancelling] = useState(false);
|
const [cancelling, setCancelling] = useState(false);
|
||||||
|
|
||||||
async function handleCancel() {
|
async function handleCancel() {
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { Modal, View, Text, Pressable, ScrollView, ActionSheetIOS, Platform, Ale
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -26,6 +27,7 @@ export function DeactivationExplainerSheet({
|
|||||||
onStartCooldown,
|
onStartCooldown,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
function showFinalConfirm() {
|
function showFinalConfirm() {
|
||||||
@ -74,7 +76,7 @@ export function DeactivationExplainerSheet({
|
|||||||
presentationStyle="pageSheet"
|
presentationStyle="pageSheet"
|
||||||
onRequestClose={onClose}
|
onRequestClose={onClose}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1, backgroundColor: '#fff' }}>
|
<View style={{ flex: 1, backgroundColor: colors.bg }}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@ -85,29 +87,29 @@ export function DeactivationExplainerSheet({
|
|||||||
paddingTop: 14,
|
paddingTop: 14,
|
||||||
paddingBottom: 12,
|
paddingBottom: 12,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#f0f0f0',
|
borderBottomColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pressable onPress={onClose} hitSlop={10}>
|
<Pressable onPress={onClose} hitSlop={10}>
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: '#525252' }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
|
||||||
{t('common.back')}
|
{t('common.back')}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
|
||||||
{t('blocker.deactivation_heading')}
|
{t('blocker.deactivation_heading')}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ width: 50 }} />
|
<View style={{ width: 50 }} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<ScrollView contentContainerStyle={{ padding: 20, gap: 18 }}>
|
<ScrollView contentContainerStyle={{ padding: 20, gap: 18 }}>
|
||||||
<Text style={{ fontSize: 22, fontFamily: 'Nunito_800ExtraBold', color: '#0a0a0a' }}>
|
<Text style={{ fontSize: 22, fontFamily: 'Nunito_800ExtraBold', color: colors.text }}>
|
||||||
{t('blocker.deactivation_title')}
|
{t('blocker.deactivation_title')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#404040',
|
color: colors.textMuted,
|
||||||
lineHeight: 22,
|
lineHeight: 22,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -195,6 +197,7 @@ function BulletRow({
|
|||||||
title: string;
|
title: string;
|
||||||
text: string;
|
text: string;
|
||||||
}) {
|
}) {
|
||||||
|
const colors = useColors();
|
||||||
return (
|
return (
|
||||||
<View style={{ flexDirection: 'row', gap: 12 }}>
|
<View style={{ flexDirection: 'row', gap: 12 }}>
|
||||||
<View
|
<View
|
||||||
@ -202,22 +205,22 @@ function BulletRow({
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: colors.surfaceElevated,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name={icon} size={18} color="#525252" />
|
<Ionicons name={icon} size={18} color={colors.textMuted} />
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
|
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text }}>
|
||||||
{title}
|
{title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#525252',
|
color: colors.textMuted,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
lineHeight: 17,
|
lineHeight: 17,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { SuccessAlert } from '../SuccessAlert';
|
import { SuccessAlert } from '../SuccessAlert';
|
||||||
import { ConfirmAlert } from '../ConfirmAlert';
|
import { ConfirmAlert } from '../ConfirmAlert';
|
||||||
import type { CustomDomain, Tier } from '../../hooks/useCustomDomains';
|
import type { CustomDomain, Tier } from '../../hooks/useCustomDomains';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
// ─── Helpers ─────────────────────────────────────────────────────────────
|
// ─── Helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -65,6 +66,7 @@ const STATUS_PRIORITY: Record<string, number> = {
|
|||||||
|
|
||||||
export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Props) {
|
export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
// Slot-relevante Domains (alles außer approved). Sortiert nach Status-Priority,
|
// Slot-relevante Domains (alles außer approved). Sortiert nach Status-Priority,
|
||||||
// innerhalb gleicher Priority dann newest-first by addedAt.
|
// innerhalb gleicher Priority dann newest-first by addedAt.
|
||||||
const visible = useMemo(() => {
|
const visible = useMemo(() => {
|
||||||
@ -85,7 +87,7 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
|
|||||||
<View style={{ gap: 12 }}>
|
<View style={{ gap: 12 }}>
|
||||||
{/* Header: Section-Title + Slot-Counter + Add-Button (inline, neben SlotPill) */}
|
{/* Header: Section-Title + Slot-Counter + Add-Button (inline, neben SlotPill) */}
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
|
<Text style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text }}>
|
||||||
{t('blocker.domain_section_title')}
|
{t('blocker.domain_section_title')}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||||
@ -122,7 +124,7 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
|
|||||||
const pct = (tier.usedSlots / tier.domainLimit) * 100;
|
const pct = (tier.usedSlots / tier.domainLimit) * 100;
|
||||||
const barColor = pct >= 90 ? '#dc2626' : pct >= 60 ? '#f59e0b' : '#16a34a';
|
const barColor = pct >= 90 ? '#dc2626' : pct >= 60 ? '#f59e0b' : '#16a34a';
|
||||||
return (
|
return (
|
||||||
<View style={{ height: 4, borderRadius: 2, backgroundColor: '#f0f0f0', overflow: 'hidden' }}>
|
<View style={{ height: 4, borderRadius: 2, backgroundColor: colors.surfaceElevated, overflow: 'hidden' }}>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
height: '100%',
|
height: '100%',
|
||||||
@ -174,16 +176,16 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
|
|||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderStyle: 'dashed',
|
borderStyle: 'dashed',
|
||||||
borderColor: '#d4d4d4',
|
borderColor: colors.border,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name="globe-outline" size={28} color="#a3a3a3" />
|
<Ionicons name="globe-outline" size={28} color={colors.textMuted} />
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#737373',
|
color: colors.textMuted,
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}
|
}}
|
||||||
@ -205,8 +207,9 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
|
|||||||
// ─── SlotPill ─────────────────────────────────────────────────────────────
|
// ─── SlotPill ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function SlotPill({ tier }: { tier: Tier }) {
|
function SlotPill({ tier }: { tier: Tier }) {
|
||||||
const bg = tier.atLimit ? '#fee2e2' : '#f5f5f5';
|
const colors = useColors();
|
||||||
const fg = tier.atLimit ? '#dc2626' : '#525252';
|
const bg = tier.atLimit ? '#fee2e2' : colors.surfaceElevated;
|
||||||
|
const fg = tier.atLimit ? '#dc2626' : colors.textMuted;
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@ -258,6 +261,7 @@ function DomainTile({
|
|||||||
onSubmit?: (id: string) => Promise<{ ok: boolean }>;
|
onSubmit?: (id: string) => Promise<{ ok: boolean }>;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [imgError, setImgError] = useState(false);
|
const [imgError, setImgError] = useState(false);
|
||||||
const [successVisible, setSuccessVisible] = useState(false);
|
const [successVisible, setSuccessVisible] = useState(false);
|
||||||
@ -346,9 +350,9 @@ function DomainTile({
|
|||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e5e5',
|
borderColor: colors.border,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
padding: 8,
|
padding: 8,
|
||||||
// KEIN aspectRatio:1 mehr — der hat den Button auf 0 Höhe gepresst.
|
// KEIN aspectRatio:1 mehr — der hat den Button auf 0 Höhe gepresst.
|
||||||
@ -417,7 +421,7 @@ function DomainTile({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
color: '#0a0a0a',
|
color: colors.text,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { View, Text, Switch, Pressable, ActivityIndicator } from 'react-native';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { ProtectionState } from '../../lib/protection';
|
import type { ProtectionState } from '../../lib/protection';
|
||||||
import { colors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
state: ProtectionState;
|
state: ProtectionState;
|
||||||
@ -15,6 +15,7 @@ type Props = {
|
|||||||
|
|
||||||
export function ProtectionCard({ state, loading, onActivate, onPressSettings }: Props) {
|
export function ProtectionCard({ state, loading, onActivate, onPressSettings }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const isActive = state.phase === 'active' || state.phase === 'cooldownActive';
|
const isActive = state.phase === 'active' || state.phase === 'cooldownActive';
|
||||||
const isCooldown = state.phase === 'cooldownActive';
|
const isCooldown = state.phase === 'cooldownActive';
|
||||||
|
|
||||||
@ -30,8 +31,8 @@ export function ProtectionCard({ state, loading, onActivate, onPressSettings }:
|
|||||||
return t('blocker.protection_subtitle_pro');
|
return t('blocker.protection_subtitle_pro');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const cardBg = isCooldown ? '#fef3c7' : isActive ? '#dcfce7' : '#ffffff';
|
const cardBg = isCooldown ? '#fef3c7' : isActive ? '#dcfce7' : colors.bg;
|
||||||
const cardBorder = isCooldown ? '#fcd34d' : isActive ? '#86efac' : '#e5e5e5';
|
const cardBorder = isCooldown ? '#fcd34d' : isActive ? '#86efac' : colors.border;
|
||||||
const iconBg = isCooldown ? '#fde68a' : isActive ? '#bbf7d0' : '#f5f5f5';
|
const iconBg = isCooldown ? '#fde68a' : isActive ? '#bbf7d0' : '#f5f5f5';
|
||||||
const iconColor = isCooldown ? '#d97706' : isActive ? '#16a34a' : '#a3a3a3';
|
const iconColor = isCooldown ? '#d97706' : isActive ? '#16a34a' : '#a3a3a3';
|
||||||
|
|
||||||
@ -67,7 +68,7 @@ export function ProtectionCard({ state, loading, onActivate, onPressSettings }:
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontFamily: 'Nunito_700Bold',
|
||||||
color: '#0a0a0a',
|
color: colors.text,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('blocker.protection_card_title')}
|
{t('blocker.protection_card_title')}
|
||||||
@ -76,7 +77,7 @@ export function ProtectionCard({ state, loading, onActivate, onPressSettings }:
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#525252',
|
color: colors.textMuted,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -100,7 +101,7 @@ export function ProtectionCard({ state, loading, onActivate, onPressSettings }:
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: colors.surface,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
@ -108,7 +109,7 @@ export function ProtectionCard({ state, loading, onActivate, onPressSettings }:
|
|||||||
shadowOpacity: 0.05,
|
shadowOpacity: 0.05,
|
||||||
shadowRadius: 2,
|
shadowRadius: 2,
|
||||||
}}>
|
}}>
|
||||||
<Ionicons name="settings-outline" size={18} color="#525252" />
|
<Ionicons name="settings-outline" size={18} color={colors.textMuted} />
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : (
|
) : (
|
||||||
@ -146,18 +147,19 @@ export function ProtectionCard({ state, loading, onActivate, onPressSettings }:
|
|||||||
function Stat({
|
function Stat({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
valueColor = '#0a0a0a',
|
valueColor,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
valueColor?: string;
|
valueColor?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const colors = useColors();
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, alignItems: 'center' }}>
|
<View style={{ flex: 1, alignItems: 'center' }}>
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_800ExtraBold', color: valueColor }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_800ExtraBold', color: valueColor ?? colors.text }}>
|
||||||
{value}
|
{value}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: '#737373' }}>
|
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import Svg, { Path, Circle } from 'react-native-svg';
|
import Svg, { Path, Circle } from 'react-native-svg';
|
||||||
import type { ProtectionState } from '../../lib/protection';
|
import type { ProtectionState } from '../../lib/protection';
|
||||||
import { apiFetch } from '../../lib/api';
|
import { apiFetch } from '../../lib/api';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -55,6 +56,7 @@ export function ProtectionDetailsSheet({
|
|||||||
onRequestDeactivation,
|
onRequestDeactivation,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const localeTag = i18n.language === 'de' ? 'de-DE' : 'en-US';
|
const localeTag = i18n.language === 'de' ? 'de-DE' : 'en-US';
|
||||||
|
|
||||||
const sheetHeight = useRef(new Animated.Value(DEFAULT_HEIGHT)).current;
|
const sheetHeight = useRef(new Animated.Value(DEFAULT_HEIGHT)).current;
|
||||||
@ -162,7 +164,7 @@ export function ProtectionDetailsSheet({
|
|||||||
<Animated.View
|
<Animated.View
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
borderTopLeftRadius: 24,
|
borderTopLeftRadius: 24,
|
||||||
borderTopRightRadius: 24,
|
borderTopRightRadius: 24,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@ -178,7 +180,7 @@ export function ProtectionDetailsSheet({
|
|||||||
{...panResponder.panHandlers}
|
{...panResponder.panHandlers}
|
||||||
style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 6 }}
|
style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 6 }}
|
||||||
>
|
>
|
||||||
<View style={{ width: 36, height: 5, borderRadius: 3, backgroundColor: '#d4d4d8' }} />
|
<View style={{ width: 36, height: 5, borderRadius: 3, backgroundColor: colors.border }} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -192,15 +194,15 @@ export function ProtectionDetailsSheet({
|
|||||||
paddingTop: 4,
|
paddingTop: 4,
|
||||||
paddingBottom: 12,
|
paddingBottom: 12,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#f0f0f0',
|
borderBottomColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{ width: 50 }} />
|
<View style={{ width: 50 }} />
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
|
||||||
{t('blocker.details_title')}
|
{t('blocker.details_title')}
|
||||||
</Text>
|
</Text>
|
||||||
<Pressable onPress={handleClose} hitSlop={10} style={{ width: 50, alignItems: 'flex-end' }}>
|
<Pressable onPress={handleClose} hitSlop={10} style={{ width: 50, alignItems: 'flex-end' }}>
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: '#525252' }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
|
||||||
{t('blocker.details_done')}
|
{t('blocker.details_done')}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
@ -249,16 +251,16 @@ export function ProtectionDetailsSheet({
|
|||||||
padding: 18,
|
padding: 18,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e5e5',
|
borderColor: colors.border,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
gap: 8,
|
gap: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View>
|
<View>
|
||||||
<Text style={{ fontSize: 13, color: '#0a0a0a', fontFamily: 'Nunito_700Bold' }}>
|
<Text style={{ fontSize: 13, color: colors.text, fontFamily: 'Nunito_700Bold' }}>
|
||||||
{t('blocker.kpi_submissions_title')}
|
{t('blocker.kpi_submissions_title')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 11, color: '#737373', fontFamily: 'Nunito_400Regular', marginTop: 2 }}>
|
<Text style={{ fontSize: 11, color: colors.textMuted, fontFamily: 'Nunito_400Regular', marginTop: 2 }}>
|
||||||
{t('blocker.kpi_submissions_subtitle')}
|
{t('blocker.kpi_submissions_subtitle')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@ -323,14 +325,14 @@ export function ProtectionDetailsSheet({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontFamily: 'Nunito_700Bold',
|
||||||
color: '#737373',
|
color: colors.textMuted,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('blocker.faq_heading')}
|
{t('blocker.faq_heading')}
|
||||||
</Text>
|
</Text>
|
||||||
<Ionicons name="help-circle-outline" size={18} color="#737373" />
|
<Ionicons name="help-circle-outline" size={18} color={colors.textMuted} />
|
||||||
</View>
|
</View>
|
||||||
{[1, 2, 3, 4].map((n) => (
|
{[1, 2, 3, 4].map((n) => (
|
||||||
<FaqItem
|
<FaqItem
|
||||||
@ -355,7 +357,7 @@ export function ProtectionDetailsSheet({
|
|||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
borderWidth: 1.5,
|
borderWidth: 1.5,
|
||||||
borderColor: HERO_COLOR,
|
borderColor: HERO_COLOR,
|
||||||
backgroundColor: '#fff7ed',
|
backgroundColor: colors.surface,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
@ -479,6 +481,7 @@ function KpiCard({
|
|||||||
decimals?: number;
|
decimals?: number;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
}) {
|
}) {
|
||||||
|
const colors = useColors();
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@ -486,14 +489,14 @@ function KpiCard({
|
|||||||
padding: 12,
|
padding: 12,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e5e5',
|
borderColor: colors.border,
|
||||||
backgroundColor: '#fafafa',
|
backgroundColor: colors.surface,
|
||||||
gap: 6,
|
gap: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
||||||
<Ionicons name={icon} size={14} color="#737373" />
|
<Ionicons name={icon} size={14} color={colors.textMuted} />
|
||||||
<Text style={{ flex: 1, fontSize: 10, color: '#737373', fontFamily: 'Nunito_400Regular', lineHeight: 13 }}>
|
<Text style={{ flex: 1, fontSize: 10, color: colors.textMuted, fontFamily: 'Nunito_400Regular', lineHeight: 13 }}>
|
||||||
{label}
|
{label}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@ -502,10 +505,10 @@ function KpiCard({
|
|||||||
value={value}
|
value={value}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
decimals={decimals}
|
decimals={decimals}
|
||||||
style={{ fontSize: 18, fontFamily: 'Nunito_900Black', color: '#0a0a0a', letterSpacing: -0.3 }}
|
style={{ fontSize: 18, fontFamily: 'Nunito_900Black', color: colors.text, letterSpacing: -0.3 }}
|
||||||
/>
|
/>
|
||||||
{suffix ? (
|
{suffix ? (
|
||||||
<Text style={{ fontSize: 10, color: '#737373', fontFamily: 'Nunito_700Bold' }}>{suffix}</Text>
|
<Text style={{ fontSize: 10, color: colors.textMuted, fontFamily: 'Nunito_700Bold' }}>{suffix}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -522,13 +525,14 @@ function LegendItem({
|
|||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
}) {
|
}) {
|
||||||
|
const colors = useColors();
|
||||||
return (
|
return (
|
||||||
<View style={{ alignItems: 'center', gap: 4 }}>
|
<View style={{ alignItems: 'center', gap: 4 }}>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 5 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 5 }}>
|
||||||
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: color }} />
|
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: color }} />
|
||||||
<Text style={{ fontSize: 11, color: '#525252', fontFamily: 'Nunito_700Bold' }}>{value}</Text>
|
<Text style={{ fontSize: 11, color: colors.textMuted, fontFamily: 'Nunito_700Bold' }}>{value}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={{ fontSize: 10, color: '#737373', fontFamily: 'Nunito_400Regular' }}>{label}</Text>
|
<Text style={{ fontSize: 10, color: colors.textMuted, fontFamily: 'Nunito_400Regular' }}>{label}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -543,6 +547,7 @@ function HalfDonut({
|
|||||||
centerValue: number;
|
centerValue: number;
|
||||||
centerLabel: string;
|
centerLabel: string;
|
||||||
}) {
|
}) {
|
||||||
|
const colors = useColors();
|
||||||
const total = Math.max(1, segments.reduce((s, x) => s + x.value, 0));
|
const total = Math.max(1, segments.reduce((s, x) => s + x.value, 0));
|
||||||
|
|
||||||
const W = 220;
|
const W = 220;
|
||||||
@ -582,7 +587,7 @@ function HalfDonut({
|
|||||||
{/* Background track */}
|
{/* Background track */}
|
||||||
<Path
|
<Path
|
||||||
d={arcPath(cx, cy, r, 180, 360)}
|
d={arcPath(cx, cy, r, 180, 360)}
|
||||||
stroke="#f0f0f0"
|
stroke={colors.surfaceElevated}
|
||||||
strokeWidth={stroke}
|
strokeWidth={stroke}
|
||||||
fill="none"
|
fill="none"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
@ -618,10 +623,10 @@ function HalfDonut({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontSize: 30, fontFamily: 'Nunito_900Black', color: '#0a0a0a', letterSpacing: -0.5 }}>
|
<Text style={{ fontSize: 30, fontFamily: 'Nunito_900Black', color: colors.text, letterSpacing: -0.5 }}>
|
||||||
{centerValue}
|
{centerValue}
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={{ fontSize: 10, color: '#737373', fontFamily: 'Nunito_400Regular', marginTop: -2 }}>
|
<Text style={{ fontSize: 10, color: colors.textMuted, fontFamily: 'Nunito_400Regular', marginTop: -2 }}>
|
||||||
{centerLabel}
|
{centerLabel}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@ -643,6 +648,7 @@ function polar(cx: number, cy: number, r: number, angleDeg: number) {
|
|||||||
|
|
||||||
// ─── FAQ Item (chevron AT END of header row, on right) ─────────────────────
|
// ─── FAQ Item (chevron AT END of header row, on right) ─────────────────────
|
||||||
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
function FaqItem({ question, answer }: { question: string; answer: string }) {
|
||||||
|
const colors = useColors();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const rotateAnim = useRef(new Animated.Value(0)).current;
|
const rotateAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
@ -664,10 +670,10 @@ function FaqItem({ question, answer }: { question: string; answer: string }) {
|
|||||||
style={{
|
style={{
|
||||||
alignSelf: 'stretch',
|
alignSelf: 'stretch',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e5e5',
|
borderColor: colors.border,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pressable
|
<Pressable
|
||||||
@ -678,7 +684,7 @@ function FaqItem({ question, answer }: { question: string; answer: string }) {
|
|||||||
>
|
>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 14 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 14 }}>
|
||||||
<View style={{ flex: 1, paddingRight: 12 }}>
|
<View style={{ flex: 1, paddingRight: 12 }}>
|
||||||
<Text style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: '#0a0a0a', lineHeight: 18 }}>
|
<Text style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: colors.text, lineHeight: 18 }}>
|
||||||
{question}
|
{question}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
@ -687,19 +693,19 @@ function FaqItem({ question, answer }: { question: string; answer: string }) {
|
|||||||
width: 28,
|
width: 28,
|
||||||
height: 28,
|
height: 28,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: colors.surfaceElevated,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
transform: [{ rotate }],
|
transform: [{ rotate }],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name="chevron-down" size={16} color="#525252" />
|
<Ionicons name="chevron-down" size={16} color={colors.textMuted} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{open && (
|
{open && (
|
||||||
<View style={{ paddingHorizontal: 14, paddingBottom: 14, paddingTop: 0 }}>
|
<View style={{ paddingHorizontal: 14, paddingBottom: 14, paddingTop: 0 }}>
|
||||||
<Text style={{ fontSize: 13, fontFamily: 'Nunito_400Regular', color: '#525252', lineHeight: 19 }}>
|
<Text style={{ fontSize: 13, fontFamily: 'Nunito_400Regular', color: colors.textMuted, lineHeight: 19 }}>
|
||||||
{answer}
|
{answer}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { View, Text, Pressable } from 'react-native';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { ProtectionState } from '../../lib/protection';
|
import type { ProtectionState } from '../../lib/protection';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
state: ProtectionState;
|
state: ProtectionState;
|
||||||
@ -16,6 +17,7 @@ type Props = {
|
|||||||
*/
|
*/
|
||||||
export function ProtectionLockedCard({ state, onPressSettings }: Props) {
|
export function ProtectionLockedCard({ state, onPressSettings }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const isCooldown = state.phase === 'cooldownActive';
|
const isCooldown = state.phase === 'cooldownActive';
|
||||||
const cardBg = isCooldown ? '#fef3c7' : '#dcfce7';
|
const cardBg = isCooldown ? '#fef3c7' : '#dcfce7';
|
||||||
const cardBorder = isCooldown ? '#fcd34d' : '#86efac';
|
const cardBorder = isCooldown ? '#fcd34d' : '#86efac';
|
||||||
@ -57,14 +59,14 @@ export function ProtectionLockedCard({ state, onPressSettings }: Props) {
|
|||||||
<Ionicons name="shield-checkmark" size={22} color={iconColor} />
|
<Ionicons name="shield-checkmark" size={22} color={iconColor} />
|
||||||
</View>
|
</View>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text style={{ fontSize: 15, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
|
<Text style={{ fontSize: 15, fontFamily: 'Nunito_700Bold', color: colors.text }}>
|
||||||
{t('blocker.protection_card_locked_title')}
|
{t('blocker.protection_card_locked_title')}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#525252',
|
color: colors.textMuted,
|
||||||
marginTop: 2,
|
marginTop: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -84,7 +86,7 @@ export function ProtectionLockedCard({ state, onPressSettings }: Props) {
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: colors.surface,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
shadowColor: '#000',
|
shadowColor: '#000',
|
||||||
@ -92,7 +94,7 @@ export function ProtectionLockedCard({ state, onPressSettings }: Props) {
|
|||||||
shadowOpacity: 0.05,
|
shadowOpacity: 0.05,
|
||||||
shadowRadius: 2,
|
shadowRadius: 2,
|
||||||
}}>
|
}}>
|
||||||
<Ionicons name="settings-outline" size={18} color="#525252" />
|
<Ionicons name="settings-outline" size={18} color={colors.textMuted} />
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
@ -118,11 +120,12 @@ export function ProtectionLockedCard({ state, onPressSettings }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Stat({ label, value, valueColor = '#0a0a0a' }: { label: string; value: string; valueColor?: string }) {
|
function Stat({ label, value, valueColor }: { label: string; value: string; valueColor?: string }) {
|
||||||
|
const colors = useColors();
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, alignItems: 'center' }}>
|
<View style={{ flex: 1, alignItems: 'center' }}>
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_800ExtraBold', color: valueColor }}>{value}</Text>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_800ExtraBold', color: valueColor ?? colors.text }}>{value}</Text>
|
||||||
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: '#737373' }}>{label}</Text>
|
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>{label}</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import * as Clipboard from 'expo-clipboard';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { resolveAvatar } from '../../lib/resolveAvatar';
|
import { resolveAvatar } from '../../lib/resolveAvatar';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
export type ChatMsg = {
|
export type ChatMsg = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -63,6 +64,8 @@ export function ChatBubble({
|
|||||||
onOpenImage,
|
onOpenImage,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
|
const styles = makeStyles(colors);
|
||||||
const [actionsOpen, setActionsOpen] = useState(false);
|
const [actionsOpen, setActionsOpen] = useState(false);
|
||||||
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
const longPressTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
@ -323,120 +326,122 @@ export function ChatBubble({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function makeStyles(colors: ReturnType<typeof useColors>) {
|
||||||
row: {
|
return StyleSheet.create({
|
||||||
flexDirection: 'row',
|
row: {
|
||||||
paddingHorizontal: 8,
|
flexDirection: 'row',
|
||||||
},
|
paddingHorizontal: 8,
|
||||||
avatarSlot: {
|
},
|
||||||
width: 30,
|
avatarSlot: {
|
||||||
marginRight: 4,
|
width: 30,
|
||||||
justifyContent: 'flex-end',
|
marginRight: 4,
|
||||||
},
|
justifyContent: 'flex-end',
|
||||||
avatar: {
|
},
|
||||||
width: 26,
|
avatar: {
|
||||||
height: 26,
|
width: 26,
|
||||||
borderRadius: 13,
|
height: 26,
|
||||||
backgroundColor: '#e5e5e5',
|
borderRadius: 13,
|
||||||
},
|
backgroundColor: colors.surfaceElevated,
|
||||||
bubbleCol: {
|
},
|
||||||
maxWidth: '78%',
|
bubbleCol: {
|
||||||
},
|
maxWidth: '78%',
|
||||||
nickname: {
|
},
|
||||||
fontSize: 10,
|
nickname: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 10,
|
||||||
color: '#007AFF',
|
fontFamily: 'Nunito_700Bold',
|
||||||
marginBottom: 2,
|
color: '#007AFF',
|
||||||
marginLeft: 10,
|
marginBottom: 2,
|
||||||
},
|
marginLeft: 10,
|
||||||
bubble: {
|
},
|
||||||
borderRadius: 18,
|
bubble: {
|
||||||
paddingHorizontal: 12,
|
borderRadius: 18,
|
||||||
paddingVertical: 6,
|
paddingHorizontal: 12,
|
||||||
shadowColor: '#000',
|
paddingVertical: 6,
|
||||||
shadowOpacity: 0.05,
|
shadowColor: '#000',
|
||||||
shadowRadius: 1,
|
shadowOpacity: 0.05,
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowRadius: 1,
|
||||||
},
|
shadowOffset: { width: 0, height: 1 },
|
||||||
bubbleOwn: {
|
},
|
||||||
backgroundColor: '#007AFF',
|
bubbleOwn: {
|
||||||
},
|
backgroundColor: '#007AFF',
|
||||||
bubbleOther: {
|
},
|
||||||
backgroundColor: '#ffffff',
|
bubbleOther: {
|
||||||
borderWidth: StyleSheet.hairlineWidth,
|
backgroundColor: colors.surface,
|
||||||
borderColor: '#e5e5e5',
|
borderWidth: StyleSheet.hairlineWidth,
|
||||||
},
|
borderColor: colors.border,
|
||||||
replyPreview: {
|
},
|
||||||
borderLeftWidth: 3,
|
replyPreview: {
|
||||||
borderRadius: 8,
|
borderLeftWidth: 3,
|
||||||
paddingHorizontal: 8,
|
borderRadius: 8,
|
||||||
paddingVertical: 4,
|
paddingHorizontal: 8,
|
||||||
marginBottom: 4,
|
paddingVertical: 4,
|
||||||
},
|
marginBottom: 4,
|
||||||
imageWrap: {
|
},
|
||||||
borderRadius: 12,
|
imageWrap: {
|
||||||
overflow: 'hidden',
|
borderRadius: 12,
|
||||||
position: 'relative',
|
overflow: 'hidden',
|
||||||
},
|
position: 'relative',
|
||||||
image: {
|
},
|
||||||
width: 220,
|
image: {
|
||||||
height: 220,
|
width: 220,
|
||||||
backgroundColor: '#f5f5f5',
|
height: 220,
|
||||||
},
|
backgroundColor: colors.surfaceElevated,
|
||||||
imageTimeOverlay: {
|
},
|
||||||
position: 'absolute',
|
imageTimeOverlay: {
|
||||||
bottom: 6,
|
position: 'absolute',
|
||||||
right: 6,
|
bottom: 6,
|
||||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
right: 6,
|
||||||
borderRadius: 10,
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
paddingHorizontal: 6,
|
borderRadius: 10,
|
||||||
paddingVertical: 2,
|
paddingHorizontal: 6,
|
||||||
flexDirection: 'row',
|
paddingVertical: 2,
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
},
|
alignItems: 'center',
|
||||||
content: {
|
},
|
||||||
fontSize: 14,
|
content: {
|
||||||
lineHeight: 20,
|
fontSize: 14,
|
||||||
fontFamily: 'Nunito_400Regular',
|
lineHeight: 20,
|
||||||
},
|
fontFamily: 'Nunito_400Regular',
|
||||||
footer: {
|
},
|
||||||
position: 'absolute',
|
footer: {
|
||||||
bottom: 4,
|
position: 'absolute',
|
||||||
right: 8,
|
bottom: 4,
|
||||||
flexDirection: 'row',
|
right: 8,
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
},
|
alignItems: 'center',
|
||||||
sheetBackdrop: {
|
},
|
||||||
flex: 1,
|
sheetBackdrop: {
|
||||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
flex: 1,
|
||||||
justifyContent: 'flex-end',
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
},
|
justifyContent: 'flex-end',
|
||||||
sheet: {
|
},
|
||||||
backgroundColor: '#fff',
|
sheet: {
|
||||||
borderTopLeftRadius: 20,
|
backgroundColor: colors.bg,
|
||||||
borderTopRightRadius: 20,
|
borderTopLeftRadius: 20,
|
||||||
padding: 8,
|
borderTopRightRadius: 20,
|
||||||
paddingBottom: Platform.OS === 'ios' ? 32 : 16,
|
padding: 8,
|
||||||
},
|
paddingBottom: Platform.OS === 'ios' ? 32 : 16,
|
||||||
sheetGrabber: {
|
},
|
||||||
width: 36,
|
sheetGrabber: {
|
||||||
height: 4,
|
width: 36,
|
||||||
borderRadius: 2,
|
height: 4,
|
||||||
backgroundColor: '#d4d4d4',
|
borderRadius: 2,
|
||||||
alignSelf: 'center',
|
backgroundColor: colors.border,
|
||||||
marginBottom: 10,
|
alignSelf: 'center',
|
||||||
},
|
marginBottom: 10,
|
||||||
sheetItem: {
|
},
|
||||||
flexDirection: 'row',
|
sheetItem: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 16,
|
alignItems: 'center',
|
||||||
paddingVertical: 14,
|
paddingHorizontal: 16,
|
||||||
borderRadius: 12,
|
paddingVertical: 14,
|
||||||
},
|
borderRadius: 12,
|
||||||
sheetText: {
|
},
|
||||||
fontSize: 15,
|
sheetText: {
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontSize: 15,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
marginLeft: 12,
|
color: colors.text,
|
||||||
},
|
marginLeft: 12,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import * as FileSystem from 'expo-file-system/legacy';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
type ReplyTo = { id: string; nickname: string; content: string };
|
type ReplyTo = { id: string; nickname: string; content: string };
|
||||||
|
|
||||||
@ -45,6 +46,7 @@ export function ChatInput({
|
|||||||
onCancelReply,
|
onCancelReply,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
const [attachment, setAttachment] = useState<{
|
const [attachment, setAttachment] = useState<{
|
||||||
uri: string;
|
uri: string;
|
||||||
@ -137,6 +139,8 @@ export function ChatInput({
|
|||||||
setAttachment(null);
|
setAttachment(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = makeStyles(colors);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
{/* Reply preview */}
|
{/* Reply preview */}
|
||||||
@ -231,103 +235,105 @@ function decodeBase64(base64: string): Uint8Array {
|
|||||||
return bytes;
|
return bytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function makeStyles(colors: ReturnType<typeof useColors>) {
|
||||||
container: {
|
return StyleSheet.create({
|
||||||
backgroundColor: '#ffffff',
|
container: {
|
||||||
borderTopWidth: StyleSheet.hairlineWidth,
|
backgroundColor: colors.bg,
|
||||||
borderTopColor: '#e5e5e5',
|
borderTopWidth: StyleSheet.hairlineWidth,
|
||||||
},
|
borderTopColor: colors.border,
|
||||||
replyBar: {
|
},
|
||||||
flexDirection: 'row',
|
replyBar: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 12,
|
alignItems: 'center',
|
||||||
paddingVertical: 8,
|
paddingHorizontal: 12,
|
||||||
backgroundColor: '#eff6ff',
|
paddingVertical: 8,
|
||||||
borderLeftWidth: 3,
|
backgroundColor: colors.surface,
|
||||||
borderLeftColor: '#007AFF',
|
borderLeftWidth: 3,
|
||||||
marginHorizontal: 8,
|
borderLeftColor: '#007AFF',
|
||||||
marginTop: 6,
|
marginHorizontal: 8,
|
||||||
borderRadius: 8,
|
marginTop: 6,
|
||||||
},
|
borderRadius: 8,
|
||||||
replyName: {
|
},
|
||||||
fontSize: 11,
|
replyName: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 11,
|
||||||
color: '#007AFF',
|
fontFamily: 'Nunito_700Bold',
|
||||||
},
|
color: '#007AFF',
|
||||||
replyContent: {
|
},
|
||||||
fontSize: 11,
|
replyContent: {
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontSize: 11,
|
||||||
color: '#525252',
|
fontFamily: 'Nunito_400Regular',
|
||||||
marginTop: 1,
|
color: colors.textMuted,
|
||||||
},
|
marginTop: 1,
|
||||||
attachBar: {
|
},
|
||||||
flexDirection: 'row',
|
attachBar: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 12,
|
alignItems: 'center',
|
||||||
paddingVertical: 6,
|
paddingHorizontal: 12,
|
||||||
backgroundColor: '#fafafa',
|
paddingVertical: 6,
|
||||||
marginHorizontal: 8,
|
backgroundColor: colors.surface,
|
||||||
marginTop: 6,
|
marginHorizontal: 8,
|
||||||
borderRadius: 8,
|
marginTop: 6,
|
||||||
},
|
borderRadius: 8,
|
||||||
attachImg: {
|
},
|
||||||
width: 36,
|
attachImg: {
|
||||||
height: 36,
|
width: 36,
|
||||||
borderRadius: 6,
|
height: 36,
|
||||||
marginRight: 8,
|
borderRadius: 6,
|
||||||
},
|
marginRight: 8,
|
||||||
attachFileIcon: {
|
},
|
||||||
width: 36,
|
attachFileIcon: {
|
||||||
height: 36,
|
width: 36,
|
||||||
borderRadius: 6,
|
height: 36,
|
||||||
backgroundColor: '#e5e5e5',
|
borderRadius: 6,
|
||||||
alignItems: 'center',
|
backgroundColor: colors.surfaceElevated,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
marginRight: 8,
|
justifyContent: 'center',
|
||||||
},
|
marginRight: 8,
|
||||||
attachName: {
|
},
|
||||||
flex: 1,
|
attachName: {
|
||||||
fontSize: 12,
|
flex: 1,
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontSize: 12,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
},
|
color: colors.text,
|
||||||
row: {
|
},
|
||||||
flexDirection: 'row',
|
row: {
|
||||||
alignItems: 'flex-end',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 8,
|
alignItems: 'flex-end',
|
||||||
paddingTop: 8,
|
paddingHorizontal: 8,
|
||||||
paddingBottom: 8,
|
paddingTop: 8,
|
||||||
},
|
paddingBottom: 8,
|
||||||
iconBtn: {
|
},
|
||||||
width: 36,
|
iconBtn: {
|
||||||
height: 36,
|
width: 36,
|
||||||
borderRadius: 18,
|
height: 36,
|
||||||
alignItems: 'center',
|
borderRadius: 18,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
marginRight: 4,
|
justifyContent: 'center',
|
||||||
},
|
marginRight: 4,
|
||||||
inputWrap: {
|
},
|
||||||
flex: 1,
|
inputWrap: {
|
||||||
backgroundColor: '#f5f5f5',
|
flex: 1,
|
||||||
borderRadius: 22,
|
backgroundColor: colors.surfaceElevated,
|
||||||
paddingHorizontal: 14,
|
borderRadius: 22,
|
||||||
minHeight: 36,
|
paddingHorizontal: 14,
|
||||||
maxHeight: 120,
|
minHeight: 36,
|
||||||
justifyContent: 'center',
|
maxHeight: 120,
|
||||||
},
|
justifyContent: 'center',
|
||||||
input: {
|
},
|
||||||
fontSize: 14,
|
input: {
|
||||||
lineHeight: 19,
|
fontSize: 14,
|
||||||
fontFamily: 'Nunito_400Regular',
|
lineHeight: 19,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_400Regular',
|
||||||
paddingVertical: Platform.OS === 'ios' ? 8 : 4,
|
color: colors.text,
|
||||||
},
|
paddingVertical: Platform.OS === 'ios' ? 8 : 4,
|
||||||
sendBtn: {
|
},
|
||||||
width: 36,
|
sendBtn: {
|
||||||
height: 36,
|
width: 36,
|
||||||
borderRadius: 18,
|
height: 36,
|
||||||
alignItems: 'center',
|
borderRadius: 18,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
marginLeft: 6,
|
justifyContent: 'center',
|
||||||
},
|
marginLeft: 6,
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { apiFetch } from '../../lib/api';
|
import { apiFetch } from '../../lib/api';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@ -21,6 +22,8 @@ type Props = {
|
|||||||
|
|
||||||
export function CreateRoomSheet({ visible, onClose, onCreated }: Props) {
|
export function CreateRoomSheet({ visible, onClose, onCreated }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
|
const styles = makeStyles(colors);
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [description, setDescription] = useState('');
|
const [description, setDescription] = useState('');
|
||||||
const [isPublic, setIsPublic] = useState(true);
|
const [isPublic, setIsPublic] = useState(true);
|
||||||
@ -145,138 +148,140 @@ export function CreateRoomSheet({ visible, onClose, onCreated }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function makeStyles(colors: ReturnType<typeof useColors>) {
|
||||||
backdrop: {
|
return StyleSheet.create({
|
||||||
flex: 1,
|
backdrop: {
|
||||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
flex: 1,
|
||||||
justifyContent: 'flex-end',
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||||
},
|
justifyContent: 'flex-end',
|
||||||
sheet: {
|
},
|
||||||
backgroundColor: '#fff',
|
sheet: {
|
||||||
borderTopLeftRadius: 22,
|
backgroundColor: colors.bg,
|
||||||
borderTopRightRadius: 22,
|
borderTopLeftRadius: 22,
|
||||||
padding: 18,
|
borderTopRightRadius: 22,
|
||||||
paddingBottom: Platform.OS === 'ios' ? 32 : 18,
|
padding: 18,
|
||||||
},
|
paddingBottom: Platform.OS === 'ios' ? 32 : 18,
|
||||||
grabber: {
|
},
|
||||||
width: 36,
|
grabber: {
|
||||||
height: 4,
|
width: 36,
|
||||||
borderRadius: 2,
|
height: 4,
|
||||||
backgroundColor: '#d4d4d4',
|
borderRadius: 2,
|
||||||
alignSelf: 'center',
|
backgroundColor: colors.border,
|
||||||
marginBottom: 12,
|
alignSelf: 'center',
|
||||||
},
|
marginBottom: 12,
|
||||||
title: {
|
},
|
||||||
fontSize: 17,
|
title: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 17,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_700Bold',
|
||||||
marginBottom: 14,
|
color: colors.text,
|
||||||
},
|
marginBottom: 14,
|
||||||
input: {
|
},
|
||||||
backgroundColor: '#f5f5f5',
|
input: {
|
||||||
borderRadius: 12,
|
backgroundColor: colors.surfaceElevated,
|
||||||
paddingHorizontal: 14,
|
borderRadius: 12,
|
||||||
paddingVertical: 12,
|
paddingHorizontal: 14,
|
||||||
fontSize: 14,
|
paddingVertical: 12,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontSize: 14,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_400Regular',
|
||||||
marginBottom: 10,
|
color: colors.text,
|
||||||
},
|
marginBottom: 10,
|
||||||
toggleRow: {
|
},
|
||||||
flexDirection: 'row',
|
toggleRow: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
alignItems: 'center',
|
||||||
paddingVertical: 6,
|
justifyContent: 'space-between',
|
||||||
marginTop: 4,
|
paddingVertical: 6,
|
||||||
},
|
marginTop: 4,
|
||||||
toggleLabel: {
|
},
|
||||||
fontSize: 14,
|
toggleLabel: {
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontSize: 14,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
},
|
color: colors.text,
|
||||||
toggle: {
|
},
|
||||||
width: 46,
|
toggle: {
|
||||||
height: 28,
|
width: 46,
|
||||||
borderRadius: 14,
|
height: 28,
|
||||||
backgroundColor: '#e5e5e5',
|
borderRadius: 14,
|
||||||
padding: 2,
|
backgroundColor: colors.surfaceElevated,
|
||||||
justifyContent: 'center',
|
padding: 2,
|
||||||
},
|
justifyContent: 'center',
|
||||||
toggleOn: {
|
},
|
||||||
backgroundColor: '#007AFF',
|
toggleOn: {
|
||||||
},
|
backgroundColor: '#007AFF',
|
||||||
toggleKnob: {
|
},
|
||||||
width: 24,
|
toggleKnob: {
|
||||||
height: 24,
|
width: 24,
|
||||||
borderRadius: 12,
|
height: 24,
|
||||||
backgroundColor: '#fff',
|
borderRadius: 12,
|
||||||
shadowColor: '#000',
|
backgroundColor: colors.bg,
|
||||||
shadowOpacity: 0.15,
|
shadowColor: '#000',
|
||||||
shadowRadius: 2,
|
shadowOpacity: 0.15,
|
||||||
shadowOffset: { width: 0, height: 1 },
|
shadowRadius: 2,
|
||||||
elevation: 2,
|
shadowOffset: { width: 0, height: 1 },
|
||||||
},
|
elevation: 2,
|
||||||
toggleKnobOn: {
|
},
|
||||||
transform: [{ translateX: 18 }],
|
toggleKnobOn: {
|
||||||
},
|
transform: [{ translateX: 18 }],
|
||||||
subLabel: {
|
},
|
||||||
fontSize: 12,
|
subLabel: {
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontSize: 12,
|
||||||
color: '#737373',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
marginBottom: 6,
|
color: colors.textMuted,
|
||||||
},
|
marginBottom: 6,
|
||||||
modeRow: {
|
},
|
||||||
flexDirection: 'row',
|
modeRow: {
|
||||||
},
|
flexDirection: 'row',
|
||||||
modeBtn: {
|
},
|
||||||
flex: 1,
|
modeBtn: {
|
||||||
paddingVertical: 8,
|
flex: 1,
|
||||||
borderRadius: 10,
|
paddingVertical: 8,
|
||||||
borderWidth: 1,
|
borderRadius: 10,
|
||||||
borderColor: '#e5e5e5',
|
borderWidth: 1,
|
||||||
alignItems: 'center',
|
borderColor: colors.border,
|
||||||
marginRight: 6,
|
alignItems: 'center',
|
||||||
},
|
marginRight: 6,
|
||||||
modeBtnActive: {
|
},
|
||||||
backgroundColor: '#eff6ff',
|
modeBtnActive: {
|
||||||
borderColor: '#007AFF',
|
backgroundColor: colors.surface,
|
||||||
},
|
borderColor: '#007AFF',
|
||||||
modeBtnText: {
|
},
|
||||||
fontSize: 12,
|
modeBtnText: {
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontSize: 12,
|
||||||
color: '#737373',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
},
|
color: colors.textMuted,
|
||||||
modeBtnTextActive: {
|
},
|
||||||
color: '#007AFF',
|
modeBtnTextActive: {
|
||||||
},
|
color: '#007AFF',
|
||||||
actions: {
|
},
|
||||||
flexDirection: 'row',
|
actions: {
|
||||||
marginTop: 20,
|
flexDirection: 'row',
|
||||||
},
|
marginTop: 20,
|
||||||
cancelBtn: {
|
},
|
||||||
flex: 1,
|
cancelBtn: {
|
||||||
backgroundColor: '#f5f5f5',
|
flex: 1,
|
||||||
paddingVertical: 12,
|
backgroundColor: colors.surfaceElevated,
|
||||||
borderRadius: 12,
|
paddingVertical: 12,
|
||||||
alignItems: 'center',
|
borderRadius: 12,
|
||||||
marginRight: 6,
|
alignItems: 'center',
|
||||||
},
|
marginRight: 6,
|
||||||
cancelText: {
|
},
|
||||||
fontSize: 14,
|
cancelText: {
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontSize: 14,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
},
|
color: colors.text,
|
||||||
createBtn: {
|
},
|
||||||
flex: 1,
|
createBtn: {
|
||||||
backgroundColor: '#007AFF',
|
flex: 1,
|
||||||
paddingVertical: 12,
|
backgroundColor: '#007AFF',
|
||||||
borderRadius: 12,
|
paddingVertical: 12,
|
||||||
alignItems: 'center',
|
borderRadius: 12,
|
||||||
marginLeft: 6,
|
alignItems: 'center',
|
||||||
},
|
marginLeft: 6,
|
||||||
createText: {
|
},
|
||||||
fontSize: 14,
|
createText: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 14,
|
||||||
color: '#fff',
|
fontFamily: 'Nunito_700Bold',
|
||||||
},
|
color: '#fff',
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { View, Text, Pressable, Image, StyleSheet } from 'react-native';
|
import { View, Text, Pressable, Image, StyleSheet } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
export type Room = {
|
export type Room = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -29,6 +30,8 @@ function formatTime(ts: string, justNow: string) {
|
|||||||
|
|
||||||
export function RoomCard({ room, onPress }: Props) {
|
export function RoomCard({ room, onPress }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
|
const styles = makeStyles(colors);
|
||||||
const initials = room.name
|
const initials = room.name
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.slice(0, 2)
|
.slice(0, 2)
|
||||||
@ -42,7 +45,7 @@ export function RoomCard({ room, onPress }: Props) {
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.avatar,
|
styles.avatar,
|
||||||
{ backgroundColor: room.isPublic ? '#eff6ff' : '#e5e5e5' },
|
{ backgroundColor: room.isPublic ? colors.surface : colors.surfaceElevated },
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{room.avatarUrl ? (
|
{room.avatarUrl ? (
|
||||||
@ -102,116 +105,118 @@ export function RoomCard({ room, onPress }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
function makeStyles(colors: ReturnType<typeof useColors>) {
|
||||||
row: {
|
return StyleSheet.create({
|
||||||
width: '100%',
|
row: {
|
||||||
flexDirection: 'row',
|
width: '100%',
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
paddingHorizontal: 14,
|
alignItems: 'center',
|
||||||
paddingVertical: 11,
|
paddingHorizontal: 14,
|
||||||
backgroundColor: '#fff',
|
paddingVertical: 11,
|
||||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
backgroundColor: colors.bg,
|
||||||
borderBottomColor: '#f5f5f5',
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||||
},
|
borderBottomColor: colors.border,
|
||||||
avatar: {
|
},
|
||||||
width: 42,
|
avatar: {
|
||||||
height: 42,
|
width: 42,
|
||||||
borderRadius: 21,
|
height: 42,
|
||||||
alignItems: 'center',
|
borderRadius: 21,
|
||||||
justifyContent: 'center',
|
alignItems: 'center',
|
||||||
overflow: 'hidden',
|
justifyContent: 'center',
|
||||||
marginRight: 10,
|
overflow: 'hidden',
|
||||||
},
|
marginRight: 10,
|
||||||
avatarImg: {
|
},
|
||||||
width: 42,
|
avatarImg: {
|
||||||
height: 42,
|
width: 42,
|
||||||
},
|
height: 42,
|
||||||
avatarInitials: {
|
},
|
||||||
fontSize: 13,
|
avatarInitials: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 13,
|
||||||
color: '#525252',
|
fontFamily: 'Nunito_700Bold',
|
||||||
},
|
color: colors.textMuted,
|
||||||
info: {
|
},
|
||||||
flex: 1,
|
info: {
|
||||||
minWidth: 0,
|
flex: 1,
|
||||||
},
|
minWidth: 0,
|
||||||
headerRow: {
|
},
|
||||||
flexDirection: 'row',
|
headerRow: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
},
|
alignItems: 'center',
|
||||||
footerRow: {
|
},
|
||||||
flexDirection: 'row',
|
footerRow: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
marginTop: 3,
|
alignItems: 'center',
|
||||||
},
|
marginTop: 3,
|
||||||
footerTextWrap: {
|
},
|
||||||
flex: 1,
|
footerTextWrap: {
|
||||||
minWidth: 0,
|
flex: 1,
|
||||||
},
|
minWidth: 0,
|
||||||
metaPill: {
|
},
|
||||||
flexDirection: 'row',
|
metaPill: {
|
||||||
alignItems: 'center',
|
flexDirection: 'row',
|
||||||
marginLeft: 8,
|
alignItems: 'center',
|
||||||
paddingHorizontal: 6,
|
marginLeft: 8,
|
||||||
paddingVertical: 2,
|
paddingHorizontal: 6,
|
||||||
borderRadius: 8,
|
paddingVertical: 2,
|
||||||
backgroundColor: '#f5f5f5',
|
borderRadius: 8,
|
||||||
},
|
backgroundColor: colors.surfaceElevated,
|
||||||
name: {
|
},
|
||||||
fontSize: 14,
|
name: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 14,
|
||||||
color: '#171717',
|
fontFamily: 'Nunito_700Bold',
|
||||||
flexShrink: 1,
|
color: colors.text,
|
||||||
},
|
flexShrink: 1,
|
||||||
defaultBadge: {
|
},
|
||||||
marginLeft: 6,
|
defaultBadge: {
|
||||||
paddingHorizontal: 6,
|
marginLeft: 6,
|
||||||
paddingVertical: 1,
|
paddingHorizontal: 6,
|
||||||
backgroundColor: '#eff6ff',
|
paddingVertical: 1,
|
||||||
borderRadius: 8,
|
backgroundColor: colors.surface,
|
||||||
},
|
borderRadius: 8,
|
||||||
defaultBadgeText: {
|
},
|
||||||
fontSize: 9,
|
defaultBadgeText: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 9,
|
||||||
color: '#007AFF',
|
fontFamily: 'Nunito_700Bold',
|
||||||
},
|
color: '#007AFF',
|
||||||
lastMessage: {
|
},
|
||||||
fontSize: 12,
|
lastMessage: {
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontSize: 12,
|
||||||
color: '#737373',
|
fontFamily: 'Nunito_400Regular',
|
||||||
},
|
color: colors.textMuted,
|
||||||
description: {
|
},
|
||||||
fontSize: 12,
|
description: {
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontSize: 12,
|
||||||
color: '#a3a3a3',
|
fontFamily: 'Nunito_400Regular',
|
||||||
},
|
color: colors.textMuted,
|
||||||
right: {
|
},
|
||||||
alignItems: 'flex-end',
|
right: {
|
||||||
marginLeft: 8,
|
alignItems: 'flex-end',
|
||||||
},
|
marginLeft: 8,
|
||||||
memberCount: {
|
},
|
||||||
fontSize: 11,
|
memberCount: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 11,
|
||||||
color: '#737373',
|
fontFamily: 'Nunito_700Bold',
|
||||||
marginLeft: 3,
|
color: colors.textMuted,
|
||||||
},
|
marginLeft: 3,
|
||||||
time: {
|
},
|
||||||
fontSize: 10,
|
time: {
|
||||||
fontFamily: 'Nunito_500Medium',
|
fontSize: 10,
|
||||||
color: '#a3a3a3',
|
fontFamily: 'Nunito_500Medium',
|
||||||
marginLeft: 'auto',
|
color: colors.textMuted,
|
||||||
paddingLeft: 6,
|
marginLeft: 'auto',
|
||||||
},
|
paddingLeft: 6,
|
||||||
joinBadge: {
|
},
|
||||||
marginLeft: 6,
|
joinBadge: {
|
||||||
paddingHorizontal: 8,
|
marginLeft: 6,
|
||||||
paddingVertical: 3,
|
paddingHorizontal: 8,
|
||||||
backgroundColor: '#eff6ff',
|
paddingVertical: 3,
|
||||||
borderRadius: 10,
|
backgroundColor: colors.surface,
|
||||||
},
|
borderRadius: 10,
|
||||||
joinBadgeText: {
|
},
|
||||||
fontSize: 10,
|
joinBadgeText: {
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontSize: 10,
|
||||||
color: '#007AFF',
|
fontFamily: 'Nunito_700Bold',
|
||||||
},
|
color: '#007AFF',
|
||||||
});
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMailConnect, detectProvider, type MailProvider } from '../../hooks/useMailConnect';
|
import { useMailConnect, detectProvider, type MailProvider } from '../../hooks/useMailConnect';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
||||||
const SHEET_HEIGHT = SCREEN_HEIGHT * 0.65;
|
const SHEET_HEIGHT = SCREEN_HEIGHT * 0.65;
|
||||||
@ -97,6 +98,7 @@ const PROVIDERS: ProviderConfig[] = [
|
|||||||
*/
|
*/
|
||||||
export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
|
export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { connect, connecting, error: connectError } = useMailConnect();
|
const { connect, connecting, error: connectError } = useMailConnect();
|
||||||
|
|
||||||
@ -203,7 +205,7 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
|
|||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
height: SHEET_HEIGHT,
|
height: SHEET_HEIGHT,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
borderTopLeftRadius: 20,
|
borderTopLeftRadius: 20,
|
||||||
borderTopRightRadius: 20,
|
borderTopRightRadius: 20,
|
||||||
transform: [{ translateY }],
|
transform: [{ translateY }],
|
||||||
@ -215,7 +217,7 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
|
|||||||
>
|
>
|
||||||
{/* Drag-Handle */}
|
{/* Drag-Handle */}
|
||||||
<View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 4 }}>
|
<View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 4 }}>
|
||||||
<View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: '#d4d4d4' }} />
|
<View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: colors.border }} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -228,24 +230,24 @@ export function ConnectMailSheet({ visible, onClose, onSuccess }: Props) {
|
|||||||
paddingTop: 6,
|
paddingTop: 6,
|
||||||
paddingBottom: 12,
|
paddingBottom: 12,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#f0f0f0',
|
borderBottomColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{view === 'form' ? (
|
{view === 'form' ? (
|
||||||
<Pressable onPress={handleBack} hitSlop={10}>
|
<Pressable onPress={handleBack} hitSlop={10}>
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: '#525252' }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
|
||||||
{t('common.back')}
|
{t('common.back')}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
) : (
|
) : (
|
||||||
<Pressable onPress={handleClose} hitSlop={10}>
|
<Pressable onPress={handleClose} hitSlop={10}>
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: '#525252' }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
|
||||||
{view === 'form' && currentProvider
|
{view === 'form' && currentProvider
|
||||||
? t(currentProvider.labelKey)
|
? t(currentProvider.labelKey)
|
||||||
: t('mail.connect_sheet_title')}
|
: t('mail.connect_sheet_title')}
|
||||||
@ -293,6 +295,7 @@ function ProviderGrid({
|
|||||||
onSelect: (p: ProviderConfig) => void;
|
onSelect: (p: ProviderConfig) => void;
|
||||||
t: (key: string) => string;
|
t: (key: string) => string;
|
||||||
}) {
|
}) {
|
||||||
|
const colors = useColors();
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
@ -303,7 +306,7 @@ function ProviderGrid({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#737373',
|
color: colors.textMuted,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
}}
|
}}
|
||||||
@ -325,9 +328,9 @@ function ProviderGrid({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: 10,
|
gap: 10,
|
||||||
backgroundColor: '#f9f9f9',
|
backgroundColor: colors.surface,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e5e5',
|
borderColor: colors.border,
|
||||||
borderRadius: 14,
|
borderRadius: 14,
|
||||||
padding: 14,
|
padding: 14,
|
||||||
}}>
|
}}>
|
||||||
@ -345,13 +348,13 @@ function ProviderGrid({
|
|||||||
</View>
|
</View>
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<Text
|
<Text
|
||||||
style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}
|
style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: colors.text }}
|
||||||
numberOfLines={1}
|
numberOfLines={1}
|
||||||
>
|
>
|
||||||
{t(p.labelKey)}
|
{t(p.labelKey)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<Ionicons name="chevron-forward" size={14} color="#d4d4d4" />
|
<Ionicons name="chevron-forward" size={14} color={colors.border} />
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
))}
|
))}
|
||||||
@ -394,6 +397,7 @@ function FormView({
|
|||||||
insets,
|
insets,
|
||||||
t,
|
t,
|
||||||
}: FormViewProps) {
|
}: FormViewProps) {
|
||||||
|
const colors = useColors();
|
||||||
const canConnect = email.trim().length > 0 && password.trim().length > 0 && !connecting;
|
const canConnect = email.trim().length > 0 && password.trim().length > 0 && !connecting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -461,7 +465,7 @@ function FormView({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
color: '#525252',
|
color: colors.textMuted,
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -471,19 +475,19 @@ function FormView({
|
|||||||
value={email}
|
value={email}
|
||||||
onChangeText={onEmailChange}
|
onChangeText={onEmailChange}
|
||||||
placeholder={t('mail.form_email_placeholder')}
|
placeholder={t('mail.form_email_placeholder')}
|
||||||
placeholderTextColor="#a3a3a3"
|
placeholderTextColor={colors.textMuted}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
keyboardType="email-address"
|
keyboardType="email-address"
|
||||||
returnKeyType="next"
|
returnKeyType="next"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: colors.surfaceElevated,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#0a0a0a',
|
color: colors.text,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@ -494,7 +498,7 @@ function FormView({
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
color: '#525252',
|
color: colors.textMuted,
|
||||||
marginBottom: 6,
|
marginBottom: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -505,21 +509,21 @@ function FormView({
|
|||||||
value={password}
|
value={password}
|
||||||
onChangeText={onPasswordChange}
|
onChangeText={onPasswordChange}
|
||||||
placeholder={t('mail.form_password_placeholder')}
|
placeholder={t('mail.form_password_placeholder')}
|
||||||
placeholderTextColor="#a3a3a3"
|
placeholderTextColor={colors.textMuted}
|
||||||
secureTextEntry={!passwordVisible}
|
secureTextEntry={!passwordVisible}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
returnKeyType="done"
|
returnKeyType="done"
|
||||||
onSubmitEditing={onConnect}
|
onSubmitEditing={onConnect}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: colors.surfaceElevated,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
paddingVertical: 12,
|
paddingVertical: 12,
|
||||||
paddingRight: 46,
|
paddingRight: 46,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#0a0a0a',
|
color: colors.text,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Pressable
|
<Pressable
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useMailConnect } from '../../hooks/useMailConnect';
|
import { useMailConnect } from '../../hooks/useMailConnect';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
||||||
const SHEET_HEIGHT = SCREEN_HEIGHT * 0.5;
|
const SHEET_HEIGHT = SCREEN_HEIGHT * 0.5;
|
||||||
@ -33,6 +34,7 @@ type Props = {
|
|||||||
*/
|
*/
|
||||||
export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Props) {
|
export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const { connect, connecting, error: connectError } = useMailConnect();
|
const { connect, connecting, error: connectError } = useMailConnect();
|
||||||
|
|
||||||
@ -104,7 +106,7 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
|
|||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
height: SHEET_HEIGHT,
|
height: SHEET_HEIGHT,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
borderTopLeftRadius: 20,
|
borderTopLeftRadius: 20,
|
||||||
borderTopRightRadius: 20,
|
borderTopRightRadius: 20,
|
||||||
transform: [{ translateY }],
|
transform: [{ translateY }],
|
||||||
@ -116,7 +118,7 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
|
|||||||
>
|
>
|
||||||
{/* Drag-Handle */}
|
{/* Drag-Handle */}
|
||||||
<View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 4 }}>
|
<View style={{ alignItems: 'center', paddingTop: 8, paddingBottom: 4 }}>
|
||||||
<View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: '#d4d4d4' }} />
|
<View style={{ width: 36, height: 4, borderRadius: 2, backgroundColor: colors.border }} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
@ -129,15 +131,15 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
|
|||||||
paddingTop: 6,
|
paddingTop: 6,
|
||||||
paddingBottom: 12,
|
paddingBottom: 12,
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: '#f0f0f0',
|
borderBottomColor: colors.border,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pressable onPress={onClose} hitSlop={10}>
|
<Pressable onPress={onClose} hitSlop={10}>
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: '#525252' }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: '#0a0a0a' }}>
|
<Text style={{ fontSize: 16, fontFamily: 'Nunito_700Bold', color: colors.text }}>
|
||||||
{t('mail.edit_account_title')}
|
{t('mail.edit_account_title')}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ width: 60 }} />
|
<View style={{ width: 60 }} />
|
||||||
@ -148,7 +150,7 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#737373',
|
color: colors.textMuted,
|
||||||
lineHeight: 18,
|
lineHeight: 18,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -159,13 +161,13 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
|
|||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
backgroundColor: '#f5f5f5',
|
backgroundColor: colors.surfaceElevated,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
gap: 10,
|
gap: 10,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name="lock-closed-outline" size={16} color="#a3a3a3" />
|
<Ionicons name="lock-closed-outline" size={16} color={colors.textMuted} />
|
||||||
<TextInput
|
<TextInput
|
||||||
value={password}
|
value={password}
|
||||||
onChangeText={(v) => {
|
onChangeText={(v) => {
|
||||||
@ -173,7 +175,7 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
|
|||||||
setFormError(null);
|
setFormError(null);
|
||||||
}}
|
}}
|
||||||
placeholder={t('mail.app_password_placeholder')}
|
placeholder={t('mail.app_password_placeholder')}
|
||||||
placeholderTextColor="#a3a3a3"
|
placeholderTextColor={colors.textMuted}
|
||||||
secureTextEntry={!passwordVisible}
|
secureTextEntry={!passwordVisible}
|
||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
@ -182,7 +184,7 @@ export function EditMailAccountSheet({ visible, email, onClose, onSuccess }: Pro
|
|||||||
paddingVertical: 14,
|
paddingVertical: 14,
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#0a0a0a',
|
color: colors.text,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Pressable onPress={() => setPasswordVisible((p) => !p)} hitSlop={8}>
|
<Pressable onPress={() => setPasswordVisible((p) => !p)} hitSlop={8}>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Pressable, Text, View } from 'react-native';
|
import { Pressable, Text, View } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onConnectPress: () => void;
|
onConnectPress: () => void;
|
||||||
@ -12,14 +13,15 @@ type Props = {
|
|||||||
*/
|
*/
|
||||||
export function MailEmptyState({ onConnectPress }: Props) {
|
export function MailEmptyState({ onConnectPress }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e5e5',
|
borderColor: colors.border,
|
||||||
padding: 28,
|
padding: 28,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
@ -45,7 +47,7 @@ export function MailEmptyState({ onConnectPress }: Props) {
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 17,
|
fontSize: 17,
|
||||||
fontFamily: 'Nunito_700Bold',
|
fontFamily: 'Nunito_700Bold',
|
||||||
color: '#0a0a0a',
|
color: colors.text,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
}}
|
}}
|
||||||
@ -57,7 +59,7 @@ export function MailEmptyState({ onConnectPress }: Props) {
|
|||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: '#737373',
|
color: colors.textMuted,
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
lineHeight: 19,
|
lineHeight: 19,
|
||||||
marginBottom: 20,
|
marginBottom: 20,
|
||||||
@ -71,7 +73,7 @@ export function MailEmptyState({ onConnectPress }: Props) {
|
|||||||
{(['privacy_1', 'privacy_2', 'privacy_3'] as const).map((key) => (
|
{(['privacy_1', 'privacy_2', 'privacy_3'] as const).map((key) => (
|
||||||
<View key={key} style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
<View key={key} style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||||
<Ionicons name="checkmark-circle" size={15} color="#16a34a" />
|
<Ionicons name="checkmark-circle" size={15} color="#16a34a" />
|
||||||
<Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: '#525252', flex: 1 }}>
|
<Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: colors.textMuted, flex: 1 }}>
|
||||||
{t(`mail.${key}`)}
|
{t(`mail.${key}`)}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { View, Text, Pressable } from 'react-native';
|
import { View, Text, Pressable } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { colors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onDismiss?: () => void;
|
onDismiss?: () => void;
|
||||||
@ -8,6 +8,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function DigaMissionBanner({ onDismiss, onContribute }: Props) {
|
export function DigaMissionBanner({ onDismiss, onContribute }: Props) {
|
||||||
|
const colors = useColors();
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@ -2,11 +2,12 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { View, Text, Pressable, Animated, StyleSheet } from 'react-native';
|
import { View, Text, Pressable, Animated, StyleSheet } from 'react-native';
|
||||||
import { BREATH_PHASES, TOTAL_ROUNDS, type BreathState } from '../../lib/sosConstants';
|
import { BREATH_PHASES, TOTAL_ROUNDS, type BreathState } from '../../lib/sosConstants';
|
||||||
import { colors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
type Props = { onDone: () => void; onSpeak?: (text: string) => Promise<void> | void };
|
type Props = { onDone: () => void; onSpeak?: (text: string) => Promise<void> | void };
|
||||||
|
|
||||||
export function BreathingCard({ onDone, onSpeak }: Props) {
|
export function BreathingCard({ onDone, onSpeak }: Props) {
|
||||||
|
const colors = useColors();
|
||||||
const [breathState, setBreathState] = useState<BreathState>('idle');
|
const [breathState, setBreathState] = useState<BreathState>('idle');
|
||||||
const [countdown, setCountdown] = useState(3);
|
const [countdown, setCountdown] = useState(3);
|
||||||
const [round, setRound] = useState(1);
|
const [round, setRound] = useState(1);
|
||||||
@ -86,7 +87,7 @@ export function BreathingCard({ onDone, onSpeak }: Props) {
|
|||||||
<View style={{ alignItems: 'center', gap: 16 }}>
|
<View style={{ alignItems: 'center', gap: 16 }}>
|
||||||
<Text style={st.breathTitle}>4-7-8 Atemübung</Text>
|
<Text style={st.breathTitle}>4-7-8 Atemübung</Text>
|
||||||
<Text style={st.breathSub}>3 Runden · beruhigt dein Nervensystem</Text>
|
<Text style={st.breathSub}>3 Runden · beruhigt dein Nervensystem</Text>
|
||||||
<Pressable style={st.breathStartBtn} onPress={() => { setCountdown(3); setBreathState('countdown'); }}>
|
<Pressable style={[st.breathStartBtn, { backgroundColor: colors.brandOrange }]} onPress={() => { setCountdown(3); setBreathState('countdown'); }}>
|
||||||
<Text style={st.breathStartTxt}>Starten</Text>
|
<Text style={st.breathStartTxt}>Starten</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
@ -114,6 +115,7 @@ export function BreathingCard({ onDone, onSpeak }: Props) {
|
|||||||
|
|
||||||
// ── BreathingDrawer (bottom sheet, covers input, slides up) ───────────────────
|
// ── BreathingDrawer (bottom sheet, covers input, slides up) ───────────────────
|
||||||
export function BreathingDrawer({ onDone, onSpeak }: Props) {
|
export function BreathingDrawer({ onDone, onSpeak }: Props) {
|
||||||
|
const colors = useColors();
|
||||||
const slideAnim = useRef(new Animated.Value(500)).current;
|
const slideAnim = useRef(new Animated.Value(500)).current;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, damping: 22, mass: 1, stiffness: 200 }).start();
|
Animated.spring(slideAnim, { toValue: 0, useNativeDriver: true, damping: 22, mass: 1, stiffness: 200 }).start();
|
||||||
@ -121,7 +123,7 @@ export function BreathingDrawer({ onDone, onSpeak }: Props) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<View style={st.breathBackdrop} pointerEvents="none" />
|
<View style={st.breathBackdrop} pointerEvents="none" />
|
||||||
<Animated.View style={[st.breathDrawerContainer, { transform: [{ translateY: slideAnim }] }]}>
|
<Animated.View style={[st.breathDrawerContainer, { transform: [{ translateY: slideAnim }], backgroundColor: colors.bg }]}>
|
||||||
<View style={st.breathDrawerHandle} />
|
<View style={st.breathDrawerHandle} />
|
||||||
<BreathingCard onDone={onDone} onSpeak={onSpeak} />
|
<BreathingCard onDone={onDone} onSpeak={onSpeak} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@ -131,14 +133,14 @@ export function BreathingDrawer({ onDone, onSpeak }: Props) {
|
|||||||
|
|
||||||
const st = StyleSheet.create({
|
const st = StyleSheet.create({
|
||||||
breathBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.28)', zIndex: 20 },
|
breathBackdrop: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.28)', zIndex: 20 },
|
||||||
breathDrawerContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 21, backgroundColor: '#ffffff', borderTopLeftRadius: 28, borderTopRightRadius: 28, paddingBottom: 36, shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.18, shadowRadius: 20, elevation: 24 },
|
breathDrawerContainer: { position: 'absolute', bottom: 0, left: 0, right: 0, zIndex: 21, borderTopLeftRadius: 28, borderTopRightRadius: 28, paddingBottom: 36, shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.18, shadowRadius: 20, elevation: 24 },
|
||||||
breathDrawerHandle: { width: 40, height: 4, borderRadius: 2, backgroundColor: '#d1d5db', alignSelf: 'center', marginTop: 14, marginBottom: 4 },
|
breathDrawerHandle: { width: 40, height: 4, borderRadius: 2, backgroundColor: '#d1d5db', alignSelf: 'center', marginTop: 14, marginBottom: 4 },
|
||||||
breathCardInner: { paddingHorizontal: 24, paddingTop: 20, paddingBottom: 8, alignItems: 'center', gap: 16 },
|
breathCardInner: { paddingHorizontal: 24, paddingTop: 20, paddingBottom: 8, alignItems: 'center', gap: 16 },
|
||||||
breathCircleLg: { width: 190, height: 190, borderRadius: 95, alignItems: 'center', justifyContent: 'center', borderWidth: 5 },
|
breathCircleLg: { width: 190, height: 190, borderRadius: 95, alignItems: 'center', justifyContent: 'center', borderWidth: 5 },
|
||||||
breathCountLg: { fontFamily: 'Nunito_800ExtraBold', fontSize: 60, color: '#111827', lineHeight: 68 },
|
breathCountLg: { fontFamily: 'Nunito_800ExtraBold', fontSize: 60, color: '#111827', lineHeight: 68 },
|
||||||
breathTitle: { fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' },
|
breathTitle: { fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' },
|
||||||
breathSub: { fontFamily: 'Nunito_400Regular', fontSize: 13, color: '#6b7280', textAlign: 'center' },
|
breathSub: { fontFamily: 'Nunito_400Regular', fontSize: 13, color: '#6b7280', textAlign: 'center' },
|
||||||
breathStartBtn: { borderRadius: 12, backgroundColor: colors.brandOrange, paddingHorizontal: 28, paddingVertical: 10, marginTop: 4 },
|
breathStartBtn: { borderRadius: 12, paddingHorizontal: 28, paddingVertical: 10, marginTop: 4 },
|
||||||
breathStartTxt: { color: '#fff', fontFamily: 'Nunito_700Bold', fontSize: 14 },
|
breathStartTxt: { color: '#fff', fontFamily: 'Nunito_700Bold', fontSize: 14 },
|
||||||
breathRound: { fontFamily: 'Nunito_600SemiBold', fontSize: 12, color: '#9ca3af' },
|
breathRound: { fontFamily: 'Nunito_600SemiBold', fontSize: 12, color: '#9ca3af' },
|
||||||
breathPhaseLabel: { fontFamily: 'Nunito_700Bold', fontSize: 13 },
|
breathPhaseLabel: { fontFamily: 'Nunito_700Bold', fontSize: 13 },
|
||||||
|
|||||||
@ -11,7 +11,7 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { colors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
import type { SosFeedback } from './SosFeedbackModal';
|
import type { SosFeedback } from './SosFeedbackModal';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,6 +25,7 @@ export function InlineRatingDrawer({
|
|||||||
onSubmit: (feedback: SosFeedback) => Promise<void> | void;
|
onSubmit: (feedback: SosFeedback) => Promise<void> | void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const colors = useColors();
|
||||||
const slide = useRef(new Animated.Value(600)).current;
|
const slide = useRef(new Animated.Value(600)).current;
|
||||||
const [better, setBetter] = useState<boolean | null>(null);
|
const [better, setBetter] = useState<boolean | null>(null);
|
||||||
const [rating, setRating] = useState(0);
|
const [rating, setRating] = useState(0);
|
||||||
@ -58,7 +59,7 @@ export function InlineRatingDrawer({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Pressable style={s.backdrop} onPress={onClose} />
|
<Pressable style={s.backdrop} onPress={onClose} />
|
||||||
<Animated.View style={[s.drawer, { transform: [{ translateY: slide }] }]}>
|
<Animated.View style={[s.drawer, { transform: [{ translateY: slide }], backgroundColor: colors.bg }]}>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 24 : 0}
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 24 : 0}
|
||||||
@ -71,13 +72,13 @@ export function InlineRatingDrawer({
|
|||||||
>
|
>
|
||||||
<View style={s.header}>
|
<View style={s.header}>
|
||||||
<Ionicons name="star" size={20} color="#f59e0b" />
|
<Ionicons name="star" size={20} color="#f59e0b" />
|
||||||
<Text style={s.title}>Bewerte diese Session</Text>
|
<Text style={[s.title, { color: colors.text }]}>Bewerte diese Session</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={s.sub}>
|
<Text style={[s.sub, { color: colors.textMuted }]}>
|
||||||
Dein Feedback hilft uns, Lyra besser zu machen.
|
Dein Feedback hilft uns, Lyra besser zu machen.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text style={s.q}>Fühlst du dich besser?</Text>
|
<Text style={[s.q, { color: colors.textMuted }]}>Fühlst du dich besser?</Text>
|
||||||
<View style={s.btnRow}>
|
<View style={s.btnRow}>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[s.choiceBtn, better === true && s.choiceBtnYes]}
|
style={[s.choiceBtn, better === true && s.choiceBtnYes]}
|
||||||
@ -103,7 +104,7 @@ export function InlineRatingDrawer({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={s.q}>Bewertung</Text>
|
<Text style={[s.q, { color: colors.textMuted }]}>Bewertung</Text>
|
||||||
<View style={s.starsRow}>
|
<View style={s.starsRow}>
|
||||||
{[1, 2, 3, 4, 5].map((n) => (
|
{[1, 2, 3, 4, 5].map((n) => (
|
||||||
<Pressable key={n} onPress={() => setRating(n)} hitSlop={6}>
|
<Pressable key={n} onPress={() => setRating(n)} hitSlop={6}>
|
||||||
@ -116,9 +117,9 @@ export function InlineRatingDrawer({
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Text style={s.q}>Bemerkung (optional)</Text>
|
<Text style={[s.q, { color: colors.textMuted }]}>Bemerkung (optional)</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={s.textArea}
|
style={[s.textArea, { backgroundColor: colors.surfaceElevated, borderColor: colors.border, color: colors.text }]}
|
||||||
placeholder="Was war hilfreich? Was nicht?"
|
placeholder="Was war hilfreich? Was nicht?"
|
||||||
placeholderTextColor="#94a3b8"
|
placeholderTextColor="#94a3b8"
|
||||||
multiline
|
multiline
|
||||||
@ -132,7 +133,7 @@ export function InlineRatingDrawer({
|
|||||||
<Text style={s.cancelTxt}>Abbrechen</Text>
|
<Text style={s.cancelTxt}>Abbrechen</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[s.submitBtn, submitting && { opacity: 0.6 }]}
|
style={[s.submitBtn, { backgroundColor: colors.brandOrange }, submitting && { opacity: 0.6 }]}
|
||||||
onPress={submit}
|
onPress={submit}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
>
|
>
|
||||||
@ -199,7 +200,7 @@ const s = StyleSheet.create({
|
|||||||
cancelTxt: { fontFamily: 'Nunito_700Bold', fontSize: 14, color: '#475569' },
|
cancelTxt: { fontFamily: 'Nunito_700Bold', fontSize: 14, color: '#475569' },
|
||||||
submitBtn: {
|
submitBtn: {
|
||||||
flex: 2, paddingVertical: 12, borderRadius: 12,
|
flex: 2, paddingVertical: 12, borderRadius: 12,
|
||||||
alignItems: 'center', backgroundColor: colors.brandOrange,
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
submitTxt: { fontFamily: 'Nunito_800ExtraBold', fontSize: 14, color: '#fff' },
|
submitTxt: { fontFamily: 'Nunito_800ExtraBold', fontSize: 14, color: '#fff' },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
ScrollView,
|
ScrollView,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { colors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
export interface ShareSuccessPayload {
|
export interface ShareSuccessPayload {
|
||||||
text: string;
|
text: string;
|
||||||
@ -35,6 +35,7 @@ export function ShareSuccessDrawer({
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onRegenerate?: () => void;
|
onRegenerate?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const colors = useColors();
|
||||||
const slide = useRef(new Animated.Value(600)).current;
|
const slide = useRef(new Animated.Value(600)).current;
|
||||||
const [text, setText] = useState(initialText);
|
const [text, setText] = useState(initialText);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@ -67,7 +68,7 @@ export function ShareSuccessDrawer({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Pressable style={s.backdrop} onPress={onClose} />
|
<Pressable style={s.backdrop} onPress={onClose} />
|
||||||
<Animated.View style={[s.drawer, { transform: [{ translateY: slide }] }]}>
|
<Animated.View style={[s.drawer, { transform: [{ translateY: slide }], backgroundColor: colors.bg }]}>
|
||||||
<KeyboardAvoidingView
|
<KeyboardAvoidingView
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 24 : 0}
|
keyboardVerticalOffset={Platform.OS === 'ios' ? 24 : 0}
|
||||||
@ -80,9 +81,9 @@ export function ShareSuccessDrawer({
|
|||||||
>
|
>
|
||||||
<View style={s.header}>
|
<View style={s.header}>
|
||||||
<Ionicons name="sparkles" size={20} color={colors.brandOrange} />
|
<Ionicons name="sparkles" size={20} color={colors.brandOrange} />
|
||||||
<Text style={s.title}>Erfolg teilen</Text>
|
<Text style={[s.title, { color: colors.text }]}>Erfolg teilen</Text>
|
||||||
</View>
|
</View>
|
||||||
<Text style={s.sub}>
|
<Text style={[s.sub, { color: colors.textMuted }]}>
|
||||||
Inspiriere andere — dein Beitrag wird anonym in der Community gepostet.
|
Inspiriere andere — dein Beitrag wird anonym in der Community gepostet.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ export function ShareSuccessDrawer({
|
|||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<TextInput
|
<TextInput
|
||||||
style={s.textArea}
|
style={[s.textArea, { backgroundColor: colors.surfaceElevated, borderColor: colors.border, color: colors.text }]}
|
||||||
multiline
|
multiline
|
||||||
value={text}
|
value={text}
|
||||||
onChangeText={setText}
|
onChangeText={setText}
|
||||||
@ -118,7 +119,7 @@ export function ShareSuccessDrawer({
|
|||||||
<Text style={s.cancelTxt}>Abbrechen</Text>
|
<Text style={s.cancelTxt}>Abbrechen</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[s.shareBtn, (!text.trim() || submitting || generating) && s.shareBtnDisabled]}
|
style={[s.shareBtn, { backgroundColor: colors.brandOrange }, (!text.trim() || submitting || generating) && s.shareBtnDisabled]}
|
||||||
onPress={handleShare}
|
onPress={handleShare}
|
||||||
disabled={!text.trim() || submitting || generating}
|
disabled={!text.trim() || submitting || generating}
|
||||||
>
|
>
|
||||||
@ -203,7 +204,6 @@ const s = StyleSheet.create({
|
|||||||
shareBtn: {
|
shareBtn: {
|
||||||
flex: 1, minWidth: 110,
|
flex: 1, minWidth: 110,
|
||||||
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6,
|
flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||||
backgroundColor: colors.brandOrange,
|
|
||||||
borderRadius: 12, paddingVertical: 12,
|
borderRadius: 12, paddingVertical: 12,
|
||||||
},
|
},
|
||||||
shareBtnDisabled: { opacity: 0.5 },
|
shareBtnDisabled: { opacity: 0.5 },
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { View, Text, Pressable, TextInput, Modal, StyleSheet, Platform, KeyboardAvoidingView, ScrollView } from 'react-native';
|
import { View, Text, Pressable, TextInput, Modal, StyleSheet, Platform, KeyboardAvoidingView, ScrollView } from 'react-native';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { colors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
export interface SosFeedback {
|
export interface SosFeedback {
|
||||||
better: boolean | null;
|
better: boolean | null;
|
||||||
@ -18,6 +18,7 @@ export function SosFeedbackModal({
|
|||||||
onSubmit: (feedback: SosFeedback) => void;
|
onSubmit: (feedback: SosFeedback) => void;
|
||||||
onSkip: () => void;
|
onSkip: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
const colors = useColors();
|
||||||
const [better, setBetter] = useState<boolean | null>(null);
|
const [better, setBetter] = useState<boolean | null>(null);
|
||||||
const [rating, setRating] = useState<number>(0);
|
const [rating, setRating] = useState<number>(0);
|
||||||
const [text, setText] = useState('');
|
const [text, setText] = useState('');
|
||||||
@ -43,12 +44,12 @@ export function SosFeedbackModal({
|
|||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<View style={s.card}>
|
<View style={[s.card, { backgroundColor: colors.bg }]}>
|
||||||
<Text style={s.title}>Wie war diese Session?</Text>
|
<Text style={[s.title, { color: colors.text }]}>Wie war diese Session?</Text>
|
||||||
<Text style={s.sub}>Dein Feedback hilft Lyra besser zu werden.</Text>
|
<Text style={[s.sub, { color: colors.textMuted }]}>Dein Feedback hilft Lyra besser zu werden.</Text>
|
||||||
|
|
||||||
{/* Better Yes/No */}
|
{/* Better Yes/No */}
|
||||||
<Text style={s.q}>Fühlst du dich besser?</Text>
|
<Text style={[s.q, { color: colors.textMuted }]}>Fühlst du dich besser?</Text>
|
||||||
<View style={s.btnRow}>
|
<View style={s.btnRow}>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[s.choiceBtn, better === true && s.choiceBtnYes]}
|
style={[s.choiceBtn, better === true && s.choiceBtnYes]}
|
||||||
@ -67,7 +68,7 @@ export function SosFeedbackModal({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Stars */}
|
{/* Stars */}
|
||||||
<Text style={s.q}>Bewertung</Text>
|
<Text style={[s.q, { color: colors.textMuted }]}>Bewertung</Text>
|
||||||
<View style={s.starsRow}>
|
<View style={s.starsRow}>
|
||||||
{[1, 2, 3, 4, 5].map((n) => (
|
{[1, 2, 3, 4, 5].map((n) => (
|
||||||
<Pressable key={n} onPress={() => setRating(n)} hitSlop={6}>
|
<Pressable key={n} onPress={() => setRating(n)} hitSlop={6}>
|
||||||
@ -81,9 +82,9 @@ export function SosFeedbackModal({
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Comment */}
|
{/* Comment */}
|
||||||
<Text style={s.q}>Bemerkung (optional)</Text>
|
<Text style={[s.q, { color: colors.textMuted }]}>Bemerkung (optional)</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
style={s.textArea}
|
style={[s.textArea, { backgroundColor: colors.surfaceElevated, borderColor: colors.border, color: colors.text }]}
|
||||||
placeholder="Was war hilfreich? Was nicht?"
|
placeholder="Was war hilfreich? Was nicht?"
|
||||||
placeholderTextColor="#94a3b8"
|
placeholderTextColor="#94a3b8"
|
||||||
multiline
|
multiline
|
||||||
@ -98,7 +99,7 @@ export function SosFeedbackModal({
|
|||||||
<Pressable style={s.skipBtn} onPress={skip}>
|
<Pressable style={s.skipBtn} onPress={skip}>
|
||||||
<Text style={s.skipTxt}>Überspringen</Text>
|
<Text style={s.skipTxt}>Überspringen</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable style={s.submitBtn} onPress={submit}>
|
<Pressable style={[s.submitBtn, { backgroundColor: colors.brandOrange }]} onPress={submit}>
|
||||||
<Text style={s.submitTxt}>Senden</Text>
|
<Text style={s.submitTxt}>Senden</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
@ -136,6 +137,6 @@ const s = StyleSheet.create({
|
|||||||
actions: { flexDirection: 'row', gap: 10, marginTop: 18 },
|
actions: { flexDirection: 'row', gap: 10, marginTop: 18 },
|
||||||
skipBtn: { flex: 1, paddingVertical: 12, borderRadius: 12, alignItems: 'center', backgroundColor: '#f1f5f9' },
|
skipBtn: { flex: 1, paddingVertical: 12, borderRadius: 12, alignItems: 'center', backgroundColor: '#f1f5f9' },
|
||||||
skipTxt: { fontFamily: 'Nunito_700Bold', fontSize: 14, color: '#475569' },
|
skipTxt: { fontFamily: 'Nunito_700Bold', fontSize: 14, color: '#475569' },
|
||||||
submitBtn: { flex: 2, paddingVertical: 12, borderRadius: 12, alignItems: 'center', backgroundColor: colors.brandOrange },
|
submitBtn: { flex: 2, paddingVertical: 12, borderRadius: 12, alignItems: 'center' },
|
||||||
submitTxt: { fontFamily: 'Nunito_800ExtraBold', fontSize: 14, color: '#fff' },
|
submitTxt: { fontFamily: 'Nunito_800ExtraBold', fontSize: 14, color: '#fff' },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { View, Text, ActivityIndicator } from 'react-native';
|
|||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { apiFetch } from '../../lib/api';
|
import { apiFetch } from '../../lib/api';
|
||||||
import { colors } from '../../lib/theme';
|
import { useColors } from '../../lib/theme';
|
||||||
|
|
||||||
type Emotion = 'stress' | 'sadness' | 'anger' | 'empty' | 'boredom' | 'other';
|
type Emotion = 'stress' | 'sadness' | 'anger' | 'empty' | 'boredom' | 'other';
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ function emotionLabel(key: string, t: (k: string) => string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function StatCard({ label, value, color }: { label: string; value: string; color: string }) {
|
function StatCard({ label, value, color }: { label: string; value: string; color: string }) {
|
||||||
|
const colors = useColors();
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@ -37,8 +38,8 @@ function StatCard({ label, value, color }: { label: string; value: string; color
|
|||||||
marginHorizontal: 4,
|
marginHorizontal: 4,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#f3f4f6',
|
borderColor: colors.border,
|
||||||
backgroundColor: '#fafafa',
|
backgroundColor: colors.surface,
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
@ -50,7 +51,7 @@ function StatCard({ label, value, color }: { label: string; value: string; color
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
fontFamily: 'Nunito_400Regular',
|
fontFamily: 'Nunito_400Regular',
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
color: '#6b7280',
|
color: colors.textMuted,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@ -61,6 +62,7 @@ function StatCard({ label, value, color }: { label: string; value: string; color
|
|||||||
|
|
||||||
export function UrgeStats() {
|
export function UrgeStats() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
const [logs, setLogs] = useState<UrgeLog[]>([]);
|
const [logs, setLogs] = useState<UrgeLog[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@ -175,18 +177,18 @@ export function UrgeStats() {
|
|||||||
style={{
|
style={{
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e7eb',
|
borderColor: colors.border,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
padding: 14,
|
padding: 14,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827', marginBottom: 10 }}
|
style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: colors.text, marginBottom: 10 }}
|
||||||
>
|
>
|
||||||
{t('urge.this_week')}
|
{t('urge.this_week')}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
|
||||||
<StatCard label={t('urge.total_urges')} value={String(weeklyStats.total)} color="#111827" />
|
<StatCard label={t('urge.total_urges')} value={String(weeklyStats.total)} color={colors.text} />
|
||||||
<StatCard
|
<StatCard
|
||||||
label={t('urge.overcome_count')}
|
label={t('urge.overcome_count')}
|
||||||
value={String(weeklyStats.overcome)}
|
value={String(weeklyStats.overcome)}
|
||||||
@ -207,8 +209,8 @@ export function UrgeStats() {
|
|||||||
style={{
|
style={{
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e7eb',
|
borderColor: colors.border,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
padding: 14,
|
padding: 14,
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -218,7 +220,7 @@ export function UrgeStats() {
|
|||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
marginLeft: 8,
|
marginLeft: 8,
|
||||||
color: '#374151',
|
color: colors.textMuted,
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
}}
|
}}
|
||||||
@ -232,12 +234,12 @@ export function UrgeStats() {
|
|||||||
style={{
|
style={{
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e7eb',
|
borderColor: colors.border,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
padding: 14,
|
padding: 14,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' }}>
|
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: colors.text }}>
|
||||||
{t('urge.chart_weekday_title')}
|
{t('urge.chart_weekday_title')}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ flexDirection: 'row', alignItems: 'flex-end', height: 70, marginTop: 10 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'flex-end', height: 70, marginTop: 10 }}>
|
||||||
@ -254,7 +256,7 @@ export function UrgeStats() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={{ fontFamily: 'Nunito_600SemiBold', fontSize: 10, color: '#6b7280' }}
|
style={{ fontFamily: 'Nunito_600SemiBold', fontSize: 10, color: colors.textMuted }}
|
||||||
>
|
>
|
||||||
{day.label}
|
{day.label}
|
||||||
</Text>
|
</Text>
|
||||||
@ -268,12 +270,12 @@ export function UrgeStats() {
|
|||||||
style={{
|
style={{
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e7eb',
|
borderColor: colors.border,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
padding: 14,
|
padding: 14,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' }}>
|
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: colors.text }}>
|
||||||
{t('urge.chart_time_title')}
|
{t('urge.chart_time_title')}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ marginTop: 8, gap: 8 }}>
|
<View style={{ marginTop: 8, gap: 8 }}>
|
||||||
@ -284,7 +286,7 @@ export function UrgeStats() {
|
|||||||
style={{
|
style={{
|
||||||
width: 74,
|
width: 74,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#6b7280',
|
color: colors.textMuted,
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -295,7 +297,7 @@ export function UrgeStats() {
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
height: 7,
|
height: 7,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
backgroundColor: '#e5e7eb',
|
backgroundColor: colors.surfaceElevated,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
@ -312,7 +314,7 @@ export function UrgeStats() {
|
|||||||
width: 24,
|
width: 24,
|
||||||
textAlign: 'right',
|
textAlign: 'right',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: '#6b7280',
|
color: colors.textMuted,
|
||||||
marginLeft: 6,
|
marginLeft: 6,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -328,12 +330,12 @@ export function UrgeStats() {
|
|||||||
style={{
|
style={{
|
||||||
borderRadius: 18,
|
borderRadius: 18,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e7eb',
|
borderColor: colors.border,
|
||||||
backgroundColor: '#fff',
|
backgroundColor: colors.bg,
|
||||||
padding: 14,
|
padding: 14,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: '#111827' }}>
|
<Text style={{ fontFamily: 'Nunito_700Bold', fontSize: 15, color: colors.text }}>
|
||||||
{t('urge.chart_top_emotions')}
|
{t('urge.chart_top_emotions')}
|
||||||
</Text>
|
</Text>
|
||||||
<View style={{ flexDirection: 'row', flexWrap: 'wrap', marginTop: 8 }}>
|
<View style={{ flexDirection: 'row', flexWrap: 'wrap', marginTop: 8 }}>
|
||||||
@ -341,9 +343,9 @@ export function UrgeStats() {
|
|||||||
<View
|
<View
|
||||||
key={emo}
|
key={emo}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#f3f4f6',
|
backgroundColor: colors.surfaceElevated,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#e5e7eb',
|
borderColor: colors.border,
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
paddingHorizontal: 10,
|
paddingHorizontal: 10,
|
||||||
paddingVertical: 6,
|
paddingVertical: 6,
|
||||||
@ -352,7 +354,7 @@ export function UrgeStats() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
style={{ fontSize: 12, color: '#374151', fontFamily: 'Nunito_600SemiBold' }}
|
style={{ fontSize: 12, color: colors.textMuted, fontFamily: 'Nunito_600SemiBold' }}
|
||||||
>
|
>
|
||||||
{emotionLabel(emo, t)} x{c}
|
{emotionLabel(emo, t)} x{c}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user