mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 02:19:43 +00:00
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:
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;
|
||||
Reference in new issue
Block a user