mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18: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
+130
-105
@@ -1,22 +1,23 @@
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { ScrollView, StyleSheet, Platform, View, Text, useWindowDimensions } from 'react-native';
|
||||
import { useFocusEffect } from 'expo-router';
|
||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useAuthStore, usePacksStore } from '../../src/store';
|
||||
import { GradientBackground } from '../../src/components/ui';
|
||||
import { Paywall } from '../../src/components/paywall';
|
||||
import { CompactHeader, ContentRow } from '../../src/components/discovery';
|
||||
import { colors, spacing, typography, radius } from '../../src/theme';
|
||||
import type { QuestionPack, Category } from '../../src/types';
|
||||
|
||||
const MAX_CONTENT_WIDTH = 600;
|
||||
|
||||
// Group packs by category
|
||||
function groupPacksByCategory(
|
||||
packs: QuestionPack[],
|
||||
categories: Category[]
|
||||
): { category: Category; packs: QuestionPack[] }[] {
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { ScrollView, StyleSheet, Platform, View, Text, useWindowDimensions } from 'react-native';
|
||||
import { useFocusEffect } from 'expo-router';
|
||||
import Animated, { FadeInDown } from 'react-native-reanimated';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { BlurView } from 'expo-blur';
|
||||
import { useAuthStore, usePacksStore } from '../../src/store';
|
||||
import { GradientBackground } from '../../src/components/ui';
|
||||
import { Paywall } from '../../src/components/paywall';
|
||||
import { CompactHeader, ContentRow } from '../../src/components/discovery';
|
||||
import { colors, spacing, typography, radius } from '../../src/theme';
|
||||
import type { QuestionPack, Category } from '../../src/types';
|
||||
|
||||
const MAX_CONTENT_WIDTH = 600;
|
||||
|
||||
// Group packs by category
|
||||
function groupPacksByCategory(
|
||||
packs: QuestionPack[],
|
||||
categories: Category[]
|
||||
): { category: Category; packs: QuestionPack[] }[] {
|
||||
const categoryMap = new Map<string, QuestionPack[]>();
|
||||
const uncategorized: QuestionPack[] = [];
|
||||
|
||||
@@ -67,88 +68,97 @@ function groupPacksByCategory(
|
||||
export default function DiscoveryScreen() {
|
||||
const { user, partner, couple } = useAuthStore();
|
||||
const { packs, categories, fetchPacks, getPackProgress } = usePacksStore();
|
||||
const { width } = useWindowDimensions();
|
||||
const [showPaywall, setShowPaywall] = useState(false);
|
||||
|
||||
const isPremiumUser = user?.is_premium ?? false;
|
||||
const isWideScreen = width > MAX_CONTENT_WIDTH;
|
||||
|
||||
// Refresh data when screen gains focus
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchPacks();
|
||||
}, [])
|
||||
);
|
||||
|
||||
// Group packs by category
|
||||
const packsByCategory = useMemo(
|
||||
() => groupPacksByCategory(packs, categories),
|
||||
[packs, categories]
|
||||
);
|
||||
|
||||
// Check if user has no packs enabled (empty state)
|
||||
const hasNoPacks = packs.length === 0;
|
||||
|
||||
return (
|
||||
<GradientBackground>
|
||||
<ScrollView
|
||||
style={styles.container}
|
||||
contentContainerStyle={[
|
||||
styles.contentContainer,
|
||||
isWideScreen && styles.contentContainerWide,
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={[styles.innerContainer, isWideScreen && styles.innerContainerWide]}>
|
||||
{/* Compact Header */}
|
||||
<CompactHeader user={user} partner={partner} couple={couple} />
|
||||
|
||||
{/* Content Rows by Category */}
|
||||
{packsByCategory.map((group, index) => (
|
||||
<ContentRow
|
||||
key={group.category.id}
|
||||
title={group.category.name}
|
||||
packs={group.packs}
|
||||
isPremiumUser={isPremiumUser}
|
||||
categoryId={group.category.id}
|
||||
delay={200 + index * 100}
|
||||
getPackProgress={getPackProgress}
|
||||
onShowPaywall={() => setShowPaywall(true)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Empty state when no packs */}
|
||||
{hasNoPacks && (
|
||||
<Animated.View
|
||||
entering={FadeInDown.delay(300).duration(500).springify()}
|
||||
style={styles.emptyState}
|
||||
>
|
||||
<View style={styles.emptyIcon}>
|
||||
<Ionicons name="layers-outline" size={48} color={colors.primary} />
|
||||
</View>
|
||||
<Text style={styles.emptyTitle}>No packs available</Text>
|
||||
<Text style={styles.emptySubtitle}>
|
||||
Check back soon for new content
|
||||
</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
<View style={styles.bottomSpacer} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Paywall Modal */}
|
||||
<Paywall
|
||||
visible={showPaywall}
|
||||
onClose={() => setShowPaywall(false)}
|
||||
onSuccess={() => {
|
||||
setShowPaywall(false);
|
||||
fetchPacks(); // Refresh packs to update lock status
|
||||
}}
|
||||
/>
|
||||
</GradientBackground>
|
||||
);
|
||||
}
|
||||
const { width } = useWindowDimensions();
|
||||
const [showPaywall, setShowPaywall] = useState(false);
|
||||
const [headerHeight, setHeaderHeight] = useState(130);
|
||||
|
||||
const isPremiumUser = user?.is_premium ?? false;
|
||||
const isWideScreen = width > MAX_CONTENT_WIDTH;
|
||||
|
||||
// Refresh data when screen gains focus
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fetchPacks();
|
||||
}, [])
|
||||
);
|
||||
|
||||
// Group packs by category
|
||||
const packsByCategory = useMemo(
|
||||
() => groupPacksByCategory(packs, categories),
|
||||
[packs, categories]
|
||||
);
|
||||
|
||||
// Check if user has no packs enabled (empty state)
|
||||
const hasNoPacks = packs.length === 0;
|
||||
|
||||
return (
|
||||
<GradientBackground>
|
||||
<View
|
||||
style={[styles.stickyHeader, isWideScreen && styles.stickyHeaderWide]}
|
||||
onLayout={(e) => setHeaderHeight(e.nativeEvent.layout.height)}
|
||||
>
|
||||
<BlurView intensity={80} tint="dark" style={StyleSheet.absoluteFill} />
|
||||
<View style={[StyleSheet.absoluteFill, { backgroundColor: 'rgba(14, 14, 17, 0.85)' }]} />
|
||||
<CompactHeader user={user} partner={partner} couple={couple} />
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
style={styles.container}
|
||||
contentContainerStyle={[
|
||||
styles.contentContainer,
|
||||
isWideScreen && styles.contentContainerWide,
|
||||
{ paddingTop: headerHeight },
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={[styles.innerContainer, isWideScreen && styles.innerContainerWide]}>
|
||||
{/* Content Rows by Category */}
|
||||
{packsByCategory.map((group, index) => (
|
||||
<ContentRow
|
||||
key={group.category.id}
|
||||
title={group.category.name}
|
||||
packs={group.packs}
|
||||
isPremiumUser={isPremiumUser}
|
||||
categoryId={group.category.id}
|
||||
delay={200 + index * 100}
|
||||
getPackProgress={getPackProgress}
|
||||
onShowPaywall={() => setShowPaywall(true)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Empty state when no packs */}
|
||||
{hasNoPacks && (
|
||||
<Animated.View
|
||||
entering={FadeInDown.delay(300).duration(500).springify()}
|
||||
style={styles.emptyState}
|
||||
>
|
||||
<View style={styles.emptyIcon}>
|
||||
<Ionicons name="layers-outline" size={48} color={colors.primary} />
|
||||
</View>
|
||||
<Text style={styles.emptyTitle}>No packs available</Text>
|
||||
<Text style={styles.emptySubtitle}>
|
||||
Check back soon for new content
|
||||
</Text>
|
||||
</Animated.View>
|
||||
)}
|
||||
|
||||
<View style={styles.bottomSpacer} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
{/* Paywall Modal */}
|
||||
<Paywall
|
||||
visible={showPaywall}
|
||||
onClose={() => setShowPaywall(false)}
|
||||
onSuccess={() => {
|
||||
setShowPaywall(false);
|
||||
fetchPacks(); // Refresh packs to update lock status
|
||||
}}
|
||||
/>
|
||||
</GradientBackground>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
@@ -194,7 +204,22 @@ const styles = StyleSheet.create({
|
||||
color: colors.textSecondary,
|
||||
textAlign: 'center',
|
||||
},
|
||||
bottomSpacer: {
|
||||
height: spacing.lg,
|
||||
},
|
||||
});
|
||||
bottomSpacer: {
|
||||
height: spacing.lg,
|
||||
},
|
||||
stickyHeader: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 10,
|
||||
overflow: 'hidden', // Ensures blur doesn't bleed if we rounded corners (optional)
|
||||
},
|
||||
stickyHeaderWide: {
|
||||
left: '50%',
|
||||
right: 'auto',
|
||||
width: MAX_CONTENT_WIDTH,
|
||||
transform: [{ translateX: -MAX_CONTENT_WIDTH / 2 }],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user