Add question types, nudge feature, and share modal

- Add new question card components (photo, audio, text, who-likely)
- Add share preview modal for matches
- Add nudge notification system with rate limiting
- Add session-based notification improvements
- Add match confetti celebration animation
- Update onboarding flow with paywall tracking
- Remove deprecated swipe card and tutorial components
- Update GlassCard/GlassInput to use solid backgrounds
- Fix routing issues after packs screen removal
This commit is contained in:
jjbrunton committed 2026-01-25 19:17:39 +00:00
1 parent 8612b16b93
commit 1daf386765
114 files changed
+14877 -7911

No files matched your search

@@ -1,72 +1,96 @@
import React from 'react';
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { Image } from 'expo-image';
import { router } from 'expo-router';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { colors, spacing, typography, radius } from '../../theme';
import type { Profile, Couple } from '@/types';
interface CompactHeaderProps {
user: Profile | null;
partner: Profile | null;
couple: Couple | null;
}
export function CompactHeader({ user, partner, couple }: CompactHeaderProps) {
return (
<Animated.View
entering={FadeInDown.delay(100).duration(500).springify()}
style={styles.container}
>
{/* Greeting */}
<View style={styles.greetingRow}>
<View>
<Text style={styles.welcomeLabel}>Discover</Text>
<Text style={styles.greeting}>
Hey, {user?.name || 'Beautiful'}
</Text>
</View>
{/* Partner badge */}
{partner ? (
<View style={styles.partnerBadge}>
{partner.avatar_url ? (
<Image
source={{ uri: partner.avatar_url }}
style={styles.partnerAvatar}
cachePolicy="disk"
contentFit="cover"
transition={200}
/>
) : (
<Ionicons name="heart" size={12} color={colors.primary} />
)}
<Text style={styles.partnerText} numberOfLines={1}>
{partner.name || partner.email?.split('@')[0] || 'Partner'}
</Text>
</View>
) : couple ? (
<TouchableOpacity
style={styles.waitingBadge}
onPress={() => router.push('/(app)/pairing')}
>
<Ionicons name="hourglass-outline" size={12} color={colors.primary} />
<Text style={styles.waitingText}>Waiting</Text>
</TouchableOpacity>
) : (
<TouchableOpacity
style={styles.pairBadge}
onPress={() => router.push('/(app)/pairing')}
>
<Ionicons name="add-circle-outline" size={14} color={colors.textTertiary} />
<Text style={styles.pairText}>Connect</Text>
</TouchableOpacity>
)}
</View>
</Animated.View>
);
}
import React from 'react';
import { StyleSheet, View, Text, TouchableOpacity } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { Image } from 'expo-image';
import { router } from 'expo-router';
import Animated, { FadeInDown } from 'react-native-reanimated';
import { colors, spacing, typography, radius } from '../../theme';
import type { Profile, Couple } from '@/types';
interface CompactHeaderProps {
user: Profile | null;
partner: Profile | null;
couple: Couple | null;
label?: string;
accessory?: React.ReactNode;
showGreeting?: boolean;
showPartnerBadge?: boolean;
}
export function CompactHeader({
user,
partner,
couple,
label = 'Discover',
accessory,
showGreeting = true,
showPartnerBadge = true,
}: CompactHeaderProps) {
return (
<Animated.View
entering={FadeInDown.delay(100).duration(500).springify()}
style={styles.container}
>
{showGreeting ? (
<View style={styles.greetingRow}>
<View>
<Text style={styles.welcomeLabel}>{label}</Text>
<Text style={styles.greeting}>
Hey, {user?.name || 'Beautiful'}
</Text>
</View>
<View style={styles.rightSection}>
{/* Partner badge */}
{showPartnerBadge ? (
partner ? (
<View style={styles.partnerBadge}>
{partner.avatar_url ? (
<Image
source={{ uri: partner.avatar_url }}
style={styles.partnerAvatar}
cachePolicy="disk"
contentFit="cover"
transition={200}
/>
) : (
<Ionicons name="heart" size={12} color={colors.primary} />
)}
<Text style={styles.partnerText} numberOfLines={1}>
{partner.name || partner.email?.split('@')[0] || 'Partner'}
</Text>
</View>
) : couple ? (
<TouchableOpacity
style={styles.waitingBadge}
onPress={() => router.push('/(app)/pairing')}
>
<Ionicons name="hourglass-outline" size={12} color={colors.primary} />
<Text style={styles.waitingText}>Waiting</Text>
</TouchableOpacity>
) : (
<TouchableOpacity
style={styles.pairBadge}
onPress={() => router.push('/(app)/pairing')}
>
<Ionicons name="add-circle-outline" size={14} color={colors.textTertiary} />
<Text style={styles.pairText}>Connect</Text>
</TouchableOpacity>
)
) : null}
{accessory ? <View style={styles.accessory}>{accessory}</View> : null}
</View>
</View>
) : (
<View style={styles.titleRow}>
{accessory ? <View style={styles.titleSpacer} /> : null}
<Text style={styles.titleOnly}>{label}</Text>
{accessory ? <View style={styles.accessory}>{accessory}</View> : null}
</View>
)}
</Animated.View>
);
}
const styles = StyleSheet.create({
container: {
@@ -74,11 +98,34 @@ const styles = StyleSheet.create({
paddingHorizontal: spacing.lg,
paddingBottom: spacing.lg,
},
greetingRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
greetingRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
rightSection: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.sm,
},
accessory: {
justifyContent: 'center',
alignItems: 'center',
},
titleRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
titleOnly: {
...typography.title2,
color: colors.text,
textAlign: 'center',
flex: 1,
},
titleSpacer: {
width: 32,
},
welcomeLabel: {
...typography.caption2,
fontWeight: '600',
@@ -32,7 +32,7 @@ export function ContentRow({
const handleSeeAll = () => {
// Navigate to packs screen with category filter
router.push({
pathname: '/(app)/packs',
pathname: '/',
params: categoryId ? { category: categoryId } : undefined,
});
};
@@ -29,14 +29,13 @@ export function ContentTile({ pack, isLocked, isNew = false, progress, onPress }
};
const backgroundColor = getCategoryColor(pack.category);
const icon = pack.icon || pack.category?.icon || 'layers';
const questionCount = pack.questions?.[0]?.count ?? 0;
// Calculate progress percentage
const progressPercent = progress && progress.totalQuestions > 0
? progress.answeredQuestions / progress.totalQuestions
: 0;
// Show progress ring for packs with questions (even at 0%)
const showProgress = progress && progress.totalQuestions > 0;
// Always show progress ring when progress data exists (even at 0% or 0 questions)
const showProgress = !!progress;
return (
<TouchableOpacity
@@ -55,6 +54,12 @@ export function ContentTile({ pack, isLocked, isNew = false, progress, onPress }
/>
</View>
<View style={styles.badges}>
{pack.is_explicit && (
<View style={styles.explicitBadge}>
<Ionicons name="flame" size={10} color={colors.text} />
<Text style={styles.explicitBadgeText}>18+</Text>
</View>
)}
{isNew && (
<View style={styles.newBadge}>
<Text style={styles.newBadgeText}>NEW</Text>
@@ -87,13 +92,6 @@ export function ContentTile({ pack, isLocked, isNew = false, progress, onPress }
</Text>
</View>
{/* Bottom info */}
<View style={styles.bottomRow}>
<Text style={styles.countText}>
{questionCount} {questionCount === 1 ? 'question' : 'questions'}
</Text>
</View>
{/* Decorative overlay for depth */}
<View style={styles.overlay} />
</View>
@@ -136,6 +134,20 @@ const styles = StyleSheet.create({
flexDirection: 'row',
gap: spacing.xs,
},
explicitBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.error,
paddingHorizontal: spacing.sm,
paddingVertical: 2,
borderRadius: radius.xs,
gap: 2,
},
explicitBadgeText: {
...typography.caption2,
fontWeight: '700',
color: colors.text,
},
newBadge: {
backgroundColor: 'rgba(255, 255, 255, 0.95)',
paddingHorizontal: spacing.sm,
@@ -173,13 +185,6 @@ const styles = StyleSheet.create({
color: colors.text,
fontWeight: '700',
},
bottomRow: {
marginTop: 'auto',
},
countText: {
...typography.caption1,
color: 'rgba(255, 255, 255, 0.7)',
},
});
export default ContentTile;