This commit is contained in:
Jamie Brunton committed 2025-12-31 10:40:17 +00:00
1 parent 71667d6104
commit f46f0d59df
24 files changed
+2647 -516

No files matched your search

+429 -233
View File
@@ -1,9 +1,14 @@
import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, Modal, Pressable } from "react-native";
import { View, Text, StyleSheet, FlatList, TouchableOpacity, ActivityIndicator, Modal, Pressable, Platform } from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useEffect, useState } from "react";
import { LinearGradient } from "expo-linear-gradient";
import { BlurView } from "expo-blur";
import { supabase } from "../../src/lib/supabase";
import { usePacksStore } from "../../src/store";
import { usePacksStore, useSubscriptionStore, useAuthStore } from "../../src/store";
import { Ionicons } from "@expo/vector-icons";
import Animated, { FadeInDown, FadeIn } from "react-native-reanimated";
import { GradientBackground, GlassCard, ShimmerEffect } from "../../src/components/ui";
import { colors, gradients, spacing, radius, typography, blur, shadows } from "../../src/theme";
import { Question } from "../../src/types";
type TooltipType = 'initiator' | 'coupleType' | null;
@@ -19,15 +24,31 @@ const TOOLTIP_CONTENT = {
},
};
// Boutique gradient for question cards
const QUESTION_GRADIENTS: Array<readonly [string, string]> = [
['rgba(233, 69, 96, 0.15)', 'rgba(155, 89, 182, 0.15)'],
['rgba(155, 89, 182, 0.15)', 'rgba(233, 69, 96, 0.15)'],
['rgba(183, 110, 121, 0.12)', 'rgba(139, 69, 87, 0.12)'],
['rgba(142, 68, 173, 0.12)', 'rgba(44, 62, 80, 0.12)'],
];
export default function PackDetailsScreen() {
const { id } = useLocalSearchParams();
const router = useRouter();
const { packs, fetchPacks } = usePacksStore();
const { subscription } = useSubscriptionStore();
const { user, partner } = useAuthStore();
const [questions, setQuestions] = useState<Question[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [activeTooltip, setActiveTooltip] = useState<TooltipType>(null);
const pack = packs.find((p) => p.id === id);
const hasPremiumAccess = user?.is_premium || partner?.is_premium || subscription.isProUser;
const isPremium = pack?.is_premium && hasPremiumAccess;
// Get category for breadcrumb
const { categories } = usePacksStore();
const category = categories.find(c => c.id === pack?.category_id);
useEffect(() => {
if (packs.length === 0) {
@@ -53,311 +74,486 @@ export default function PackDetailsScreen() {
}
};
const renderItem = ({ item }: { item: Question }) => (
<View style={styles.card}>
<Text style={styles.questionText}>{item.text}</Text>
{item.partner_text && (
<Text style={styles.partnerText}>Partner role: {item.partner_text}</Text>
)}
<View style={styles.footerContainer}>
<View style={styles.intensityContainer}>
{[...Array(5)].map((_, index) => (
<Ionicons
key={index}
name="flame"
size={12}
color={index < item.intensity ? "#e94560" : "#333"}
style={{ marginRight: 2 }}
/>
))}
</View>
{/* Initiator indicator */}
{item.target_user_genders && item.target_user_genders.length > 0 ? (
<View style={styles.targetsContainer}>
<Pressable
style={[styles.targetBadge, styles.initiatorBadge]}
onPress={() => setActiveTooltip('initiator')}
>
<Ionicons name="arrow-forward" size={12} color="#f39c12" style={{ marginRight: 4 }} />
{item.target_user_genders.map((g, i) => (
<Ionicons
key={g}
name={g === 'male' ? 'male' : g === 'female' ? 'female' : 'person'}
size={14}
color="#f39c12"
style={i > 0 ? { marginLeft: 4 } : {}}
/>
))}
<Ionicons name="help-circle-outline" size={14} color="#f39c12" style={{ marginLeft: 6 }} />
</Pressable>
</View>
) : null}
{/* Couple type indicator */}
{item.allowed_couple_genders && item.allowed_couple_genders.length > 0 ? (
<Pressable
style={styles.targetsContainer}
onPress={() => setActiveTooltip('coupleType')}
>
{item.allowed_couple_genders.map((g) => {
const icons = [];
if (g === 'male+male') {
icons.push('male', 'male');
} else if (g === 'female+female') {
icons.push('female', 'female');
} else {
icons.push('male', 'female');
}
const useBlur = Platform.OS === 'ios';
return (
<View key={g} style={styles.targetBadge}>
{icons.map((icon, i) => (
<Ionicons
key={i}
name={icon as any}
size={14}
color="#ccc"
style={i > 0 ? { marginLeft: 4 } : {}}
/>
))}
</View>
);
})}
<Ionicons name="help-circle-outline" size={14} color="#ccc" style={{ marginLeft: 6 }} />
</Pressable>
) : (
<Pressable
style={styles.targetsContainer}
onPress={() => setActiveTooltip('coupleType')}
>
<View style={styles.targetBadge}>
<Ionicons name="people" size={14} color="#ccc" />
const renderItem = ({ item, index }: { item: Question; index: number }) => {
const cardGradient = QUESTION_GRADIENTS[index % QUESTION_GRADIENTS.length];
return (
<Animated.View entering={FadeInDown.delay(index * 50).duration(300)}>
<GlassCard style={styles.questionCard}>
{/* Subtle gradient overlay */}
<LinearGradient
colors={cardGradient}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={StyleSheet.absoluteFill}
/>
<View style={styles.questionContent}>
<Text style={styles.questionText}>{item.text}</Text>
{item.partner_text && (
<View style={styles.partnerTextContainer}>
<Ionicons name="swap-horizontal" size={14} color={colors.premium.rose} />
<Text style={styles.partnerText}>{item.partner_text}</Text>
</View>
)}
<View style={styles.footerContainer}>
{/* Intensity */}
<View style={styles.intensityContainer}>
{[...Array(5)].map((_, i) => (
<Ionicons
key={i}
name="flame"
size={14}
color={i < item.intensity ? colors.primary : colors.textTertiary}
style={{ marginRight: 2 }}
/>
))}
</View>
<View style={styles.badgesContainer}>
{/* Initiator indicator */}
{item.target_user_genders && item.target_user_genders.length > 0 && (
<Pressable
style={styles.targetBadge}
onPress={() => setActiveTooltip('initiator')}
>
<Ionicons name="arrow-forward" size={12} color={colors.premium.gold} style={{ marginRight: 4 }} />
{item.target_user_genders.map((g, i) => (
<Ionicons
key={g}
name={g === 'male' ? 'male' : g === 'female' ? 'female' : 'person'}
size={14}
color={colors.premium.gold}
style={i > 0 ? { marginLeft: 4 } : {}}
/>
))}
<Ionicons name="help-circle-outline" size={12} color={colors.premium.gold} style={{ marginLeft: 4 }} />
</Pressable>
)}
{/* Couple type indicator */}
<Pressable
style={styles.targetBadge}
onPress={() => setActiveTooltip('coupleType')}
>
{item.allowed_couple_genders && item.allowed_couple_genders.length > 0 ? (
item.allowed_couple_genders.map((g) => {
const icons: ('male' | 'female')[] = [];
if (g === 'male+male') {
icons.push('male', 'male');
} else if (g === 'female+female') {
icons.push('female', 'female');
} else {
icons.push('male', 'female');
}
return (
<View key={g} style={styles.coupleIconGroup}>
{icons.map((icon, i) => (
<Ionicons
key={i}
name={icon}
size={14}
color={colors.textSecondary}
style={i > 0 ? { marginLeft: 2 } : {}}
/>
))}
</View>
);
})
) : (
<Ionicons name="people" size={14} color={colors.textSecondary} />
)}
<Ionicons name="help-circle-outline" size={12} color={colors.textTertiary} style={{ marginLeft: 4 }} />
</Pressable>
</View>
</View>
<Ionicons name="help-circle-outline" size={14} color="#ccc" style={{ marginLeft: 6 }} />
</Pressable>
</View>
</GlassCard>
</Animated.View>
);
};
if (isLoading) {
return (
<GradientBackground>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
</GradientBackground>
);
}
const heroContent = (
<View style={styles.heroInner}>
{/* Background gradient */}
<LinearGradient
colors={isPremium ? gradients.boutiqueGold as [string, string] : gradients.boutiqueRose as [string, string]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={StyleSheet.absoluteFill}
/>
{/* Glass overlay */}
{useBlur ? (
<BlurView
intensity={blur.light}
tint="dark"
style={[StyleSheet.absoluteFill, styles.heroBlur]}
/>
) : (
<View style={[StyleSheet.absoluteFill, styles.heroBlurFallback]} />
)}
{/* Silk texture */}
<LinearGradient
colors={gradients.silkLight as [string, string]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={[StyleSheet.absoluteFill, { opacity: 0.5 }]}
/>
{/* Back button */}
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
<View style={styles.backButtonInner}>
<Ionicons name="arrow-back" size={22} color={colors.text} />
</View>
</TouchableOpacity>
{/* Pack info */}
<View style={styles.heroContent}>
<View style={[
styles.iconContainer,
isPremium && styles.iconContainerPremium
]}>
<Text style={styles.emoji}>{pack?.icon || "📦"}</Text>
</View>
{pack?.is_premium && (
<View style={styles.proBadge}>
<Ionicons name="star" size={12} color={colors.text} />
<Text style={styles.proBadgeText}>PRO</Text>
</View>
)}
{/* Category breadcrumb - tappable to filter by category */}
{category && (
<TouchableOpacity
style={styles.breadcrumb}
onPress={() => router.replace(`/(app)/packs?category=${category.id}`)}
activeOpacity={0.7}
>
<Text style={styles.breadcrumbText}>{category.icon} {category.name}</Text>
<Ionicons name="chevron-forward" size={12} color={colors.textTertiary} />
</TouchableOpacity>
)}
<Text style={styles.title}>{pack?.name || "Pack Details"}</Text>
<Text style={styles.description}>{pack?.description}</Text>
<View style={styles.statsBadge}>
<Text style={styles.statsText}>{questions.length} questions</Text>
</View>
</View>
</View>
);
if (isLoading) {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#e94560" />
</View>
);
}
return (
<View style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()} style={styles.backButton}>
<Ionicons name="arrow-back" size={24} color="#fff" />
</TouchableOpacity>
<Text style={styles.title}>{pack?.name || "Pack Details"}</Text>
<View style={{ width: 24 }} />
</View>
<GradientBackground>
<View style={styles.container}>
{/* Hero Section */}
<Animated.View entering={FadeIn.duration(400)}>
{isPremium ? (
<ShimmerEffect enabled={true} style={styles.heroContainer}>
{heroContent}
</ShimmerEffect>
) : (
<View style={styles.heroContainer}>
{heroContent}
</View>
)}
</Animated.View>
{pack && (
<View style={styles.packInfo}>
<View style={styles.iconContainer}>
<Text style={styles.emoji}>{pack.icon || "📦"}</Text>
</View>
<Text style={styles.description}>{pack.description}</Text>
</View>
)}
{/* Questions List */}
<FlatList
data={questions}
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
ListEmptyComponent={
<Animated.View entering={FadeInDown.delay(100).duration(300)} style={styles.emptyContainer}>
<Ionicons name="help-circle-outline" size={48} color={colors.textTertiary} />
<Text style={styles.emptyText}>No questions in this pack yet</Text>
</Animated.View>
}
/>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Questions ({questions.length})</Text>
</View>
<FlatList
data={questions}
renderItem={renderItem}
keyExtractor={(item) => item.id}
contentContainerStyle={styles.list}
/>
{/* Tooltip Modal */}
<Modal
visible={activeTooltip !== null}
transparent
animationType="fade"
onRequestClose={() => setActiveTooltip(null)}
>
<Pressable
style={styles.modalOverlay}
onPress={() => setActiveTooltip(null)}
{/* Tooltip Modal */}
<Modal
visible={activeTooltip !== null}
transparent
animationType="fade"
onRequestClose={() => setActiveTooltip(null)}
>
<View style={styles.tooltipContainer}>
<Text style={styles.tooltipTitle}>
{activeTooltip && TOOLTIP_CONTENT[activeTooltip].title}
</Text>
<Text style={styles.tooltipDescription}>
{activeTooltip && TOOLTIP_CONTENT[activeTooltip].description}
</Text>
<TouchableOpacity
style={styles.tooltipButton}
onPress={() => setActiveTooltip(null)}
>
<Text style={styles.tooltipButtonText}>Got it</Text>
</TouchableOpacity>
</View>
</Pressable>
</Modal>
</View>
<Pressable
style={styles.modalOverlay}
onPress={() => setActiveTooltip(null)}
>
<GlassCard style={styles.tooltipContainer}>
<LinearGradient
colors={gradients.laceTint as [string, string]}
style={StyleSheet.absoluteFill}
/>
<Text style={styles.tooltipTitle}>
{activeTooltip && TOOLTIP_CONTENT[activeTooltip].title}
</Text>
<Text style={styles.tooltipDescription}>
{activeTooltip && TOOLTIP_CONTENT[activeTooltip].description}
</Text>
<TouchableOpacity
style={styles.tooltipButton}
onPress={() => setActiveTooltip(null)}
>
<LinearGradient
colors={gradients.primary as [string, string]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.tooltipButtonGradient}
>
<Text style={styles.tooltipButtonText}>Got it</Text>
</LinearGradient>
</TouchableOpacity>
</GlassCard>
</Pressable>
</Modal>
</View>
</GradientBackground>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#1a1a2e",
},
loadingContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#1a1a2e",
},
header: {
paddingTop: 60,
paddingHorizontal: 24,
paddingBottom: 24,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
borderBottomWidth: 1,
borderBottomColor: "#16213e",
heroContainer: {
borderBottomLeftRadius: radius.xl,
borderBottomRightRadius: radius.xl,
overflow: 'hidden',
...shadows.lg,
},
heroInner: {
paddingTop: 50,
paddingBottom: spacing.lg,
minHeight: 260,
},
heroBlur: {
backgroundColor: 'rgba(22, 33, 62, 0.4)',
},
heroBlurFallback: {
backgroundColor: 'rgba(22, 33, 62, 0.6)',
},
backButton: {
padding: 4,
position: 'absolute',
top: 50,
left: spacing.md,
zIndex: 10,
},
title: {
fontSize: 20,
fontWeight: "bold",
color: "#fff",
backButtonInner: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(0, 0, 0, 0.3)',
justifyContent: 'center',
alignItems: 'center',
borderWidth: 1,
borderColor: colors.glass.border,
},
packInfo: {
padding: 24,
alignItems: "center",
borderBottomWidth: 1,
borderBottomColor: "#16213e",
heroContent: {
alignItems: 'center',
paddingHorizontal: spacing.lg,
paddingTop: spacing.xl,
},
iconContainer: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: "rgba(255, 255, 255, 0.05)",
justifyContent: "center",
alignItems: "center",
marginBottom: 16,
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
justifyContent: 'center',
alignItems: 'center',
marginBottom: spacing.md,
borderWidth: 2,
borderColor: 'rgba(255, 255, 255, 0.15)',
},
iconContainerPremium: {
borderColor: colors.premium.gold,
backgroundColor: colors.premium.goldLight,
},
emoji: {
fontSize: 32,
fontSize: 40,
},
proBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.premium.gold,
paddingHorizontal: spacing.sm,
paddingVertical: 4,
borderRadius: radius.sm,
gap: 4,
marginBottom: spacing.sm,
},
proBadgeText: {
...typography.caption1,
color: colors.text,
fontWeight: '700',
},
breadcrumb: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
marginBottom: spacing.xs,
},
breadcrumbText: {
...typography.caption1,
color: colors.textSecondary,
},
title: {
...typography.title2,
color: colors.text,
textAlign: 'center',
marginBottom: spacing.xs,
},
description: {
fontSize: 16,
color: "#ccc",
textAlign: "center",
...typography.body,
color: colors.textSecondary,
textAlign: 'center',
lineHeight: 24,
paddingHorizontal: spacing.md,
},
sectionHeader: {
padding: 24,
paddingBottom: 8,
statsBadge: {
marginTop: spacing.md,
backgroundColor: 'rgba(255, 255, 255, 0.1)',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
borderRadius: radius.full,
borderWidth: 1,
borderColor: colors.glass.border,
},
sectionTitle: {
fontSize: 18,
fontWeight: "bold",
color: "#e94560",
statsText: {
...typography.subhead,
color: colors.textSecondary,
fontWeight: '500',
},
list: {
padding: 24,
padding: spacing.md,
paddingBottom: Platform.OS === 'ios' ? 100 : 80,
},
card: {
backgroundColor: "#16213e",
borderRadius: 12,
padding: 16,
marginBottom: 12,
borderWidth: 1,
borderColor: "#0f3460",
questionCard: {
marginBottom: spacing.md,
overflow: 'hidden',
},
questionContent: {
padding: spacing.md,
},
questionText: {
fontSize: 16,
color: "#fff",
marginBottom: 8,
lineHeight: 22,
...typography.body,
color: colors.text,
marginBottom: spacing.sm,
lineHeight: 24,
},
intensityContainer: {
flexDirection: "row",
partnerTextContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
marginBottom: spacing.sm,
paddingVertical: spacing.xs,
paddingHorizontal: spacing.sm,
backgroundColor: colors.premium.roseLight,
borderRadius: radius.sm,
alignSelf: 'flex-start',
},
partnerText: {
fontSize: 14,
color: "#aaa",
fontStyle: "italic",
marginBottom: 8,
...typography.caption1,
color: colors.premium.rose,
fontStyle: 'italic',
},
footerContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginTop: 8,
marginTop: spacing.xs,
},
targetsContainer: {
intensityContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
alignItems: 'center',
gap: 6,
},
badgesContainer: {
flexDirection: 'row',
gap: spacing.sm,
},
targetBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255, 255, 255, 0.1)',
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 6,
backgroundColor: 'rgba(255, 255, 255, 0.08)',
paddingHorizontal: spacing.sm,
paddingVertical: spacing.xs,
borderRadius: radius.sm,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.2)',
borderColor: colors.glass.border,
},
initiatorBadge: {
backgroundColor: 'rgba(243, 156, 18, 0.15)',
borderColor: 'rgba(243, 156, 18, 0.3)',
coupleIconGroup: {
flexDirection: 'row',
alignItems: 'center',
},
emptyContainer: {
alignItems: 'center',
paddingVertical: spacing.xxl,
gap: spacing.md,
},
emptyText: {
...typography.body,
color: colors.textSecondary,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0, 0, 0, 0.7)',
justifyContent: 'center',
alignItems: 'center',
padding: 24,
padding: spacing.lg,
},
tooltipContainer: {
backgroundColor: '#16213e',
borderRadius: 16,
padding: 24,
maxWidth: 320,
borderWidth: 1,
borderColor: '#0f3460',
overflow: 'hidden',
},
tooltipTitle: {
fontSize: 18,
fontWeight: 'bold',
color: '#fff',
marginBottom: 12,
...typography.title3,
color: colors.text,
marginBottom: spacing.sm,
},
tooltipDescription: {
fontSize: 14,
color: '#ccc',
lineHeight: 22,
marginBottom: 20,
...typography.body,
color: colors.textSecondary,
lineHeight: 24,
marginBottom: spacing.lg,
},
tooltipButton: {
backgroundColor: '#e94560',
borderRadius: 8,
paddingVertical: 12,
borderRadius: radius.md,
overflow: 'hidden',
},
tooltipButtonGradient: {
paddingVertical: spacing.sm,
alignItems: 'center',
},
tooltipButtonText: {
fontSize: 16,
fontWeight: '600',
color: '#fff',
...typography.headline,
color: colors.text,
},
});