diff --git a/apps/mobile/app/(app)/_layout.tsx b/apps/mobile/app/(app)/_layout.tsx
index 9dff876..7456fb6 100755
--- a/apps/mobile/app/(app)/_layout.tsx
+++ b/apps/mobile/app/(app)/_layout.tsx
@@ -1,7 +1,22 @@
import { Tabs, Redirect } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
-import { View, ActivityIndicator, StyleSheet } from "react-native";
+import { View, ActivityIndicator, StyleSheet, Platform } from "react-native";
+import { BlurView } from "expo-blur";
import { useAuthStore, useMatchStore } from "../../src/store";
+import { colors, blur } from "../../src/theme";
+
+function TabBarBackground() {
+ if (Platform.OS === 'ios') {
+ return (
+
+ );
+ }
+ return ;
+}
export default function AppLayout() {
const { isAuthenticated, isLoading } = useAuthStore();
@@ -11,7 +26,7 @@ export default function AppLayout() {
if (isLoading) {
return (
-
+
);
}
@@ -25,19 +40,22 @@ export default function AppLayout() {
,
tabBarStyle: {
- backgroundColor: "#16213e",
- borderTopColor: "#0f3460",
+ position: 'absolute',
+ backgroundColor: 'transparent',
+ borderTopColor: colors.glass.border,
borderTopWidth: 1,
paddingTop: 8,
- paddingBottom: 8,
- height: 60,
+ paddingBottom: Platform.OS === 'ios' ? 28 : 8,
+ height: Platform.OS === 'ios' ? 88 : 64,
+ elevation: 0,
},
- tabBarActiveTintColor: "#e94560",
- tabBarInactiveTintColor: "#666",
+ tabBarActiveTintColor: colors.primary,
+ tabBarInactiveTintColor: colors.textTertiary,
tabBarLabelStyle: {
- fontSize: 12,
- fontWeight: "500",
+ fontSize: 11,
+ fontWeight: "600",
},
}}
>
@@ -77,8 +95,9 @@ export default function AppLayout() {
),
tabBarBadge: newMatchesCount > 0 ? newMatchesCount : undefined,
tabBarBadgeStyle: {
- backgroundColor: "#e94560",
+ backgroundColor: colors.primary,
fontSize: 10,
+ fontWeight: '600',
},
}}
/>
@@ -112,6 +131,6 @@ const styles = StyleSheet.create({
flex: 1,
justifyContent: "center",
alignItems: "center",
- backgroundColor: "#1a1a2e",
+ backgroundColor: colors.background,
},
});
diff --git a/apps/mobile/app/(app)/chat/[id].tsx b/apps/mobile/app/(app)/chat/[id].tsx
index 87314bf..0af638f 100644
--- a/apps/mobile/app/(app)/chat/[id].tsx
+++ b/apps/mobile/app/(app)/chat/[id].tsx
@@ -1,13 +1,18 @@
import { useState, useEffect, useRef } from "react";
-import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity, KeyboardAvoidingView, Platform, Image, ActivityIndicator, Alert, AppState } from "react-native";
+import { View, Text, StyleSheet, FlatList, TextInput, TouchableOpacity, KeyboardAvoidingView, Platform, Image, ActivityIndicator, Alert } from "react-native";
import { useLocalSearchParams, useRouter, Stack } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
+import { BlurView } from "expo-blur";
+import { LinearGradient } from "expo-linear-gradient";
+import Animated, { FadeIn, FadeInUp } from "react-native-reanimated";
import { supabase } from "../../../src/lib/supabase";
import { useAuthStore } from "../../../src/store";
import * as ImagePicker from "expo-image-picker";
import * as FileSystem from 'expo-file-system';
import { decode } from 'base64-arraybuffer';
import { Database } from "../../../src/types/supabase";
+import { GradientBackground, GlassCard } from "../../../src/components/ui";
+import { colors, gradients, spacing, radius, typography, blur, shadows } from "../../../src/theme";
type Message = Database["public"]["Tables"]["messages"]["Row"];
@@ -27,7 +32,6 @@ export default function ChatScreen() {
useEffect(() => {
if (!matchId) return;
- // Fetch match context (question and responses)
const fetchMatch = async () => {
const { data } = await supabase
.from("matches")
@@ -36,7 +40,6 @@ export default function ChatScreen() {
.single();
if (data) {
- // Fetch responses for both users in this couple for this question
const { data: responses } = await supabase
.from("responses")
.select("*, profiles(name)")
@@ -48,9 +51,8 @@ export default function ChatScreen() {
};
fetchMatch();
- // Fetch initial messages and mark as read
const fetchMessagesAndMarkRead = async () => {
- const { data, error } = await supabase
+ const { data } = await supabase
.from("messages")
.select("*")
.eq("match_id", matchId)
@@ -59,7 +61,6 @@ export default function ChatScreen() {
if (data) {
setMessages(data);
- // Identify unread messages from partner
const unreadIds = data
.filter(m => m.user_id !== user?.id && !m.read_at)
.map(m => m.id);
@@ -70,7 +71,6 @@ export default function ChatScreen() {
.update({ read_at: new Date().toISOString() })
.in("id", unreadIds);
- // Optimistically update local state
setMessages(prev => prev.map(m =>
unreadIds.includes(m.id) ? { ...m, read_at: new Date().toISOString() } : m
));
@@ -80,7 +80,6 @@ export default function ChatScreen() {
fetchMessagesAndMarkRead();
- // Subscribe to new messages, updates (read receipts), and typing
const channel = supabase
.channel(`chat:${matchId}`)
.on(
@@ -94,7 +93,6 @@ export default function ChatScreen() {
async (payload) => {
const newMessage = payload.new as Message;
- // If receiving a message from partner, mark as read immediately since we are in the chat
if (newMessage.user_id !== user?.id) {
await supabase
.from("messages")
@@ -104,7 +102,7 @@ export default function ChatScreen() {
}
setMessages((prev) => [newMessage, ...prev]);
- setPartnerTyping(false); // Stop typing indicator if they sent a message
+ setPartnerTyping(false);
}
)
.on(
@@ -126,7 +124,6 @@ export default function ChatScreen() {
if (payload.payload.userId !== user?.id) {
setPartnerTyping(true);
- // Auto-hide typing indicator after 3s
if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current);
typingTimeoutRef.current = setTimeout(() => {
setPartnerTyping(false);
@@ -242,92 +239,48 @@ export default function ChatScreen() {
};
const revealMessage = async (messageId: string) => {
- // Optimistic update
setMessages(prev => prev.map(m =>
m.id === messageId ? { ...m, media_viewed_at: new Date().toISOString() } : m
));
- const { error } = await supabase
+ await supabase
.from("messages")
.update({ media_viewed_at: new Date().toISOString() })
.eq("id", messageId);
-
- if (error) {
- console.error("Failed to mark as viewed", error);
- }
};
-
- const renderMessage = ({ item }: { item: Message }) => {
+ const renderMessage = ({ item, index }: { item: Message; index: number }) => {
const isMe = item.user_id === user?.id;
return (
-
-
- {item.media_path ? (
-
-
- {(!isMe && !item.media_viewed_at) && (
- revealMessage(item.id)}
- >
-
-
- Tap to reveal
-
-
- )}
-
- ) : (
- {item.content}
- )}
-
-
- {new Date(item.created_at!).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
-
- {isMe && (
-
- )}
+
+ {isMe ? (
+
+
+
+ ) : (
+
+
-
-
+ )}
+
);
};
return (
-
+
(
-
-
- {match.question.text}
- {match.question.partner_text ? ` & ${match.question.partner_text}` : ""}
-
- {match.responses && match.responses.length > 0 && (
-
- {match.responses.map((response) => (
-
- {response.profiles?.name || 'You'}: {response.answer}
-
- ))}
-
- )}
-
- ) : "Chat",
- headerStyle: { backgroundColor: "#16213e" },
- headerTintColor: "#fff",
- headerBackTitleVisible: false,
+ headerStyle: { backgroundColor: colors.backgroundLight },
+ headerTintColor: colors.text,
+ headerShadowVisible: false,
}} />
{/* Sticky top bar with question and answers */}
{match?.question && (
-
-
- {match.question.text}
- {match.question.partner_text ? ` & ${match.question.partner_text}` : ""}
-
- {match.responses && match.responses.length > 0 && (
-
- {match.responses.map((response) => (
-
- {response.user_id === user?.id ? 'You' : (response.profiles?.name || 'Partner')}: {response.answer}
-
- ))}
-
- )}
-
+
+
+
+ {match.question.text}
+ {match.question.partner_text ? ` & ${match.question.partner_text}` : ""}
+
+ {match.responses && match.responses.length > 0 && (
+
+ {match.responses.map((response: any) => (
+
+
+ {response.user_id === user?.id ? 'You' : (response.profiles?.name || 'Partner')}: {response.answer}
+
+
+ ))}
+
+ )}
+
+
)}
+
Partner is typing...
-
+
) : null
}
/>
-
-
- {uploading ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
+
-
-
+
+ ) : (
+
+
+
+ )}
+
+ );
+}
+
+function MessageContent({
+ item,
+ isMe,
+ revealMessage,
+}: {
+ item: Message;
+ isMe: boolean;
+ revealMessage: (id: string) => void;
+}) {
+ if (item.media_path) {
+ return (
+
+
+ {(!isMe && !item.media_viewed_at) && (
+ revealMessage(item.id)}
+ >
+
+
+ Tap to reveal
+
+
+ )}
+
+
+ );
+ }
+
+ return (
+ <>
+ {item.content}
+
+ >
+ );
+}
+
+function MessageMeta({ item, isMe }: { item: Message; isMe: boolean }) {
+ return (
+
+
+ {new Date(item.created_at!).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
+
+ {isMe && (
+
+ )}
+
+ );
+}
+
+function InputBar({
+ inputText,
+ uploading,
+ onChangeText,
+ onSend,
+ onPickImage,
+}: {
+ inputText: string;
+ uploading: boolean;
+ onChangeText: (text: string) => void;
+ onSend: () => void;
+ onPickImage: () => void;
+}) {
+ return (
+
+
+ {uploading ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
);
}
const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: "#1a1a2e",
- },
keyboardAvoiding: {
flex: 1,
},
listContent: {
- padding: 16,
+ padding: spacing.md,
+ paddingBottom: spacing.lg,
},
messageRow: {
- marginBottom: 16,
+ marginBottom: spacing.sm,
flexDirection: "row",
},
myMessageRow: {
@@ -424,148 +487,143 @@ const styles = StyleSheet.create({
justifyContent: "flex-start",
},
bubble: {
- maxWidth: "75%",
- padding: 12,
- borderRadius: 20,
- shadowColor: "#000",
- shadowOffset: { width: 0, height: 2 },
- shadowOpacity: 0.1,
- shadowRadius: 4,
- elevation: 2,
+ maxWidth: "78%",
+ padding: spacing.md,
+ borderRadius: radius.xl,
+ ...shadows.sm,
},
myBubble: {
- backgroundColor: "#e94560",
- borderBottomRightRadius: 4,
+ borderBottomRightRadius: radius.xs,
},
theirBubble: {
- backgroundColor: "#16213e",
+ backgroundColor: colors.glass.backgroundLight,
borderWidth: 1,
- borderColor: "#0f3460",
- borderBottomLeftRadius: 4,
+ borderColor: colors.glass.border,
+ borderBottomLeftRadius: radius.xs,
},
messageText: {
- color: "#fff",
- fontSize: 16,
- lineHeight: 22,
+ ...typography.body,
+ color: colors.text,
},
messageImage: {
width: 200,
height: 200,
- borderRadius: 12,
- backgroundColor: "rgba(0,0,0,0.2)",
+ borderRadius: radius.md,
+ backgroundColor: colors.glass.background,
},
metaContainer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "flex-end",
- marginTop: 4,
+ marginTop: spacing.xs,
},
timestamp: {
- fontSize: 10,
- },
- myTimestamp: {
- color: "rgba(255, 255, 255, 0.7)",
- },
- theirTimestamp: {
- color: "rgba(255, 255, 255, 0.4)",
+ ...typography.caption2,
+ color: "rgba(255, 255, 255, 0.6)",
},
readIcon: {
- marginLeft: 4,
+ marginLeft: spacing.xs,
+ },
+ inputWrapper: {
+ borderTopWidth: 1,
+ borderTopColor: colors.glass.border,
+ },
+ inputWrapperAndroid: {
+ backgroundColor: colors.glass.backgroundLight,
},
inputContainer: {
flexDirection: "row",
- alignItems: "center",
- padding: 12,
- paddingBottom: Platform.OS === "ios" ? 34 : 12,
- backgroundColor: "#16213e",
- borderTopWidth: 1,
- borderTopColor: "#0f3460",
+ alignItems: "flex-end",
+ padding: spacing.sm,
+ paddingBottom: Platform.OS === "ios" ? spacing.xl : spacing.sm,
},
attachButton: {
- padding: 10,
+ padding: spacing.sm,
+ marginBottom: spacing.xs,
+ },
+ inputFieldWrapper: {
+ flex: 1,
+ backgroundColor: colors.glass.background,
+ borderRadius: radius.xl,
+ borderWidth: 1,
+ borderColor: colors.glass.border,
+ marginHorizontal: spacing.sm,
},
input: {
- flex: 1,
- backgroundColor: "#0f3460",
- borderRadius: 24,
- paddingHorizontal: 16,
- paddingVertical: 10,
- color: "#fff",
- maxHeight: 120,
- marginHorizontal: 8,
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.sm,
+ color: colors.text,
fontSize: 16,
+ maxHeight: 100,
},
sendButton: {
- padding: 10,
- },
- headerTitleContainer: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: colors.glass.background,
+ justifyContent: "center",
alignItems: "center",
- maxWidth: 200,
+ marginBottom: spacing.xs,
},
- headerTitle: {
- color: "#fff",
- fontWeight: "bold",
- fontSize: 16,
- textAlign: "center",
- },
- answersContainer: {
- marginTop: 4,
- alignItems: "center",
- },
- answerText: {
- color: "rgba(255,255,255,0.8)",
- fontSize: 12,
- marginBottom: 2,
+ sendButtonActive: {
+ backgroundColor: colors.primary,
},
typingContainer: {
- paddingHorizontal: 16,
- paddingVertical: 8,
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.sm,
},
typingText: {
- color: "#666",
+ ...typography.caption1,
+ color: colors.textTertiary,
fontStyle: "italic",
- fontSize: 12,
},
revealOverlay: {
...StyleSheet.absoluteFillObject,
justifyContent: "center",
alignItems: "center",
- backgroundColor: "rgba(0,0,0,0.3)",
- borderRadius: 12,
+ backgroundColor: "rgba(0,0,0,0.4)",
+ borderRadius: radius.md,
},
revealContent: {
alignItems: "center",
backgroundColor: "rgba(0,0,0,0.6)",
- padding: 12,
- borderRadius: 12,
+ padding: spacing.md,
+ borderRadius: radius.md,
},
revealText: {
- color: "#fff",
- fontSize: 12,
- fontWeight: "bold",
- marginTop: 4,
+ ...typography.caption1,
+ color: colors.text,
+ fontWeight: "600",
+ marginTop: spacing.xs,
},
stickyTopBar: {
- backgroundColor: "#16213e",
- borderBottomWidth: 1,
- borderBottomColor: "#0f3460",
- padding: 16,
- paddingTop: 12,
- paddingBottom: 12,
+ marginHorizontal: spacing.md,
+ marginTop: spacing.sm,
+ marginBottom: spacing.xs,
+ },
+ stickyContent: {
+ padding: spacing.md,
},
stickyQuestionText: {
- color: "#fff",
- fontSize: 16,
- fontWeight: "bold",
+ ...typography.subhead,
+ color: colors.text,
+ fontWeight: "600",
textAlign: "center",
- marginBottom: 8,
+ marginBottom: spacing.sm,
},
stickyAnswersContainer: {
- alignItems: "center",
+ flexDirection: "row",
+ justifyContent: "center",
+ gap: spacing.sm,
+ },
+ answerBadge: {
+ backgroundColor: colors.primaryLight,
+ paddingHorizontal: spacing.sm,
+ paddingVertical: spacing.xs,
+ borderRadius: radius.sm,
},
stickyAnswerText: {
- color: "rgba(255,255,255,0.8)",
- fontSize: 14,
- marginBottom: 4,
- }
+ ...typography.caption1,
+ color: colors.text,
+ },
});
diff --git a/apps/mobile/app/(app)/index.tsx b/apps/mobile/app/(app)/index.tsx
index ada08aa..b101702 100755
--- a/apps/mobile/app/(app)/index.tsx
+++ b/apps/mobile/app/(app)/index.tsx
@@ -1,8 +1,12 @@
import { useEffect } from "react";
-import { View, Text, StyleSheet, TouchableOpacity, ScrollView } from "react-native";
+import { View, Text, StyleSheet, TouchableOpacity, ScrollView, Platform } from "react-native";
+import Animated, { FadeInDown, FadeInRight } from "react-native-reanimated";
+import { LinearGradient } from "expo-linear-gradient";
import { useAuthStore, useMatchStore, usePacksStore } from "../../src/store";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
+import { GradientBackground, GlassCard, GlassButton } from "../../src/components/ui";
+import { colors, gradients, spacing, radius, typography, shadows } from "../../src/theme";
export default function HomeScreen() {
const { user, partner } = useAuthStore();
@@ -17,190 +21,275 @@ export default function HomeScreen() {
const recentMatches = matches.slice(0, 3);
return (
-
-
-
- Hey, {user?.name || "there"} 👋
-
- {partner && (
-
- Paired with {partner.name || partner.email || 'your partner'}
-
- )}
-
-
- {/* Quick Stats */}
-
-
-
- {matches.length}
- Matches
-
-
-
- {packs.length}
- Packs
-
-
-
- {newMatchesCount}
- New
-
-
-
- {/* Start Playing CTA */}
- router.push("/(app)/swipe")}
+
+
-
- Ready to explore?
-
- Swipe through questions and discover what you both enjoy
-
-
-
-
-
- {/* Recent Matches */}
- {recentMatches.length > 0 && (
-
-
- Recent Matches
- router.push("/(app)/matches")}>
- See all
-
-
- {recentMatches.map((match) => (
-
-
-
-
-
- {(match as any).question?.text || "A new match!"}
+ {/* Header */}
+
+
+
+ Hey, {user?.name || "there"}
+
+ {partner && (
+
+ Paired with {partner.name || partner.email || 'your partner'}
- {match.is_new && }
+ )}
+
+ router.push("/(app)/profile")}
+ activeOpacity={0.7}
+ >
+
+ {user?.name?.[0]?.toUpperCase() || "U"}
+
+
+
+
+ {/* Quick Stats */}
+
+
+
+
- ))}
-
- )}
-
+ {matches.length}
+ Matches
+
+
+
+
+
+ {packs.length}
+ Packs
+
+
+ 0 && styles.statIconHighlight]}>
+ 0 ? colors.text : colors.primary} />
+
+ {newMatchesCount}
+ New
+
+
+
+ {/* Start Playing CTA */}
+
+ router.push("/(app)/swipe")}
+ activeOpacity={0.9}
+ >
+
+
+ Ready to explore?
+
+ Swipe through questions and discover what you both enjoy
+
+
+
+
+
+
+
+
+
+ {/* Recent Matches */}
+ {recentMatches.length > 0 && (
+
+
+ Recent Matches
+ router.push("/(app)/matches")}>
+ See all
+
+
+ {recentMatches.map((match, index) => (
+
+ router.push(`/chat/${match.id}`)}
+ activeOpacity={0.7}
+ >
+
+
+
+
+
+ {(match as any).question?.text || "A new match!"}
+
+ {match.is_new && }
+
+
+
+ ))}
+
+ )}
+
+ {/* Bottom spacing for tab bar */}
+
+
+
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#1a1a2e",
+ },
+ contentContainer: {
+ paddingBottom: Platform.OS === 'ios' ? 100 : 80,
},
header: {
paddingTop: 60,
- paddingHorizontal: 24,
- paddingBottom: 24,
+ paddingHorizontal: spacing.lg,
+ paddingBottom: spacing.lg,
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
},
greeting: {
- fontSize: 28,
- fontWeight: "bold",
- color: "#fff",
+ ...typography.title1,
+ color: colors.text,
},
partnerText: {
- fontSize: 14,
- color: "#888",
- marginTop: 4,
+ ...typography.subhead,
+ color: colors.textSecondary,
+ marginTop: spacing.xs,
+ },
+ profileButton: {
+ width: 44,
+ height: 44,
+ borderRadius: 22,
+ backgroundColor: colors.primary,
+ justifyContent: 'center',
+ alignItems: 'center',
+ ...shadows.md,
+ },
+ profileButtonText: {
+ ...typography.headline,
+ color: colors.text,
},
statsContainer: {
flexDirection: "row",
- paddingHorizontal: 24,
- gap: 12,
+ paddingHorizontal: spacing.lg,
+ gap: spacing.sm,
},
statCard: {
flex: 1,
- backgroundColor: "#16213e",
- borderRadius: 16,
- padding: 16,
alignItems: "center",
- borderWidth: 1,
- borderColor: "#0f3460",
+ paddingVertical: spacing.md,
+ },
+ statIconContainer: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: colors.primaryLight,
+ justifyContent: "center",
+ alignItems: "center",
+ marginBottom: spacing.sm,
+ },
+ statIconHighlight: {
+ backgroundColor: colors.primary,
},
statNumber: {
- fontSize: 24,
- fontWeight: "bold",
- color: "#fff",
- marginTop: 8,
+ ...typography.title2,
+ color: colors.text,
},
statLabel: {
- fontSize: 12,
- color: "#888",
- marginTop: 4,
+ ...typography.caption1,
+ color: colors.textSecondary,
+ marginTop: spacing.xs,
},
ctaCard: {
- margin: 24,
- backgroundColor: "#e94560",
- borderRadius: 16,
- padding: 20,
+ margin: spacing.lg,
+ borderRadius: radius.xl,
+ padding: spacing.lg,
flexDirection: "row",
alignItems: "center",
+ ...shadows.lg,
},
ctaContent: {
flex: 1,
},
ctaTitle: {
- fontSize: 18,
- fontWeight: "bold",
- color: "#fff",
+ ...typography.title3,
+ color: colors.text,
},
ctaSubtitle: {
- fontSize: 14,
+ ...typography.subhead,
color: "rgba(255,255,255,0.8)",
- marginTop: 4,
+ marginTop: spacing.xs,
+ },
+ ctaIconContainer: {
+ width: 44,
+ height: 44,
+ borderRadius: 22,
+ backgroundColor: "rgba(255,255,255,0.2)",
+ justifyContent: "center",
+ alignItems: "center",
+ marginLeft: spacing.md,
},
section: {
- paddingHorizontal: 24,
- marginBottom: 24,
+ paddingHorizontal: spacing.lg,
+ marginTop: spacing.sm,
},
sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
- marginBottom: 16,
+ marginBottom: spacing.md,
},
sectionTitle: {
- fontSize: 18,
- fontWeight: "600",
- color: "#fff",
+ ...typography.headline,
+ color: colors.text,
},
seeAll: {
- color: "#e94560",
- fontSize: 14,
+ ...typography.subhead,
+ color: colors.primary,
},
matchItem: {
flexDirection: "row",
alignItems: "center",
- backgroundColor: "#16213e",
- borderRadius: 12,
- padding: 16,
- marginBottom: 8,
- borderWidth: 1,
- borderColor: "#0f3460",
+ marginBottom: spacing.sm,
},
matchIcon: {
- width: 40,
- height: 40,
- borderRadius: 20,
- backgroundColor: "rgba(233, 69, 96, 0.2)",
+ width: 36,
+ height: 36,
+ borderRadius: 18,
+ backgroundColor: colors.primaryLight,
justifyContent: "center",
alignItems: "center",
- marginRight: 12,
+ marginRight: spacing.md,
},
matchText: {
flex: 1,
- color: "#fff",
- fontSize: 14,
+ ...typography.body,
+ color: colors.text,
},
newBadge: {
width: 8,
height: 8,
borderRadius: 4,
- backgroundColor: "#e94560",
+ backgroundColor: colors.primary,
+ },
+ bottomSpacer: {
+ height: spacing.lg,
},
});
diff --git a/apps/mobile/app/(app)/matches.tsx b/apps/mobile/app/(app)/matches.tsx
index 97e2523..c219938 100755
--- a/apps/mobile/app/(app)/matches.tsx
+++ b/apps/mobile/app/(app)/matches.tsx
@@ -1,27 +1,28 @@
-import { View, Text, StyleSheet, FlatList, RefreshControl, TouchableOpacity } from "react-native";
+import { View, Text, StyleSheet, FlatList, RefreshControl, TouchableOpacity, ActivityIndicator, Platform } from "react-native";
import { useMatchStore, useAuthStore } from "../../src/store";
import { useEffect } from "react";
import { Ionicons } from "@expo/vector-icons";
import { useRouter } from "expo-router";
+import Animated, { FadeInDown, FadeInRight } from "react-native-reanimated";
+import { GradientBackground, GlassCard } from "../../src/components/ui";
+import { colors, spacing, typography, radius } from "../../src/theme";
export default function MatchesScreen() {
const { matches, fetchMatches, markAllAsSeen } = useMatchStore();
const { user } = useAuthStore();
+ const router = useRouter();
useEffect(() => {
fetchMatches();
}, []);
- // Mark all matches as seen when matches are loaded
useEffect(() => {
if (matches.length > 0) {
markAllAsSeen();
}
}, [matches.length]);
- const router = useRouter();
-
- const renderItem = ({ item }: { item: any }) => {
+ const renderItem = ({ item, index }: { item: any; index: number }) => {
const userResponse = item.responses?.find((r: any) => r.user_id === user?.id);
const partnerResponse = item.responses?.find((r: any) => r.user_id !== user?.id);
@@ -32,161 +33,220 @@ export default function MatchesScreen() {
const userTime = new Date(userResponse.created_at).getTime();
const partnerTime = new Date(partnerResponse.created_at).getTime();
- // If user answered after partner, they saw the partner_text
if (userTime > partnerTime) {
userText = item.question.partner_text;
partnerText = item.question.text;
}
}
+ const isYesYes = item.match_type === "yes_yes";
+
return (
- router.push(`/chat/${item.id}`)}
- activeOpacity={0.7}
- >
-
-
-
-
- {userText}
- {item.question.partner_text && (
- Partner: {partnerText}
- )}
-
-
-
- {item.match_type === "yes_yes" ? "Review: YES + YES" : "Review: YES + MAYBE"}
-
+
+ router.push(`/chat/${item.id}`)}
+ activeOpacity={0.7}
+ >
+
+
+
-
- {new Date(item.created_at).toLocaleDateString()}
-
-
-
-
+
+
+ {userText}
+
+ {item.question.partner_text && (
+
+ Partner: {partnerText}
+
+ )}
+
+
+
+ {isYesYes ? "YES + YES" : "YES + MAYBE"}
+
+
+
+ {new Date(item.created_at).toLocaleDateString()}
+
+
+
+
+
+
+
+
+
);
};
if (!user) {
return (
-
+
-
+
-
+
);
}
return (
-
-
- Your Matches
-
+
+
+
+ Your Matches
+
+ {matches.length} {matches.length === 1 ? 'match' : 'matches'} found
+
+
- item.id}
- contentContainerStyle={styles.list}
- refreshControl={
-
- }
- ListEmptyComponent={
-
- No matches yet. Keep swiping!
-
- }
- />
-
+ item.id}
+ contentContainerStyle={styles.list}
+ showsVerticalScrollIndicator={false}
+ refreshControl={
+
+ }
+ ListEmptyComponent={
+
+
+
+
+ No matches yet
+
+ Keep swiping to find things you both enjoy!
+
+
+ }
+ />
+
+
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#1a1a2e",
- },
- header: {
- paddingTop: 60,
- paddingHorizontal: 24,
- paddingBottom: 24,
- borderBottomWidth: 1,
- borderBottomColor: "#16213e",
- },
- title: {
- fontSize: 28,
- fontWeight: "bold",
- color: "#fff",
- },
- list: {
- padding: 24,
- },
- matchCard: {
- flexDirection: "row",
- backgroundColor: "#16213e",
- borderRadius: 16,
- padding: 16,
- marginBottom: 16,
- borderWidth: 1,
- borderColor: "#0f3460",
- },
- iconContainer: {
- width: 48,
- height: 48,
- borderRadius: 24,
- backgroundColor: "rgba(233, 69, 96, 0.1)",
- justifyContent: "center",
- alignItems: "center",
- marginRight: 16,
- },
- content: {
- flex: 1,
- },
- questionText: {
- fontSize: 16,
- fontWeight: "600",
- color: "#fff",
- marginBottom: 8,
- lineHeight: 22,
- },
- tagContainer: {
- flexDirection: "row",
- alignItems: "center",
- justifyContent: "space-between",
- },
- tag: {
- backgroundColor: "rgba(233, 69, 96, 0.2)",
- paddingHorizontal: 8,
- paddingVertical: 4,
- borderRadius: 4,
- },
- tagText: {
- color: "#e94560",
- fontSize: 12,
- fontWeight: "bold",
- },
- date: {
- color: "#666",
- fontSize: 12,
- },
- emptyContainer: {
- padding: 40,
- alignItems: "center",
- },
- emptyText: {
- color: "#666",
- fontSize: 16,
- },
- partnerText: {
- fontSize: 14,
- color: "#aaa",
- fontStyle: "italic",
- marginBottom: 8,
},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
+ header: {
+ paddingTop: 60,
+ paddingHorizontal: spacing.lg,
+ paddingBottom: spacing.md,
+ },
+ title: {
+ ...typography.title1,
+ color: colors.text,
+ },
+ subtitle: {
+ ...typography.subhead,
+ color: colors.textSecondary,
+ marginTop: spacing.xs,
+ },
+ list: {
+ padding: spacing.lg,
+ paddingBottom: Platform.OS === 'ios' ? 120 : 100,
+ },
+ matchCard: {
+ flexDirection: "row",
+ alignItems: "center",
+ marginBottom: spacing.sm,
+ },
+ iconContainer: {
+ width: 48,
+ height: 48,
+ borderRadius: 24,
+ backgroundColor: colors.primaryLight,
+ justifyContent: "center",
+ alignItems: "center",
+ marginRight: spacing.md,
+ },
+ content: {
+ flex: 1,
+ },
+ questionText: {
+ ...typography.body,
+ color: colors.text,
+ fontWeight: "600",
+ marginBottom: spacing.xs,
+ },
+ partnerText: {
+ ...typography.subhead,
+ color: colors.textSecondary,
+ fontStyle: "italic",
+ marginBottom: spacing.sm,
+ },
+ metaContainer: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ },
+ tag: {
+ backgroundColor: colors.glass.background,
+ paddingHorizontal: spacing.sm,
+ paddingVertical: spacing.xs,
+ borderRadius: radius.xs,
+ },
+ tagHighlight: {
+ backgroundColor: colors.primaryLight,
+ },
+ tagText: {
+ ...typography.caption2,
+ color: colors.textSecondary,
+ fontWeight: "600",
+ },
+ tagTextHighlight: {
+ color: colors.primary,
+ },
+ date: {
+ ...typography.caption1,
+ color: colors.textTertiary,
+ },
+ chevronContainer: {
+ marginLeft: spacing.sm,
+ },
+ emptyContainer: {
+ alignItems: "center",
+ paddingVertical: spacing.xxl,
+ },
+ emptyIconContainer: {
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ backgroundColor: colors.glass.background,
+ justifyContent: "center",
+ alignItems: "center",
+ marginBottom: spacing.md,
+ },
+ emptyTitle: {
+ ...typography.title3,
+ color: colors.text,
+ marginBottom: spacing.xs,
+ },
+ emptyText: {
+ ...typography.body,
+ color: colors.textSecondary,
+ textAlign: "center",
+ },
});
diff --git a/apps/mobile/app/(app)/packs.tsx b/apps/mobile/app/(app)/packs.tsx
index 3a42222..ce158fa 100755
--- a/apps/mobile/app/(app)/packs.tsx
+++ b/apps/mobile/app/(app)/packs.tsx
@@ -1,8 +1,11 @@
-import { View, Text, StyleSheet, FlatList, TouchableOpacity, Image, Switch } from "react-native";
+import { View, Text, StyleSheet, FlatList, TouchableOpacity, Switch, RefreshControl, Platform } from "react-native";
import { usePacksStore } from "../../src/store";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { useEffect } from "react";
+import Animated, { FadeInDown, FadeInRight } from "react-native-reanimated";
+import { GradientBackground, GlassCard } from "../../src/components/ui";
+import { colors, spacing, typography, radius } from "../../src/theme";
export default function PacksScreen() {
const { packs, enabledPackIds, togglePack, isLoading, fetchPacks } = usePacksStore();
@@ -11,86 +14,132 @@ export default function PacksScreen() {
fetchPacks();
}, []);
- const renderItem = ({ item }: { item: any }) => (
-
- router.push(`/pack/${item.id}`)}
- >
-
- {item.icon || "📦"}
-
-
-
- {item.name}
- {item.is_premium && (
-
- PRO
+ const enabledCount = packs.filter(p => enabledPackIds.includes(p.id)).length;
+
+ const renderItem = ({ item, index }: { item: any; index: number }) => {
+ const isEnabled = enabledPackIds.includes(item.id);
+
+ return (
+
+
+ router.push(`/pack/${item.id}`)}
+ >
+
+ {item.icon || "📦"}
+
+
+
+ {item.name}
+ {item.is_premium && (
+
+
+ PRO
+
+ )}
- )}
+
+ {item.description}
+
+ {item.question_count && (
+
+ {item.question_count} questions
+
+ )}
+
+
+
+ togglePack(item.id)}
+ trackColor={{
+ false: colors.glass.background,
+ true: colors.primary,
+ }}
+ thumbColor={colors.text}
+ ios_backgroundColor={colors.glass.background}
+ />
-
- {item.description}
-
-
-
- togglePack(item.id)}
- trackColor={{ false: "#767577", true: "#e94560" }}
- thumbColor={"#fff"}
- />
-
- );
+
+
+ );
+ };
return (
-
-
- Question Packs
-
+
+
+
+ Question Packs
+
+ {enabledCount} of {packs.length} packs enabled
+
+
- item.id}
- contentContainerStyle={styles.list}
- refreshing={isLoading}
- onRefresh={fetchPacks}
- />
-
+ item.id}
+ contentContainerStyle={styles.list}
+ showsVerticalScrollIndicator={false}
+ refreshControl={
+
+ }
+ ListEmptyComponent={
+
+
+
+
+ No packs available
+
+ Check back later for new question packs!
+
+
+ }
+ />
+
+
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#1a1a2e",
},
header: {
paddingTop: 60,
- paddingHorizontal: 24,
- paddingBottom: 24,
- borderBottomWidth: 1,
- borderBottomColor: "#16213e",
+ paddingHorizontal: spacing.lg,
+ paddingBottom: spacing.md,
},
title: {
- fontSize: 28,
- fontWeight: "bold",
- color: "#fff",
+ ...typography.title1,
+ color: colors.text,
+ },
+ subtitle: {
+ ...typography.subhead,
+ color: colors.textSecondary,
+ marginTop: spacing.xs,
},
list: {
- padding: 24,
+ padding: spacing.lg,
+ paddingBottom: Platform.OS === 'ios' ? 120 : 100,
},
card: {
flexDirection: "row",
alignItems: "center",
- backgroundColor: "#16213e",
- borderRadius: 16,
- padding: 16,
- marginBottom: 16,
- borderWidth: 1,
- borderColor: "#0f3460",
+ marginBottom: spacing.sm,
},
cardContent: {
flex: 1,
@@ -98,46 +147,82 @@ const styles = StyleSheet.create({
alignItems: "center",
},
iconContainer: {
- width: 48,
- height: 48,
- borderRadius: 24,
- backgroundColor: "rgba(255, 255, 255, 0.05)",
+ width: 52,
+ height: 52,
+ borderRadius: 26,
+ backgroundColor: colors.glass.background,
justifyContent: "center",
alignItems: "center",
- marginRight: 16,
+ marginRight: spacing.md,
+ },
+ iconContainerActive: {
+ backgroundColor: colors.primaryLight,
},
emoji: {
- fontSize: 24,
+ fontSize: 26,
},
content: {
flex: 1,
- marginRight: 16,
+ marginRight: spacing.sm,
},
headerRow: {
flexDirection: "row",
alignItems: "center",
- marginBottom: 4,
+ marginBottom: spacing.xs,
},
name: {
- fontSize: 18,
- fontWeight: "600",
- color: "#fff",
- marginRight: 8,
+ ...typography.headline,
+ color: colors.text,
+ marginRight: spacing.sm,
},
badge: {
- backgroundColor: "#e94560",
- paddingHorizontal: 6,
+ flexDirection: "row",
+ alignItems: "center",
+ backgroundColor: colors.primary,
+ paddingHorizontal: spacing.sm,
paddingVertical: 2,
- borderRadius: 4,
+ borderRadius: radius.xs,
+ gap: 2,
},
badgeText: {
- color: "#fff",
- fontSize: 10,
- fontWeight: "bold",
+ ...typography.caption2,
+ color: colors.text,
+ fontWeight: "700",
},
description: {
- fontSize: 14,
- color: "#888",
+ ...typography.subhead,
+ color: colors.textSecondary,
lineHeight: 20,
},
+ questionCount: {
+ ...typography.caption1,
+ color: colors.textTertiary,
+ marginTop: spacing.xs,
+ },
+ switchContainer: {
+ paddingLeft: spacing.sm,
+ },
+ emptyContainer: {
+ alignItems: "center",
+ paddingVertical: spacing.xxl,
+ },
+ emptyIconContainer: {
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ backgroundColor: colors.glass.background,
+ justifyContent: "center",
+ alignItems: "center",
+ marginBottom: spacing.md,
+ },
+ emptyTitle: {
+ ...typography.title3,
+ color: colors.text,
+ marginBottom: spacing.xs,
+ },
+ emptyText: {
+ ...typography.body,
+ color: colors.textSecondary,
+ textAlign: "center",
+ },
});
diff --git a/apps/mobile/app/(app)/profile.tsx b/apps/mobile/app/(app)/profile.tsx
index 25b4ee1..c61ff26 100755
--- a/apps/mobile/app/(app)/profile.tsx
+++ b/apps/mobile/app/(app)/profile.tsx
@@ -1,7 +1,12 @@
-import { View, Text, StyleSheet, TouchableOpacity, Alert, Image } from "react-native";
+import { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView, Platform } from "react-native";
import { useAuthStore } from "../../src/store";
import { Ionicons } from "@expo/vector-icons";
+import { LinearGradient } from "expo-linear-gradient";
+import Animated, { FadeInDown } from "react-native-reanimated";
import { supabase } from "../../src/lib/supabase";
+import { router } from "expo-router";
+import { GradientBackground, GlassCard, GlassButton } from "../../src/components/ui";
+import { colors, gradients, spacing, radius, typography, shadows } from "../../src/theme";
export default function ProfileScreen() {
const { user, partner, signOut, fetchCouple } = useAuthStore();
@@ -20,7 +25,7 @@ export default function ProfileScreen() {
await supabase.functions.invoke("manage-couple", {
method: "DELETE",
});
- await fetchCouple(); // Refresh state
+ await fetchCouple();
} catch (error) {
Alert.alert("Error", "Failed to unpair");
}
@@ -30,139 +35,269 @@ export default function ProfileScreen() {
);
};
+ const handleSignOut = () => {
+ Alert.alert(
+ "Sign Out",
+ "Are you sure you want to sign out?",
+ [
+ { text: "Cancel", style: "cancel" },
+ {
+ text: "Sign Out",
+ style: "destructive",
+ onPress: signOut,
+ },
+ ]
+ );
+ };
+
return (
-
-
- Profile
-
+
+
+ {/* Header */}
+
+ Profile
+
-
-
-
-
- {user?.name?.[0]?.toUpperCase() || "U"}
-
-
- {user?.name || "User"}
- {user?.email}
-
-
- Relationship
-
- {partner ? (
-
-
-
- Paired with {partner.name || partner.email || 'your partner'}
+ {/* Profile Card */}
+
+
+
+
+
+ {user?.name?.[0]?.toUpperCase() || "U"}
+
-
-
+
+ {user?.name || "User"}
+ {user?.email}
+
+
+
+ {/* Relationship Section */}
+
+ Relationship
+
+ {partner ? (
+
+
+
+
+
+
+ Paired with
+
+ {partner.name || partner.email || 'your partner'}
+
+
+
+
+
+
+
+ ) : (
+ router.push("/(app)/pairing")}
+ activeOpacity={0.7}
+ >
+
+
+
+
+
+ Not paired yet
+ Tap to connect with your partner
+
+
+
-
- ) : (
-
- Not paired yet
-
- )}
-
+ )}
+
+
- Settings
-
-
- Sign Out
-
-
-
+ {/* Settings Section */}
+
+ Account
+
+
+
+
+
+
+ Sign Out
+
+
+
+
+
- Sauci v1.0.0
-
-
+ {/* Version */}
+
+ Sauci v1.0.0
+ Made with love
+
+
+ {/* Bottom spacing for tab bar */}
+
+
+
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#1a1a2e",
+ },
+ contentContainer: {
+ paddingBottom: Platform.OS === 'ios' ? 100 : 80,
},
header: {
paddingTop: 60,
- paddingHorizontal: 24,
- paddingBottom: 24,
- borderBottomWidth: 1,
- borderBottomColor: "#16213e",
+ paddingHorizontal: spacing.lg,
+ paddingBottom: spacing.md,
},
title: {
- fontSize: 28,
- fontWeight: "bold",
- color: "#fff",
- },
- content: {
- padding: 24,
+ ...typography.title1,
+ color: colors.text,
},
profileCard: {
+ marginHorizontal: spacing.lg,
alignItems: "center",
- marginBottom: 40,
+ paddingVertical: spacing.xl,
+ marginBottom: spacing.lg,
+ },
+ avatarContainer: {
+ width: 88,
+ height: 88,
+ borderRadius: 44,
+ justifyContent: "center",
+ alignItems: "center",
+ marginBottom: spacing.md,
+ ...shadows.lg,
},
avatar: {
width: 80,
height: 80,
borderRadius: 40,
- backgroundColor: "#e94560",
+ backgroundColor: colors.background,
justifyContent: "center",
alignItems: "center",
- marginBottom: 16,
},
avatarText: {
fontSize: 32,
fontWeight: "bold",
- color: "#fff",
+ color: colors.text,
},
name: {
- fontSize: 24,
- fontWeight: "bold",
- color: "#fff",
- marginBottom: 4,
+ ...typography.title2,
+ color: colors.text,
+ marginBottom: spacing.xs,
},
email: {
- fontSize: 14,
- color: "#666",
+ ...typography.subhead,
+ color: colors.textSecondary,
},
sectionTitle: {
- fontSize: 14,
- fontWeight: "600",
- color: "#666",
- marginBottom: 8,
- marginLeft: 4,
+ ...typography.caption1,
+ color: colors.textTertiary,
textTransform: "uppercase",
- },
- section: {
- backgroundColor: "#16213e",
- borderRadius: 12,
- marginBottom: 24,
- overflow: "hidden",
+ letterSpacing: 1,
+ marginBottom: spacing.sm,
+ marginLeft: spacing.lg + spacing.xs,
},
row: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
- padding: 16,
- borderBottomWidth: 1,
- borderBottomColor: "#1a1a2e",
+ padding: spacing.md,
},
- partnerInfo: {
+ rowLeft: {
flexDirection: "row",
alignItems: "center",
- gap: 12,
+ flex: 1,
},
- rowText: {
- fontSize: 16,
- color: "#fff",
+ rowIconContainer: {
+ width: 40,
+ height: 40,
+ borderRadius: 20,
+ backgroundColor: colors.primaryLight,
+ justifyContent: "center",
+ alignItems: "center",
+ marginRight: spacing.md,
+ },
+ rowIconInactive: {
+ backgroundColor: colors.glass.background,
+ },
+ rowIconDanger: {
+ backgroundColor: colors.errorLight,
+ },
+ rowLabel: {
+ ...typography.subhead,
+ color: colors.textSecondary,
+ },
+ rowValue: {
+ ...typography.body,
+ color: colors.text,
+ fontWeight: "600",
+ },
+ rowValueMuted: {
+ ...typography.caption1,
+ color: colors.textTertiary,
+ },
+ rowLabelDanger: {
+ ...typography.body,
+ color: colors.error,
+ fontWeight: "500",
+ },
+ rowAction: {
+ padding: spacing.sm,
+ },
+ menuRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ padding: spacing.md,
+ },
+ menuRowDanger: {
+ borderRadius: radius.lg,
+ },
+ versionContainer: {
+ alignItems: "center",
+ marginTop: spacing.xl,
},
version: {
- textAlign: "center",
- color: "#666",
- fontSize: 12,
- marginTop: 24,
+ ...typography.caption1,
+ color: colors.textTertiary,
+ },
+ versionSub: {
+ ...typography.caption2,
+ color: colors.textTertiary,
+ marginTop: spacing.xs,
+ },
+ bottomSpacer: {
+ height: spacing.lg,
},
});
diff --git a/apps/mobile/app/(app)/swipe.tsx b/apps/mobile/app/(app)/swipe.tsx
index 659ef5c..7a04e3d 100755
--- a/apps/mobile/app/(app)/swipe.tsx
+++ b/apps/mobile/app/(app)/swipe.tsx
@@ -1,9 +1,12 @@
import { useState, useEffect } from "react";
-import { View, StyleSheet, Text, ActivityIndicator } from "react-native";
+import { View, StyleSheet, Text, ActivityIndicator, Platform } from "react-native";
import { useLocalSearchParams } from "expo-router";
import { Ionicons } from "@expo/vector-icons";
+import Animated, { FadeIn, FadeInUp } from "react-native-reanimated";
import { supabase } from "../../src/lib/supabase";
import SwipeCard from "../../src/components/SwipeCard";
+import { GradientBackground, GlassCard, GlassButton } from "../../src/components/ui";
+import { colors, spacing, typography, radius, shadows } from "../../src/theme";
export default function SwipeScreen() {
const { packId } = useLocalSearchParams();
@@ -26,11 +29,6 @@ export default function SwipeScreen() {
throw error;
}
- // Smart Shuffle
- // Priority:
- // 1. Completion of Two-Part Match (Partner answered, is_two_part = true) -> +1.5
- // 2. Partner Answered (Any) -> +0.7
- // 3. New Two-Part (Get someone to start) -> +0.4
const sorted = (data || []).sort((a: any, b: any) => {
let scoreA = Math.random();
let scoreB = Math.random();
@@ -58,23 +56,18 @@ export default function SwipeScreen() {
const question = questions[currentIndex];
const answer = direction === "right" ? "yes" : direction === "left" ? "no" : "maybe";
- // Optimistic update
setCurrentIndex(prev => prev + 1);
try {
- // DEBUG: Check if we have a valid session
const { data: { session } } = await supabase.auth.getSession();
console.log("DEBUG - Session exists:", !!session);
- console.log("DEBUG - Access token exists:", !!session?.access_token);
- console.log("DEBUG - Token preview:", session?.access_token?.substring(0, 50) + "...");
- const result = await supabase.functions.invoke("submit-response", {
+ await supabase.functions.invoke("submit-response", {
body: {
question_id: question.id,
answer,
},
});
- console.log("DEBUG - Function result:", result);
} catch (error) {
console.error("Failed to submit response", error);
}
@@ -82,94 +75,178 @@ export default function SwipeScreen() {
if (isLoading) {
return (
-
-
-
+
+
+
+
+
);
}
if (currentIndex >= questions.length) {
return (
-
-
- All caught up!
-
- Check back later for more questions or try a different pack.
-
-
+
+
+
+
+
+
+ All caught up!
+
+ Check back later for more questions or try a different pack.
+
+
+ Refresh Questions
+
+
+
+
);
}
return (
-
-
-
- Question {currentIndex + 1} of {questions.length}
-
-
+
+
+ {/* Header */}
+
+
+
+ {currentIndex + 1} of {questions.length}
+
+
+
+
+
+
-
- {/* Render next card in background for smoothness */}
- {questions[currentIndex + 1] && (
-
- )}
+ {/* Card Stack */}
+
+ {/* Background card (next) */}
+ {questions[currentIndex + 1] && (
+
+ )}
- {/* Active card */}
-
+ {/* Active card */}
+
+
+
+ {/* Hint */}
+
+ Swipe or tap to answer
+
+
+ {/* Bottom spacing for tab bar */}
+
-
+
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#1a1a2e",
+ },
+ centerContainer: {
+ flex: 1,
justifyContent: "center",
alignItems: "center",
},
header: {
- position: "absolute",
- top: 60,
- zIndex: 1,
+ paddingTop: 60,
+ paddingHorizontal: spacing.lg,
+ paddingBottom: spacing.md,
},
- progress: {
- color: "#666",
- fontSize: 16,
- fontWeight: "600",
+ progressContainer: {
+ alignItems: "center",
+ },
+ progressText: {
+ ...typography.subhead,
+ color: colors.textSecondary,
+ marginBottom: spacing.sm,
+ },
+ progressBar: {
+ width: 120,
+ height: 4,
+ backgroundColor: colors.glass.background,
+ borderRadius: 2,
+ overflow: "hidden",
+ },
+ progressFill: {
+ height: "100%",
+ backgroundColor: colors.primary,
+ borderRadius: 2,
},
cardContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
- marginTop: 40,
},
backgroundCard: {
position: "absolute",
- width: "100%", // Match SwipeCard width logic
- height: 500,
- backgroundColor: "#16213e",
- borderRadius: 24,
+ width: "85%",
+ height: 480,
+ backgroundColor: colors.glass.backgroundLight,
+ borderRadius: radius.xxl,
borderWidth: 1,
- borderColor: "#0f3460",
- transform: [{ scale: 0.95 }, { translateY: 10 }],
+ borderColor: colors.glass.border,
+ transform: [{ scale: 0.95 }, { translateY: 12 }],
opacity: 0.5,
+ ...shadows.md,
+ },
+ hintContainer: {
+ alignItems: "center",
+ paddingBottom: spacing.md,
+ },
+ hintText: {
+ ...typography.caption1,
+ color: colors.textTertiary,
+ },
+ emptyContent: {
+ alignItems: "center",
+ paddingHorizontal: spacing.xl,
+ },
+ emptyIconContainer: {
+ width: 120,
+ height: 120,
+ borderRadius: 60,
+ backgroundColor: colors.primaryLight,
+ justifyContent: "center",
+ alignItems: "center",
+ marginBottom: spacing.lg,
},
emptyTitle: {
- fontSize: 24,
- fontWeight: "bold",
- color: "#fff",
- marginTop: 24,
- marginBottom: 8,
+ ...typography.title2,
+ color: colors.text,
+ marginBottom: spacing.sm,
},
emptySubtitle: {
- fontSize: 16,
- color: "#888",
+ ...typography.body,
+ color: colors.textSecondary,
textAlign: "center",
- paddingHorizontal: 40,
+ },
+ bottomSpacer: {
+ height: Platform.OS === 'ios' ? 100 : 80,
},
});
diff --git a/apps/mobile/app/(auth)/login.tsx b/apps/mobile/app/(auth)/login.tsx
index 5643f7e..cc50838 100755
--- a/apps/mobile/app/(auth)/login.tsx
+++ b/apps/mobile/app/(auth)/login.tsx
@@ -2,7 +2,6 @@ import { useState } from "react";
import {
View,
Text,
- TextInput,
TouchableOpacity,
StyleSheet,
Alert,
@@ -12,9 +11,16 @@ import {
} from "react-native";
import { Redirect } from "expo-router";
import * as Linking from "expo-linking";
+import Animated, {
+ FadeIn,
+ FadeInDown,
+ FadeInUp,
+} from "react-native-reanimated";
+import { Ionicons } from "@expo/vector-icons";
import { supabase } from "../../src/lib/supabase";
import { useAuthStore } from "../../src/store";
-
+import { GradientBackground, GlassCard, GlassButton, GlassInput } from "../../src/components/ui";
+import { colors, spacing, radius, typography } from "../../src/theme";
type AuthMode = "magic-link" | "password";
@@ -25,22 +31,32 @@ export default function LoginScreen() {
const [isMagicLinkSent, setIsMagicLinkSent] = useState(false);
const [authMode, setAuthMode] = useState("magic-link");
const [isSignUp, setIsSignUp] = useState(false);
+ const [error, setError] = useState(null);
const { isAuthenticated } = useAuthStore();
- // Use declarative Redirect instead of imperative router.replace()
- // This ensures navigation only happens after the Root Layout is mounted
+ const showError = (message: string) => {
+ setError(message);
+ // Also show native alert on mobile
+ if (Platform.OS !== 'web') {
+ Alert.alert("Error", message);
+ }
+ };
+
+ const clearError = () => setError(null);
+
if (isAuthenticated) {
return ;
}
const handleMagicLink = async () => {
+ clearError();
if (!email.trim()) {
- Alert.alert("Error", "Please enter your email");
+ showError("Please enter your email");
return;
}
setIsLoading(true);
- const { error } = await supabase.auth.signInWithOtp({
+ const { error: authError } = await supabase.auth.signInWithOtp({
email: email.trim(),
options: {
emailRedirectTo: Linking.createURL("/(auth)/login"),
@@ -49,56 +65,66 @@ export default function LoginScreen() {
setIsLoading(false);
- if (error) {
- Alert.alert("Error", error.message);
+ if (authError) {
+ showError(authError.message);
} else {
setIsMagicLinkSent(true);
}
};
const handlePasswordAuth = async () => {
+ clearError();
if (!email.trim()) {
- Alert.alert("Error", "Please enter your email");
+ showError("Please enter your email");
return;
}
if (!password) {
- Alert.alert("Error", "Please enter your password");
+ showError("Please enter your password");
return;
}
if (isSignUp && password.length < 6) {
- Alert.alert("Error", "Password must be at least 6 characters");
+ showError("Password must be at least 6 characters");
return;
}
setIsLoading(true);
if (isSignUp) {
- const { error } = await supabase.auth.signUp({
+ const { error: signUpError } = await supabase.auth.signUp({
email: email.trim(),
password,
});
setIsLoading(false);
- if (error) {
- Alert.alert("Error", error.message);
+ if (signUpError) {
+ showError(signUpError.message);
} else {
- Alert.alert(
- "Check your email",
- "We sent you a confirmation link to verify your email address.",
- [{ text: "OK" }]
- );
+ // Show success message
+ if (Platform.OS === 'web') {
+ setError(null);
+ Alert.alert(
+ "Check your email",
+ "We sent you a confirmation link to verify your email address."
+ );
+ } else {
+ Alert.alert(
+ "Check your email",
+ "We sent you a confirmation link to verify your email address.",
+ [{ text: "OK" }]
+ );
+ }
}
} else {
- const { error } = await supabase.auth.signInWithPassword({
+ const { error: signInError } = await supabase.auth.signInWithPassword({
email: email.trim(),
password,
});
setIsLoading(false);
- if (error) {
- Alert.alert("Error", error.message);
+ if (signInError) {
+ showError(signInError.message);
}
}
};
@@ -131,290 +157,334 @@ export default function LoginScreen() {
if (isMagicLinkSent) {
return (
-
-
- ✉️
- Check your email
-
- We sent a magic link to {email}
-
- setIsMagicLinkSent(false)}
+
+
+
- Try a different email
-
+
+
+
+ Check your email
+
+ We sent a magic link to{'\n'}
+ {email}
+
+ setIsMagicLinkSent(false)}
+ style={{ marginTop: spacing.lg }}
+ >
+ Try a different email
+
+
-
+
);
}
return (
-
-
+
-
- 💕
- Sauci
-
- Explore intimacy together
-
-
- {/* Auth Mode Toggle */}
-
- setAuthMode("magic-link")}
+
+
+ {/* Logo/Brand */}
+
-
- Magic Link
+
+
+
+ Sauci
+
+ Explore intimacy together
-
- setAuthMode("password")}
+
+
+ {/* Auth Card */}
+
-
- Password
-
-
-
+
+ {/* Auth Mode Toggle */}
+
+ setAuthMode("magic-link")}
+ activeOpacity={0.7}
+ >
+
+ Magic Link
+
+
+ setAuthMode("password")}
+ activeOpacity={0.7}
+ >
+
+ Password
+
+
+
-
-
+ {/* Error Message */}
+ {error && (
+
+
+ {error}
+
+ )}
- {authMode === "password" && (
-
- )}
+ {/* Form Fields */}
+
+ { clearError(); setEmail(text); }}
+ keyboardType="email-address"
+ autoCapitalize="none"
+ autoCorrect={false}
+ icon={}
+ />
- {authMode === "magic-link" ? (
-
-
- {isLoading ? "Sending..." : "Continue with Magic Link"}
-
-
- ) : (
- <>
- { clearError(); setPassword(text); }}
+ secureTextEntry
+ autoCapitalize="none"
+ icon={}
+ />
+ )}
+
+ {authMode === "magic-link" ? (
+
+ Continue with Magic Link
+
+ ) : (
+ <>
+
+ {isSignUp ? "Create Account" : "Sign In"}
+
+ setIsSignUp(!isSignUp)}
+ >
+
+ {isSignUp
+ ? "Already have an account? Sign In"
+ : "Don't have an account? Sign Up"}
+
+
+ >
+ )}
+
+
+
+
+ {/* Social Sign In */}
+
+
+
+ or continue with
+
+
+
+
+ }
+ style={styles.socialButton}
>
-
- {isLoading
- ? isSignUp
- ? "Creating Account..."
- : "Signing In..."
- : isSignUp
- ? "Create Account"
- : "Sign In"}
-
-
- setIsSignUp(!isSignUp)}
+ Google
+
+
+ }
+ style={styles.socialButton}
>
-
- {isSignUp
- ? "Already have an account? Sign In"
- : "Don't have an account? Sign Up"}
-
-
- >
- )}
-
-
-
- or
-
-
-
-
- Continue with Google
-
-
-
- Continue with Apple
-
+ Apple
+
+
+
-
-
-
+
+
+
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#1a1a2e",
- },
- content: {
- flex: 1,
- paddingHorizontal: 24,
- paddingTop: 80,
- alignItems: "center",
- },
- emoji: {
- fontSize: 64,
- marginBottom: 16,
- },
- title: {
- fontSize: 32,
- fontWeight: "bold",
- color: "#fff",
- marginBottom: 8,
- },
- subtitle: {
- fontSize: 16,
- color: "#888",
- marginBottom: 48,
- textAlign: "center",
- },
- form: {
- width: "100%",
- },
- input: {
- backgroundColor: "#16213e",
- borderRadius: 12,
- padding: 16,
- fontSize: 16,
- color: "#fff",
- marginBottom: 16,
- borderWidth: 1,
- borderColor: "#0f3460",
- },
- button: {
- backgroundColor: "#e94560",
- borderRadius: 12,
- padding: 16,
- alignItems: "center",
- },
- buttonDisabled: {
- opacity: 0.7,
- },
- buttonText: {
- color: "#fff",
- fontSize: 16,
- fontWeight: "600",
- },
- divider: {
- flexDirection: "row",
- alignItems: "center",
- marginVertical: 24,
- },
- dividerLine: {
- flex: 1,
- height: 1,
- backgroundColor: "#333",
- },
- dividerText: {
- color: "#666",
- paddingHorizontal: 16,
- },
- socialButton: {
- backgroundColor: "#16213e",
- borderRadius: 12,
- padding: 16,
- alignItems: "center",
- marginBottom: 12,
- borderWidth: 1,
- borderColor: "#0f3460",
- },
- socialButtonText: {
- color: "#fff",
- fontSize: 16,
- fontWeight: "500",
- },
- secondaryButton: {
- marginTop: 24,
- },
- secondaryButtonText: {
- color: "#e94560",
- fontSize: 16,
},
scrollContent: {
flexGrow: 1,
},
+ content: {
+ flex: 1,
+ paddingHorizontal: spacing.lg,
+ paddingTop: 80,
+ paddingBottom: spacing.xl,
+ },
+ brandContainer: {
+ alignItems: "center",
+ marginBottom: spacing.xl,
+ },
+ logoContainer: {
+ width: 80,
+ height: 80,
+ borderRadius: 40,
+ backgroundColor: colors.primaryLight,
+ justifyContent: "center",
+ alignItems: "center",
+ marginBottom: spacing.md,
+ },
+ title: {
+ ...typography.largeTitle,
+ color: colors.text,
+ marginBottom: spacing.xs,
+ },
+ subtitle: {
+ ...typography.body,
+ color: colors.textSecondary,
+ textAlign: "center",
+ },
+ emailHighlight: {
+ color: colors.primary,
+ fontWeight: "600",
+ },
+ iconContainer: {
+ width: 100,
+ height: 100,
+ borderRadius: 50,
+ backgroundColor: colors.primaryLight,
+ justifyContent: "center",
+ alignItems: "center",
+ marginBottom: spacing.lg,
+ },
+ formContainer: {
+ marginBottom: spacing.lg,
+ },
modeToggleContainer: {
flexDirection: "row",
- backgroundColor: "#16213e",
- borderRadius: 12,
+ backgroundColor: colors.glass.background,
+ borderRadius: radius.md,
padding: 4,
- marginBottom: 24,
- width: "100%",
+ marginBottom: spacing.lg,
},
modeToggle: {
flex: 1,
paddingVertical: 12,
- borderRadius: 8,
+ borderRadius: radius.sm,
alignItems: "center",
},
modeToggleActive: {
- backgroundColor: "#e94560",
+ backgroundColor: colors.primary,
},
modeToggleText: {
- color: "#666",
- fontSize: 14,
- fontWeight: "500",
+ ...typography.subhead,
+ color: colors.textTertiary,
+ fontWeight: "600",
},
modeToggleTextActive: {
- color: "#fff",
+ color: colors.text,
+ },
+ errorContainer: {
+ flexDirection: "row",
+ alignItems: "center",
+ backgroundColor: colors.errorLight,
+ padding: spacing.md,
+ borderRadius: radius.md,
+ marginBottom: spacing.md,
+ gap: spacing.sm,
+ },
+ errorText: {
+ ...typography.subhead,
+ color: colors.error,
+ flex: 1,
+ },
+ form: {
+ gap: spacing.sm,
},
toggleSignUp: {
- marginTop: 16,
+ marginTop: spacing.md,
alignItems: "center",
},
toggleSignUpText: {
- color: "#e94560",
- fontSize: 14,
+ ...typography.subhead,
+ color: colors.primary,
+ },
+ socialContainer: {
+ marginTop: spacing.md,
+ },
+ divider: {
+ flexDirection: "row",
+ alignItems: "center",
+ marginBottom: spacing.lg,
+ },
+ dividerLine: {
+ flex: 1,
+ height: 1,
+ backgroundColor: colors.glass.border,
+ },
+ dividerText: {
+ ...typography.caption1,
+ color: colors.textTertiary,
+ paddingHorizontal: spacing.md,
+ },
+ socialButtons: {
+ flexDirection: "row",
+ gap: spacing.md,
+ },
+ socialButton: {
+ flex: 1,
},
});
-
diff --git a/apps/mobile/package-lock.json b/apps/mobile/package-lock.json
index d06b040..618d371 100644
--- a/apps/mobile/package-lock.json
+++ b/apps/mobile/package-lock.json
@@ -16,10 +16,13 @@
"@tanstack/react-query": "^5.17.0",
"base64-arraybuffer": "^1.0.2",
"expo": "~52.0.0",
+ "expo-blur": "~14.0.3",
"expo-clipboard": "~7.0.1",
"expo-constants": "~17.0.0",
"expo-file-system": "~18.0.12",
+ "expo-haptics": "~14.0.1",
"expo-image-picker": "~16.0.6",
+ "expo-linear-gradient": "~14.0.2",
"expo-linking": "~7.0.0",
"expo-notifications": "~0.29.0",
"expo-router": "~4.0.0",
@@ -6195,6 +6198,17 @@
"react-native": "*"
}
},
+ "node_modules/expo-blur": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/expo-blur/-/expo-blur-14.0.3.tgz",
+ "integrity": "sha512-BL3xnqBJbYm3Hg9t/HjNjdeY7N/q8eK5tsLYxswWG1yElISWZmMvrXYekl7XaVCPfyFyz8vQeaxd7q74ZY3Wrw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-clipboard": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-7.0.1.tgz",
@@ -6249,6 +6263,15 @@
"react-native": "*"
}
},
+ "node_modules/expo-haptics": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/expo-haptics/-/expo-haptics-14.0.1.tgz",
+ "integrity": "sha512-V81FZ7xRUfqM6uSI6FA1KnZ+QpEKnISqafob/xEfcx1ymwhm4V3snuLWWFjmAz+XaZQTqlYa8z3QbqEXz7G63w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*"
+ }
+ },
"node_modules/expo-image-loader": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/expo-image-loader/-/expo-image-loader-5.0.0.tgz",
@@ -6280,6 +6303,17 @@
"react": "*"
}
},
+ "node_modules/expo-linear-gradient": {
+ "version": "14.0.2",
+ "resolved": "https://registry.npmjs.org/expo-linear-gradient/-/expo-linear-gradient-14.0.2.tgz",
+ "integrity": "sha512-nvac1sPUfFFJ4mY25UkvubpUV/olrBH+uQw5k+beqSvQaVQiUfFtYzfRr+6HhYBNb4AEsOtpsCRkpDww3M2iGQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "expo": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/expo-linking": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-7.0.5.tgz",
diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index 8d1307a..56ce8a0 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -20,10 +20,13 @@
"@tanstack/react-query": "^5.17.0",
"base64-arraybuffer": "^1.0.2",
"expo": "~52.0.0",
+ "expo-blur": "~14.0.3",
"expo-clipboard": "~7.0.1",
"expo-constants": "~17.0.0",
"expo-file-system": "~18.0.12",
+ "expo-haptics": "~14.0.1",
"expo-image-picker": "~16.0.6",
+ "expo-linear-gradient": "~14.0.2",
"expo-linking": "~7.0.0",
"expo-notifications": "~0.29.0",
"expo-router": "~4.0.0",
diff --git a/apps/mobile/src/components/SwipeCard.native.tsx b/apps/mobile/src/components/SwipeCard.native.tsx
index f437210..be80dad 100755
--- a/apps/mobile/src/components/SwipeCard.native.tsx
+++ b/apps/mobile/src/components/SwipeCard.native.tsx
@@ -1,5 +1,7 @@
-import { View, Text, StyleSheet, Image, Dimensions, TouchableOpacity } from "react-native";
+import { View, Text, StyleSheet, Dimensions, Platform } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
+import { BlurView } from "expo-blur";
+import { LinearGradient } from "expo-linear-gradient";
import Animated, {
useSharedValue,
useAnimatedStyle,
@@ -8,17 +10,33 @@ import Animated, {
interpolate,
Extrapolate,
withTiming,
+ interpolateColor,
} from "react-native-reanimated";
import { Ionicons } from "@expo/vector-icons";
+import { colors, gradients, radius, shadows, blur, typography, spacing, animations } from "../theme";
+
+// Haptics helper - not supported on web
+const triggerHaptic = async (style: 'light' | 'medium' | 'heavy' = 'medium') => {
+ if (Platform.OS === 'web') return;
+ const Haptics = await import('expo-haptics');
+ const feedbackStyle = style === 'light'
+ ? Haptics.ImpactFeedbackStyle.Light
+ : style === 'heavy'
+ ? Haptics.ImpactFeedbackStyle.Heavy
+ : Haptics.ImpactFeedbackStyle.Medium;
+ await Haptics.impactAsync(feedbackStyle);
+};
const SCREEN_WIDTH = Dimensions.get("window").width;
const SWIPE_THRESHOLD = SCREEN_WIDTH * 0.25;
interface Props {
- question: { id: string; text: string; intensity: number; partner_text?: string | null };
+ question: { id: string; text: string; intensity: number; partner_text?: string | null; is_two_part?: boolean };
onSwipe: (direction: "left" | "right" | "up") => void;
}
+const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
+
export default function SwipeCard({ question, onSwipe }: Props) {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
@@ -32,7 +50,7 @@ export default function SwipeCard({ question, onSwipe }: Props) {
.activeOffsetY([-15, 15])
.onStart(() => {
context.value = { x: translateX.value, y: translateY.value };
- scale.value = withTiming(1.05);
+ scale.value = withTiming(1.02, { duration: animations.timing.fast });
})
.onUpdate((event) => {
translateX.value = event.translationX + context.value.x;
@@ -40,27 +58,29 @@ export default function SwipeCard({ question, onSwipe }: Props) {
rotation.value = interpolate(
translateX.value,
[-SCREEN_WIDTH / 2, SCREEN_WIDTH / 2],
- [-15, 15],
+ [-12, 12],
Extrapolate.CLAMP
);
})
.onEnd(() => {
- scale.value = withTiming(1);
+ scale.value = withTiming(1, { duration: animations.timing.fast });
if (Math.abs(translateX.value) > SWIPE_THRESHOLD) {
- // Horizontal swipe (YES/NO)
const direction = translateX.value > 0 ? "right" : "left";
- translateX.value = withSpring(direction === "right" ? SCREEN_WIDTH * 1.5 : -SCREEN_WIDTH * 1.5);
+ runOnJS(triggerHaptic)('medium');
+ translateX.value = withSpring(
+ direction === "right" ? SCREEN_WIDTH * 1.5 : -SCREEN_WIDTH * 1.5,
+ animations.springGentle
+ );
runOnJS(onSwipe)(direction);
} else if (translateY.value < -SWIPE_THRESHOLD) {
- // Vertical swipe up (MAYBE)
- translateY.value = withSpring(-SCREEN_WIDTH * 1.5);
+ runOnJS(triggerHaptic)('medium');
+ translateY.value = withSpring(-SCREEN_WIDTH * 1.5, animations.springGentle);
runOnJS(onSwipe)("up");
} else {
- // Reset
- translateX.value = withSpring(0);
- translateY.value = withSpring(0);
- rotation.value = withSpring(0);
+ translateX.value = withSpring(0, animations.spring);
+ translateY.value = withSpring(0, animations.spring);
+ rotation.value = withSpring(0, animations.spring);
}
});
@@ -73,80 +93,184 @@ export default function SwipeCard({ question, onSwipe }: Props) {
],
}));
- const overlayStyle = (direction: "left" | "right" | "up", color: string) =>
+ const overlayStyle = (direction: "left" | "right" | "up") =>
useAnimatedStyle(() => {
let opacity = 0;
+ let backgroundColor = 'transparent';
+
if (direction === "right") {
- opacity = interpolate(translateX.value, [0, SWIPE_THRESHOLD], [0, 1]);
+ opacity = interpolate(translateX.value, [0, SWIPE_THRESHOLD], [0, 0.9]);
+ backgroundColor = interpolateColor(
+ opacity,
+ [0, 0.9],
+ ['transparent', 'rgba(46, 204, 113, 0.4)']
+ ) as string;
} else if (direction === "left") {
- opacity = interpolate(translateX.value, [0, -SWIPE_THRESHOLD], [0, 1]);
+ opacity = interpolate(translateX.value, [0, -SWIPE_THRESHOLD], [0, 0.9]);
+ backgroundColor = interpolateColor(
+ opacity,
+ [0, 0.9],
+ ['transparent', 'rgba(231, 76, 60, 0.4)']
+ ) as string;
} else {
- opacity = interpolate(translateY.value, [0, -SWIPE_THRESHOLD], [0, 1]);
+ opacity = interpolate(translateY.value, [0, -SWIPE_THRESHOLD], [0, 0.9]);
+ backgroundColor = interpolateColor(
+ opacity,
+ [0, 0.9],
+ ['transparent', 'rgba(243, 156, 18, 0.4)']
+ ) as string;
}
return {
opacity,
- backgroundColor: color,
+ backgroundColor,
};
});
+ const handleButtonPress = async (direction: "left" | "right" | "up") => {
+ await triggerHaptic('light');
+ onSwipe(direction);
+ };
+
+ const useBlur = Platform.OS === 'ios';
+
return (
-
- {/* Overlays */}
-
- YES
-
-
- NO
-
-
- MAYBE
-
-
-
-
- {[...Array(question.intensity)].map((_, i) => (
-
- ))}
+
+ {useBlur ? (
+
+
+
+ ) : (
+
+
- {question.text}
-
+ )}
-
+
+ );
+}
+
+function CardContent({
+ question,
+ overlayStyle,
+ handleButtonPress,
+}: {
+ question: Props['question'];
+ overlayStyle: (direction: "left" | "right" | "up") => any;
+ handleButtonPress: (direction: "left" | "right" | "up") => void;
+}) {
+ return (
+ <>
+ {/* Top highlight */}
+
+
+ {/* Overlays */}
+
+
+ YES
+
+
+
+
+ NO
+
+
+
+
+ MAYBE
+
+
+
+ {/* Content */}
+
+
+ {[...Array(question.intensity)].map((_, i) => (
+
+ ))}
+
+
+ {question.text}
+
+
+
+ {/* Action Buttons */}
- onSwipe("left")}
- activeOpacity={0.7}
- >
-
-
-
-
- onSwipe("up")}
- activeOpacity={0.7}
- >
-
-
-
-
- onSwipe("right")}
- activeOpacity={0.7}
- >
-
-
-
+ handleButtonPress("left")}
+ colors={gradients.error as [string, string]}
+ icon="close"
+ />
+ handleButtonPress("up")}
+ colors={gradients.warning as [string, string]}
+ icon="help"
+ small
+ />
+ handleButtonPress("right")}
+ colors={gradients.success as [string, string]}
+ icon="heart"
+ />
-
+ >
+ );
+}
+
+function ActionButton({
+ onPress,
+ colors: buttonColors,
+ icon,
+ small = false,
+}: {
+ onPress: () => void;
+ colors: [string, string];
+ icon: string;
+ small?: boolean;
+}) {
+ return (
+
+
+
+
+
+
);
}
@@ -154,159 +278,113 @@ const styles = StyleSheet.create({
cardWrapper: {
position: "absolute",
width: SCREEN_WIDTH - 48,
- height: 500,
+ height: 480,
},
- card: {
+ cardOuter: {
width: "100%",
- height: 500,
- backgroundColor: "#16213e",
- borderRadius: 24,
- borderWidth: 1,
- borderColor: "#0f3460",
- shadowColor: "#000",
- shadowOffset: {
- width: 0,
- height: 4,
- },
- shadowOpacity: 0.3,
- shadowRadius: 4.65,
- elevation: 8,
+ height: "100%",
+ borderRadius: radius.xxl,
overflow: "hidden",
},
+ blurContainer: {
+ flex: 1,
+ backgroundColor: colors.glass.background,
+ borderWidth: 1,
+ borderColor: colors.glass.border,
+ borderRadius: radius.xxl,
+ overflow: "hidden",
+ },
+ fallbackContainer: {
+ flex: 1,
+ backgroundColor: colors.glass.backgroundLight,
+ borderWidth: 1,
+ borderColor: colors.glass.border,
+ borderRadius: radius.xxl,
+ overflow: "hidden",
+ },
+ highlight: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ height: 80,
+ },
content: {
flex: 1,
justifyContent: "center",
alignItems: "center",
- padding: 32,
+ padding: spacing.xl,
},
intensityContainer: {
flexDirection: "row",
- marginBottom: 24,
- backgroundColor: "rgba(233, 69, 96, 0.1)",
- paddingHorizontal: 12,
- paddingVertical: 6,
- borderRadius: 12,
+ marginBottom: spacing.lg,
+ backgroundColor: colors.primaryLight,
+ paddingHorizontal: spacing.md,
+ paddingVertical: spacing.sm,
+ borderRadius: radius.full,
+ gap: 2,
},
text: {
- fontSize: 24,
- fontWeight: "bold",
- color: "#fff",
+ ...typography.title2,
+ color: colors.text,
textAlign: "center",
lineHeight: 32,
},
twoPartText: {
- fontSize: 20,
+ ...typography.title3,
lineHeight: 28,
},
footer: {
- position: "absolute",
- bottom: 0,
- left: 0,
- right: 0,
- paddingTop: 8,
- paddingBottom: 16,
- paddingHorizontal: 8,
- alignItems: "center",
- backgroundColor: "rgba(0,0,0,0.2)",
- borderBottomLeftRadius: 24,
- borderBottomRightRadius: 24,
- },
- hint: {
- color: "#666",
- fontSize: 12,
- marginBottom: 4,
+ paddingBottom: spacing.lg,
+ paddingHorizontal: spacing.lg,
},
overlay: {
...StyleSheet.absoluteFillObject,
justifyContent: "center",
alignItems: "center",
zIndex: 10,
+ borderRadius: radius.xxl,
+ },
+ overlayBadge: {
+ paddingHorizontal: spacing.lg,
+ paddingVertical: spacing.sm,
+ borderRadius: radius.md,
+ transform: [{ rotate: "-15deg" }],
},
overlayText: {
- fontSize: 48,
+ fontSize: 36,
fontWeight: "900",
- color: "#fff",
- textShadowColor: "rgba(0,0,0,0.5)",
- textShadowOffset: { width: 0, height: 2 },
- textShadowRadius: 4,
- transform: [{ rotate: "-15deg" }],
- borderWidth: 4,
- borderColor: "#fff",
- paddingHorizontal: 16,
- paddingVertical: 8,
- borderRadius: 12,
- },
- partnerContainer: {
- width: "100%",
- alignItems: "center",
- },
- divider: {
- height: 1,
- width: "80%",
- backgroundColor: "rgba(255,255,255,0.1)",
- marginVertical: 24,
- position: "relative",
- justifyContent: "center",
- alignItems: "center",
- },
- dividerText: {
- color: "#666",
- backgroundColor: "#16213e",
- paddingHorizontal: 10,
- fontSize: 12,
- fontWeight: "bold",
- position: "absolute",
+ color: colors.text,
+ letterSpacing: 2,
},
buttonContainer: {
flexDirection: "row",
- justifyContent: "space-between",
+ justifyContent: "center",
alignItems: "center",
- paddingHorizontal: 32,
- paddingVertical: 16,
- gap: 20,
- zIndex: 21,
+ gap: spacing.md,
},
actionButton: {
- alignItems: "center",
- justifyContent: "center",
width: 64,
height: 64,
borderRadius: 32,
- shadowColor: "#000",
- shadowOffset: { width: 0, height: 4 },
- shadowOpacity: 0.4,
- shadowRadius: 8,
- elevation: 8,
- borderWidth: 2,
+ justifyContent: "center",
+ alignItems: "center",
overflow: "hidden",
- zIndex: 22,
},
- noButton: {
- backgroundColor: "#E74C3C",
- borderColor: "rgba(255, 255, 255, 0.3)",
+ actionButtonSmall: {
+ width: 52,
+ height: 52,
+ borderRadius: 26,
},
- maybeButton: {
- backgroundColor: "#F39C12",
- borderColor: "rgba(255, 255, 255, 0.3)",
- },
- yesButton: {
- backgroundColor: "#2ECC71",
- borderColor: "rgba(255, 255, 255, 0.3)",
- },
- buttonInnerGlow: {
+ buttonHighlight: {
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "50%",
backgroundColor: "rgba(255, 255, 255, 0.15)",
- borderTopLeftRadius: 32,
- borderTopRightRadius: 32,
},
buttonIcon: {
zIndex: 1,
- textShadowColor: "rgba(0, 0, 0, 0.3)",
- textShadowOffset: { width: 0, height: 1 },
- textShadowRadius: 2,
},
});
diff --git a/apps/mobile/src/components/ui/GlassButton.tsx b/apps/mobile/src/components/ui/GlassButton.tsx
new file mode 100644
index 0000000..dbc2eea
--- /dev/null
+++ b/apps/mobile/src/components/ui/GlassButton.tsx
@@ -0,0 +1,238 @@
+import React from 'react';
+import { StyleSheet, Text, Pressable, ViewStyle, TextStyle, ActivityIndicator, Platform } from 'react-native';
+import { LinearGradient } from 'expo-linear-gradient';
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+ withSpring,
+} from 'react-native-reanimated';
+import { colors, gradients, radius, shadows, typography, animations } from '../../theme';
+
+// Haptics is not supported on web
+const triggerHaptic = async () => {
+ if (Platform.OS === 'web') return;
+ const Haptics = await import('expo-haptics');
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+};
+
+type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
+type ButtonSize = 'sm' | 'md' | 'lg';
+
+interface GlassButtonProps {
+ children: React.ReactNode;
+ onPress?: () => void;
+ variant?: ButtonVariant;
+ size?: ButtonSize;
+ disabled?: boolean;
+ loading?: boolean;
+ fullWidth?: boolean;
+ style?: ViewStyle;
+ textStyle?: TextStyle;
+ haptic?: boolean;
+ icon?: React.ReactNode;
+}
+
+const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
+
+export function GlassButton({
+ children,
+ onPress,
+ variant = 'primary',
+ size = 'md',
+ disabled = false,
+ loading = false,
+ fullWidth = false,
+ style,
+ textStyle,
+ haptic = true,
+ icon,
+}: GlassButtonProps) {
+ const scale = useSharedValue(1);
+
+ const animatedStyle = useAnimatedStyle(() => ({
+ transform: [{ scale: scale.value }],
+ }));
+
+ const handlePressIn = () => {
+ scale.value = withSpring(0.96, animations.spring);
+ };
+
+ const handlePressOut = () => {
+ scale.value = withSpring(1, animations.spring);
+ };
+
+ const handlePress = async () => {
+ if (disabled || loading) return;
+ if (haptic) {
+ await triggerHaptic();
+ }
+ onPress?.();
+ };
+
+ const sizeStyles = sizes[size];
+ const variantStyles = variants[variant];
+
+ const content = (
+ <>
+ {loading ? (
+
+ ) : (
+ <>
+ {icon && {icon}}
+
+ {children}
+
+ >
+ )}
+ >
+ );
+
+ return (
+
+ {variant === 'primary' || variant === 'danger' ? (
+
+ {content}
+
+ ) : (
+
+ {content}
+
+ )}
+
+ );
+}
+
+const variants = {
+ primary: {
+ gradient: gradients.primary,
+ textColor: colors.text,
+ background: undefined,
+ borderWidth: 0,
+ borderColor: undefined,
+ },
+ secondary: {
+ gradient: undefined,
+ textColor: colors.text,
+ background: colors.glass.background,
+ borderWidth: 1,
+ borderColor: colors.glass.border,
+ },
+ ghost: {
+ gradient: undefined,
+ textColor: colors.primary,
+ background: 'transparent',
+ borderWidth: 0,
+ borderColor: undefined,
+ },
+ danger: {
+ gradient: gradients.error,
+ textColor: colors.text,
+ background: undefined,
+ borderWidth: 0,
+ borderColor: undefined,
+ },
+};
+
+const sizes = {
+ sm: {
+ button: {
+ paddingHorizontal: 16,
+ paddingVertical: 10,
+ borderRadius: radius.md,
+ },
+ text: {
+ fontSize: 14,
+ fontWeight: '600' as const,
+ },
+ },
+ md: {
+ button: {
+ paddingHorizontal: 24,
+ paddingVertical: 14,
+ borderRadius: radius.md,
+ },
+ text: {
+ fontSize: 16,
+ fontWeight: '600' as const,
+ },
+ },
+ lg: {
+ button: {
+ paddingHorizontal: 32,
+ paddingVertical: 18,
+ borderRadius: radius.lg,
+ },
+ text: {
+ fontSize: 18,
+ fontWeight: '600' as const,
+ },
+ },
+};
+
+const styles = StyleSheet.create({
+ button: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ text: {
+ textAlign: 'center',
+ },
+ fullWidth: {
+ width: '100%',
+ },
+ disabled: {
+ opacity: 0.5,
+ },
+ loader: {
+ marginHorizontal: 8,
+ },
+ icon: {
+ marginRight: 8,
+ },
+});
+
+export default GlassButton;
diff --git a/apps/mobile/src/components/ui/GlassCard.tsx b/apps/mobile/src/components/ui/GlassCard.tsx
new file mode 100644
index 0000000..f70ea59
--- /dev/null
+++ b/apps/mobile/src/components/ui/GlassCard.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import { StyleSheet, View, ViewStyle, Platform } from 'react-native';
+import { BlurView } from 'expo-blur';
+import { LinearGradient } from 'expo-linear-gradient';
+import { colors, radius, shadows, blur } from '../../theme';
+
+interface GlassCardProps {
+ children: React.ReactNode;
+ style?: ViewStyle;
+ intensity?: 'light' | 'medium' | 'heavy';
+ variant?: 'default' | 'elevated' | 'subtle';
+ noPadding?: boolean;
+}
+
+export function GlassCard({
+ children,
+ style,
+ intensity = 'medium',
+ variant = 'default',
+ noPadding = false,
+}: GlassCardProps) {
+ const blurIntensity = blur[intensity];
+ const shadowStyle = variant === 'elevated' ? shadows.lg : variant === 'subtle' ? shadows.sm : shadows.md;
+ const borderColor = variant === 'elevated' ? colors.glass.borderLight : colors.glass.border;
+
+ // Use BlurView on iOS, fallback to solid background on Android for performance
+ const useBlur = Platform.OS === 'ios';
+
+ return (
+
+ {useBlur ? (
+
+ {/* Top highlight gradient */}
+
+
+ {children}
+
+
+ ) : (
+ // Android fallback with solid background
+
+
+
+ {children}
+
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ borderRadius: radius.lg,
+ overflow: 'hidden',
+ },
+ blurContainer: {
+ overflow: 'hidden',
+ borderRadius: radius.lg,
+ borderWidth: 1,
+ backgroundColor: colors.glass.background,
+ },
+ fallbackContainer: {
+ overflow: 'hidden',
+ borderRadius: radius.lg,
+ borderWidth: 1,
+ backgroundColor: colors.glass.backgroundLight,
+ },
+ highlight: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ height: 60,
+ },
+ content: {
+ padding: 16,
+ },
+ noPadding: {
+ padding: 0,
+ },
+});
+
+export default GlassCard;
diff --git a/apps/mobile/src/components/ui/GlassInput.tsx b/apps/mobile/src/components/ui/GlassInput.tsx
new file mode 100644
index 0000000..4e90b86
--- /dev/null
+++ b/apps/mobile/src/components/ui/GlassInput.tsx
@@ -0,0 +1,140 @@
+import React, { useState } from 'react';
+import { StyleSheet, TextInput, View, Text, TextInputProps, ViewStyle, Platform } from 'react-native';
+import { BlurView } from 'expo-blur';
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+ withTiming,
+ interpolateColor,
+} from 'react-native-reanimated';
+import { colors, radius, blur, animations, typography } from '../../theme';
+
+interface GlassInputProps extends Omit {
+ label?: string;
+ error?: string;
+ containerStyle?: ViewStyle;
+ icon?: React.ReactNode;
+}
+
+const AnimatedBlurView = Animated.createAnimatedComponent(BlurView);
+const AnimatedView = Animated.View;
+
+export function GlassInput({
+ label,
+ error,
+ containerStyle,
+ icon,
+ ...props
+}: GlassInputProps) {
+ const [isFocused, setIsFocused] = useState(false);
+ const focusAnim = useSharedValue(0);
+
+ const handleFocus = (e: any) => {
+ setIsFocused(true);
+ focusAnim.value = withTiming(1, { duration: animations.timing.fast });
+ props.onFocus?.(e);
+ };
+
+ const handleBlur = (e: any) => {
+ setIsFocused(false);
+ focusAnim.value = withTiming(0, { duration: animations.timing.fast });
+ props.onBlur?.(e);
+ };
+
+ const animatedBorderStyle = useAnimatedStyle(() => ({
+ borderColor: interpolateColor(
+ focusAnim.value,
+ [0, 1],
+ [error ? colors.error : colors.glass.border, error ? colors.error : colors.primary]
+ ),
+ }));
+
+ const useBlur = Platform.OS === 'ios';
+
+ const inputContent = (
+
+ {icon && {icon}}
+
+
+ );
+
+ return (
+
+ {label && {label}}
+
+ {useBlur ? (
+
+
+ {inputContent}
+
+
+ ) : (
+
+ {inputContent}
+
+ )}
+
+ {error && {error}}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ marginBottom: 16,
+ },
+ label: {
+ ...typography.subhead,
+ color: colors.textSecondary,
+ marginBottom: 8,
+ marginLeft: 4,
+ },
+ blurWrapper: {
+ borderRadius: radius.md,
+ borderWidth: 1,
+ overflow: 'hidden',
+ },
+ blurContainer: {
+ backgroundColor: colors.glass.background,
+ },
+ fallbackContainer: {
+ borderRadius: radius.md,
+ borderWidth: 1,
+ backgroundColor: colors.glass.backgroundLight,
+ },
+ inputWrapper: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ icon: {
+ paddingLeft: 16,
+ },
+ input: {
+ flex: 1,
+ padding: 16,
+ fontSize: 16,
+ color: colors.text,
+ },
+ inputWithIcon: {
+ paddingLeft: 12,
+ },
+ error: {
+ ...typography.caption1,
+ color: colors.error,
+ marginTop: 6,
+ marginLeft: 4,
+ },
+});
+
+export default GlassInput;
diff --git a/apps/mobile/src/components/ui/GradientBackground.tsx b/apps/mobile/src/components/ui/GradientBackground.tsx
new file mode 100644
index 0000000..336ea83
--- /dev/null
+++ b/apps/mobile/src/components/ui/GradientBackground.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+import { StyleSheet, View, ViewStyle } from 'react-native';
+import { LinearGradient } from 'expo-linear-gradient';
+import { colors, gradients } from '../../theme';
+
+interface GradientBackgroundProps {
+ children: React.ReactNode;
+ variant?: 'default' | 'reverse' | 'subtle';
+ style?: ViewStyle;
+ showAccent?: boolean;
+}
+
+export function GradientBackground({
+ children,
+ variant = 'default',
+ style,
+ showAccent = false,
+}: GradientBackgroundProps) {
+ const gradientColors = variant === 'reverse'
+ ? gradients.backgroundReverse
+ : gradients.background;
+
+ return (
+
+
+ {showAccent && (
+ <>
+ {/* Top-right accent glow */}
+
+ {/* Bottom-left accent glow */}
+
+ >
+ )}
+ {children}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: colors.background,
+ },
+ accentTopRight: {
+ position: 'absolute',
+ top: -100,
+ right: -100,
+ width: 300,
+ height: 300,
+ borderRadius: 150,
+ backgroundColor: colors.primaryGlow,
+ opacity: 0.15,
+ },
+ accentBottomLeft: {
+ position: 'absolute',
+ bottom: -50,
+ left: -100,
+ width: 250,
+ height: 250,
+ borderRadius: 125,
+ backgroundColor: colors.primaryGlow,
+ opacity: 0.1,
+ },
+});
+
+export default GradientBackground;
diff --git a/apps/mobile/src/components/ui/index.ts b/apps/mobile/src/components/ui/index.ts
new file mode 100644
index 0000000..dcf048f
--- /dev/null
+++ b/apps/mobile/src/components/ui/index.ts
@@ -0,0 +1,4 @@
+export { GradientBackground } from './GradientBackground';
+export { GlassCard } from './GlassCard';
+export { GlassButton } from './GlassButton';
+export { GlassInput } from './GlassInput';
diff --git a/apps/mobile/src/theme/index.ts b/apps/mobile/src/theme/index.ts
new file mode 100644
index 0000000..6cd5097
--- /dev/null
+++ b/apps/mobile/src/theme/index.ts
@@ -0,0 +1,204 @@
+import { Platform } from 'react-native';
+
+export const colors = {
+ // Backgrounds
+ background: '#0d0d1a',
+ backgroundLight: '#1a1a2e',
+ surface: 'rgba(22, 33, 62, 0.6)',
+ surfaceSolid: '#16213e',
+
+ // Glass effects
+ glass: {
+ background: 'rgba(22, 33, 62, 0.4)',
+ backgroundLight: 'rgba(22, 33, 62, 0.6)',
+ border: 'rgba(255, 255, 255, 0.08)',
+ borderLight: 'rgba(255, 255, 255, 0.12)',
+ highlight: 'rgba(255, 255, 255, 0.05)',
+ },
+
+ // Brand
+ primary: '#e94560',
+ primaryDark: '#c73a52',
+ primaryLight: 'rgba(233, 69, 96, 0.15)',
+ primaryGlow: 'rgba(233, 69, 96, 0.3)',
+
+ // Text
+ text: '#ffffff',
+ textSecondary: 'rgba(255, 255, 255, 0.6)',
+ textTertiary: 'rgba(255, 255, 255, 0.4)',
+
+ // Semantic
+ success: '#2ECC71',
+ successLight: 'rgba(46, 204, 113, 0.15)',
+ warning: '#F39C12',
+ warningLight: 'rgba(243, 156, 18, 0.15)',
+ error: '#E74C3C',
+ errorLight: 'rgba(231, 76, 60, 0.15)',
+};
+
+export const gradients = {
+ primary: ['#e94560', '#c73a52'],
+ primarySubtle: ['rgba(233, 69, 96, 0.8)', 'rgba(199, 58, 82, 0.8)'],
+ background: ['#1a1a2e', '#0d0d1a'],
+ backgroundReverse: ['#0d0d1a', '#1a1a2e'],
+ glass: ['rgba(255, 255, 255, 0.08)', 'rgba(255, 255, 255, 0)'],
+ success: ['#2ECC71', '#27ae60'],
+ warning: ['#F39C12', '#e67e22'],
+ error: ['#E74C3C', '#c0392b'],
+};
+
+export const spacing = {
+ xs: 4,
+ sm: 8,
+ md: 16,
+ lg: 24,
+ xl: 32,
+ xxl: 48,
+};
+
+export const radius = {
+ xs: 4,
+ sm: 8,
+ md: 12,
+ lg: 16,
+ xl: 24,
+ xxl: 32,
+ full: 9999,
+};
+
+export const typography = {
+ largeTitle: {
+ fontSize: 32,
+ fontWeight: 'bold' as const,
+ lineHeight: 40,
+ },
+ title1: {
+ fontSize: 28,
+ fontWeight: 'bold' as const,
+ lineHeight: 34,
+ },
+ title2: {
+ fontSize: 24,
+ fontWeight: 'bold' as const,
+ lineHeight: 30,
+ },
+ title3: {
+ fontSize: 20,
+ fontWeight: '600' as const,
+ lineHeight: 26,
+ },
+ headline: {
+ fontSize: 17,
+ fontWeight: '600' as const,
+ lineHeight: 22,
+ },
+ body: {
+ fontSize: 16,
+ fontWeight: 'normal' as const,
+ lineHeight: 22,
+ },
+ callout: {
+ fontSize: 15,
+ fontWeight: 'normal' as const,
+ lineHeight: 20,
+ },
+ subhead: {
+ fontSize: 14,
+ fontWeight: 'normal' as const,
+ lineHeight: 20,
+ },
+ footnote: {
+ fontSize: 13,
+ fontWeight: 'normal' as const,
+ lineHeight: 18,
+ },
+ caption1: {
+ fontSize: 12,
+ fontWeight: 'normal' as const,
+ lineHeight: 16,
+ },
+ caption2: {
+ fontSize: 11,
+ fontWeight: 'normal' as const,
+ lineHeight: 14,
+ },
+};
+
+export const shadows = {
+ none: {},
+ sm: {
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 2 },
+ shadowOpacity: 0.15,
+ shadowRadius: 4,
+ elevation: 2,
+ },
+ md: {
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 4 },
+ shadowOpacity: 0.2,
+ shadowRadius: 8,
+ elevation: 4,
+ },
+ lg: {
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 8 },
+ shadowOpacity: 0.25,
+ shadowRadius: 16,
+ elevation: 8,
+ },
+ xl: {
+ shadowColor: '#000',
+ shadowOffset: { width: 0, height: 12 },
+ shadowOpacity: 0.3,
+ shadowRadius: 24,
+ elevation: 12,
+ },
+ glow: (color: string) => ({
+ shadowColor: color,
+ shadowOffset: { width: 0, height: 0 },
+ shadowOpacity: 0.5,
+ shadowRadius: 16,
+ elevation: 8,
+ }),
+};
+
+export const blur = {
+ light: 10,
+ medium: 20,
+ heavy: 40,
+};
+
+// Animation presets for react-native-reanimated
+export const animations = {
+ spring: {
+ damping: 15,
+ stiffness: 150,
+ },
+ springBouncy: {
+ damping: 10,
+ stiffness: 100,
+ },
+ springGentle: {
+ damping: 20,
+ stiffness: 120,
+ },
+ timing: {
+ fast: 150,
+ normal: 250,
+ slow: 400,
+ },
+};
+
+export const theme = {
+ colors,
+ gradients,
+ spacing,
+ radius,
+ typography,
+ shadows,
+ blur,
+ animations,
+};
+
+export default theme;