mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Update mobile app UI with premium design system
- Add design guidelines to CLAUDE.md - Redesign navigation with boutique styling - Add video support to chat with compression - Update matches/dares/quiz screens with gold/rose accents - Improve packs browsing with category tabs - Add SwipeCardPremium component - Update theme with premium color palette - Add video cleanup edge function and migration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
feb7657562
commit
cac7a2a31d
31 files changed
+6638
-1827
No files matched your search
@@ -121,6 +121,19 @@ export function PackTeaser({
|
||||
<Pressable style={StyleSheet.absoluteFill} onPress={onClose} />
|
||||
</Animated.View>
|
||||
<Animated.View style={[styles.content, contentStyle]}>
|
||||
{/* Premium gradient background */}
|
||||
<LinearGradient
|
||||
colors={['rgba(22, 33, 62, 0.98)', 'rgba(13, 13, 26, 1)']}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
{/* Top silk highlight */}
|
||||
<LinearGradient
|
||||
colors={['rgba(212, 175, 55, 0.08)', 'transparent']}
|
||||
style={styles.silkHighlight}
|
||||
/>
|
||||
{/* Premium border accent */}
|
||||
<View style={styles.premiumBorderAccent} />
|
||||
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
@@ -136,12 +149,35 @@ export function PackTeaser({
|
||||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.iconContainer}>
|
||||
<Text style={styles.packEmoji}>{packIcon || "📦"}</Text>
|
||||
{/* Premium label */}
|
||||
<Text style={styles.label}>PREMIUM COLLECTION</Text>
|
||||
|
||||
{/* Icon with glow */}
|
||||
<View style={styles.iconWrapper}>
|
||||
<View style={styles.iconContainer}>
|
||||
<LinearGradient
|
||||
colors={['rgba(212, 175, 55, 0.15)', 'rgba(184, 134, 11, 0.1)']}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
<Text style={styles.packEmoji}>{packIcon || "📦"}</Text>
|
||||
</View>
|
||||
{/* Crown badge */}
|
||||
<View style={styles.crownBadge}>
|
||||
<Ionicons name="star" size={12} color={colors.premium.gold} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text style={styles.title}>{packName}</Text>
|
||||
|
||||
{/* Decorative separator */}
|
||||
<View style={styles.separator}>
|
||||
<View style={styles.separatorLine} />
|
||||
<View style={styles.separatorDiamond} />
|
||||
<View style={styles.separatorLine} />
|
||||
</View>
|
||||
|
||||
<Text style={styles.subtitle}>
|
||||
Preview a few questions from this pack
|
||||
Preview a few questions from this exclusive pack
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -149,7 +185,7 @@ export function PackTeaser({
|
||||
<View style={styles.questionsContainer}>
|
||||
{isLoading ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator color={colors.primary} size="large" />
|
||||
<ActivityIndicator color={colors.premium.gold} size="large" />
|
||||
<Text style={styles.loadingText}>
|
||||
Loading preview...
|
||||
</Text>
|
||||
@@ -159,12 +195,12 @@ export function PackTeaser({
|
||||
<View key={question.id} style={styles.questionCard}>
|
||||
<View style={styles.questionHeader}>
|
||||
<View style={styles.intensityContainer}>
|
||||
{[...Array(question.intensity)].map((_, i) => (
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Ionicons
|
||||
key={i}
|
||||
name="flame"
|
||||
name={i < question.intensity ? "flame" : "flame-outline"}
|
||||
size={12}
|
||||
color={colors.primary}
|
||||
color={i < question.intensity ? colors.premium.gold : 'rgba(255, 255, 255, 0.2)'}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
@@ -188,40 +224,39 @@ export function PackTeaser({
|
||||
{questions.length > 0 && (
|
||||
<View style={styles.moreHint}>
|
||||
<Ionicons
|
||||
name="lock-closed"
|
||||
name="diamond"
|
||||
size={16}
|
||||
color={colors.textTertiary}
|
||||
color={colors.premium.gold}
|
||||
/>
|
||||
<Text style={styles.moreHintText}>
|
||||
+ many more questions to unlock
|
||||
+ many more exclusive questions to unlock
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Unlock Button */}
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity
|
||||
<Pressable
|
||||
style={styles.unlockButton}
|
||||
onPress={onUnlock}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<LinearGradient
|
||||
colors={gradients.primary as [string, string]}
|
||||
colors={[colors.premium.gold, colors.premium.goldDark]}
|
||||
style={styles.unlockButtonGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<Ionicons
|
||||
name="star"
|
||||
size={20}
|
||||
color={colors.text}
|
||||
name="diamond"
|
||||
size={18}
|
||||
color={colors.background}
|
||||
style={{ marginRight: spacing.sm }}
|
||||
/>
|
||||
<Text style={styles.unlockButtonText}>
|
||||
Unlock with Pro
|
||||
</Text>
|
||||
</LinearGradient>
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</Animated.View>
|
||||
@@ -237,16 +272,35 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.85)",
|
||||
},
|
||||
content: {
|
||||
backgroundColor: colors.backgroundLight,
|
||||
backgroundColor: colors.background,
|
||||
borderTopLeftRadius: radius.xxl,
|
||||
borderTopRightRadius: radius.xxl,
|
||||
maxHeight: "85%",
|
||||
borderWidth: 1,
|
||||
borderBottomWidth: 0,
|
||||
borderColor: colors.glass.border,
|
||||
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
silkHighlight: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 150,
|
||||
borderTopLeftRadius: radius.xxl,
|
||||
borderTopRightRadius: radius.xxl,
|
||||
},
|
||||
premiumBorderAccent: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 2,
|
||||
backgroundColor: colors.premium.gold,
|
||||
opacity: 0.4,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
@@ -260,36 +314,98 @@ const styles = StyleSheet.create({
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.glass.background,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
},
|
||||
header: {
|
||||
alignItems: "center",
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
backgroundColor: colors.glass.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
label: {
|
||||
...typography.caption1,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 3,
|
||||
color: colors.premium.gold,
|
||||
textAlign: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
iconWrapper: {
|
||||
position: 'relative',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 40,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
overflow: 'hidden',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.premium.gold,
|
||||
shadowColor: colors.premium.gold,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.6,
|
||||
shadowRadius: 20,
|
||||
elevation: 12,
|
||||
},
|
||||
crownBadge: {
|
||||
position: 'absolute',
|
||||
top: -4,
|
||||
right: -4,
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
backgroundColor: colors.background,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderWidth: 2,
|
||||
borderColor: colors.premium.gold,
|
||||
shadowColor: colors.premium.gold,
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.4,
|
||||
shadowRadius: 4,
|
||||
elevation: 4,
|
||||
},
|
||||
packEmoji: {
|
||||
fontSize: 36,
|
||||
fontSize: 40,
|
||||
},
|
||||
title: {
|
||||
...typography.title1,
|
||||
color: colors.text,
|
||||
marginBottom: spacing.xs,
|
||||
textShadowColor: 'rgba(212, 175, 55, 0.3)',
|
||||
textShadowOffset: { width: 0, height: 0 },
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
separator: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginVertical: spacing.md,
|
||||
width: 140,
|
||||
},
|
||||
separatorLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: 'rgba(212, 175, 55, 0.3)',
|
||||
},
|
||||
separatorDiamond: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
backgroundColor: colors.premium.gold,
|
||||
transform: [{ rotate: '45deg' }],
|
||||
marginHorizontal: spacing.md,
|
||||
opacity: 0.8,
|
||||
},
|
||||
subtitle: {
|
||||
...typography.body,
|
||||
color: colors.textSecondary,
|
||||
textAlign: "center",
|
||||
lineHeight: 24,
|
||||
},
|
||||
questionsContainer: {
|
||||
marginBottom: spacing.lg,
|
||||
@@ -305,10 +421,10 @@ const styles = StyleSheet.create({
|
||||
marginTop: spacing.md,
|
||||
},
|
||||
questionCard: {
|
||||
backgroundColor: colors.glass.background,
|
||||
backgroundColor: 'rgba(22, 33, 62, 0.4)',
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.glass.border,
|
||||
borderColor: 'rgba(212, 175, 55, 0.15)',
|
||||
padding: spacing.md,
|
||||
},
|
||||
questionHeader: {
|
||||
@@ -319,20 +435,23 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
intensityContainer: {
|
||||
flexDirection: "row",
|
||||
backgroundColor: colors.primaryLight,
|
||||
backgroundColor: 'rgba(212, 175, 55, 0.1)',
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 4,
|
||||
borderRadius: radius.full,
|
||||
gap: 1,
|
||||
gap: 2,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(212, 175, 55, 0.15)',
|
||||
},
|
||||
questionNumber: {
|
||||
...typography.caption2,
|
||||
color: colors.textTertiary,
|
||||
color: colors.premium.champagne,
|
||||
opacity: 0.7,
|
||||
},
|
||||
questionText: {
|
||||
...typography.body,
|
||||
color: colors.text,
|
||||
lineHeight: 22,
|
||||
lineHeight: 24,
|
||||
},
|
||||
emptyContainer: {
|
||||
alignItems: "center",
|
||||
@@ -347,11 +466,19 @@ const styles = StyleSheet.create({
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginBottom: spacing.lg,
|
||||
gap: spacing.xs,
|
||||
gap: spacing.sm,
|
||||
backgroundColor: 'rgba(212, 175, 55, 0.08)',
|
||||
paddingVertical: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderRadius: radius.full,
|
||||
alignSelf: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(212, 175, 55, 0.15)',
|
||||
},
|
||||
moreHintText: {
|
||||
...typography.caption1,
|
||||
color: colors.textTertiary,
|
||||
color: colors.premium.champagne,
|
||||
fontWeight: '500',
|
||||
},
|
||||
actions: {
|
||||
marginBottom: spacing.md,
|
||||
@@ -359,6 +486,11 @@ const styles = StyleSheet.create({
|
||||
unlockButton: {
|
||||
borderRadius: radius.lg,
|
||||
overflow: "hidden",
|
||||
shadowColor: colors.premium.gold,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 8,
|
||||
},
|
||||
unlockButtonGradient: {
|
||||
flexDirection: "row",
|
||||
@@ -368,7 +500,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
unlockButtonText: {
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
color: colors.background,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ActivityIndicator,
|
||||
ScrollView,
|
||||
Platform,
|
||||
Pressable,
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
@@ -24,9 +25,9 @@ interface PaywallProps {
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
icon: "heart" as const,
|
||||
icon: "diamond" as const,
|
||||
title: "Unlock All Packs",
|
||||
description: "Access every question pack",
|
||||
description: "Access every premium collection",
|
||||
},
|
||||
{
|
||||
icon: "sparkles" as const,
|
||||
@@ -34,7 +35,7 @@ const FEATURES = [
|
||||
description: "New packs added regularly",
|
||||
},
|
||||
{
|
||||
icon: "people" as const,
|
||||
icon: "heart" as const,
|
||||
title: "Share with Partner",
|
||||
description: "Both of you get Pro access",
|
||||
},
|
||||
@@ -125,6 +126,7 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
||||
|
||||
const savings = calculateSavings();
|
||||
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
@@ -134,6 +136,19 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
||||
>
|
||||
<View style={styles.overlay}>
|
||||
<View style={styles.content}>
|
||||
{/* Premium gradient background */}
|
||||
<LinearGradient
|
||||
colors={['rgba(22, 33, 62, 0.98)', 'rgba(13, 13, 26, 1)']}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
{/* Top silk highlight */}
|
||||
<LinearGradient
|
||||
colors={['rgba(212, 175, 55, 0.08)', 'transparent']}
|
||||
style={styles.silkHighlight}
|
||||
/>
|
||||
{/* Premium border accent */}
|
||||
<View style={styles.premiumBorderAccent} />
|
||||
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
@@ -149,15 +164,29 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
||||
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<LinearGradient
|
||||
colors={gradients.primary as [string, string]}
|
||||
style={styles.iconContainer}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
>
|
||||
<Ionicons name="star" size={32} color={colors.text} />
|
||||
</LinearGradient>
|
||||
<Text style={styles.title}>Upgrade to Pro</Text>
|
||||
{/* Premium label */}
|
||||
<Text style={styles.label}>EXCLUSIVE ACCESS</Text>
|
||||
|
||||
{/* Icon with glow */}
|
||||
<View style={styles.iconContainer}>
|
||||
<LinearGradient
|
||||
colors={[colors.premium.gold, colors.premium.goldDark]}
|
||||
style={StyleSheet.absoluteFill}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 1 }}
|
||||
/>
|
||||
<Ionicons name="diamond" size={32} color={colors.background} />
|
||||
</View>
|
||||
|
||||
<Text style={styles.title}>Sauci Pro</Text>
|
||||
|
||||
{/* Decorative separator */}
|
||||
<View style={styles.separator}>
|
||||
<View style={styles.separatorLine} />
|
||||
<View style={styles.separatorDiamond} />
|
||||
<View style={styles.separatorLine} />
|
||||
</View>
|
||||
|
||||
<Text style={styles.subtitle}>
|
||||
Unlock all premium content for you and your partner
|
||||
</Text>
|
||||
@@ -165,13 +194,13 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
||||
|
||||
{/* Features */}
|
||||
<View style={styles.features}>
|
||||
{FEATURES.map((feature) => (
|
||||
{FEATURES.map((feature, index) => (
|
||||
<View key={feature.title} style={styles.featureRow}>
|
||||
<View style={styles.featureIcon}>
|
||||
<Ionicons
|
||||
name={feature.icon}
|
||||
size={20}
|
||||
color={colors.primary}
|
||||
size={18}
|
||||
color={colors.premium.gold}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.featureText}>
|
||||
@@ -180,6 +209,11 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
||||
{feature.description}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons
|
||||
name="checkmark-circle"
|
||||
size={20}
|
||||
color={colors.premium.gold}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
@@ -188,9 +222,9 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
||||
<View style={styles.packages}>
|
||||
{isLoadingOfferings ? (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator color={colors.primary} size="large" />
|
||||
<ActivityIndicator color={colors.premium.gold} size="large" />
|
||||
<Text style={styles.loadingText}>
|
||||
Loading subscription options...
|
||||
Loading options...
|
||||
</Text>
|
||||
</View>
|
||||
) : offerings?.availablePackages ? (
|
||||
@@ -200,17 +234,30 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
||||
const isAnnual = pkg.packageType === "ANNUAL";
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
<Pressable
|
||||
key={pkg.identifier}
|
||||
onPress={() => setSelectedPackage(pkg)}
|
||||
activeOpacity={0.7}
|
||||
style={[
|
||||
styles.packageCard,
|
||||
isSelected && styles.packageCardSelected,
|
||||
isAnnual && styles.packageCardAnnual,
|
||||
]}
|
||||
>
|
||||
{isAnnual && (
|
||||
<LinearGradient
|
||||
colors={['rgba(212, 175, 55, 0.1)', 'rgba(184, 134, 11, 0.05)']}
|
||||
style={StyleSheet.absoluteFill}
|
||||
/>
|
||||
)}
|
||||
{isAnnual && savings && savings > 0 && (
|
||||
<View style={styles.savingsBadge}>
|
||||
<LinearGradient
|
||||
colors={[colors.premium.gold, colors.premium.goldDark]}
|
||||
style={StyleSheet.absoluteFill}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
/>
|
||||
<Ionicons name="star" size={10} color={colors.background} />
|
||||
<Text style={styles.savingsText}>
|
||||
SAVE {savings}%
|
||||
</Text>
|
||||
@@ -226,15 +273,28 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
||||
{isSelected && <View style={styles.radioInner} />}
|
||||
</View>
|
||||
<View style={styles.packageInfo}>
|
||||
<Text style={styles.packageTitle}>
|
||||
{isAnnual ? "Annual" : "Monthly"}
|
||||
</Text>
|
||||
<Text style={styles.packagePrice}>
|
||||
<View style={styles.packageTitleRow}>
|
||||
<Text style={[
|
||||
styles.packageTitle,
|
||||
isAnnual && styles.packageTitleAnnual
|
||||
]}>
|
||||
{isAnnual ? "Annual" : "Monthly"}
|
||||
</Text>
|
||||
{isAnnual && (
|
||||
<View style={styles.bestValueBadge}>
|
||||
<Text style={styles.bestValueText}>BEST VALUE</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Text style={[
|
||||
styles.packagePrice,
|
||||
isAnnual && styles.packagePriceAnnual
|
||||
]}>
|
||||
{formatPrice(pkg)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
@@ -266,7 +326,7 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
||||
|
||||
{/* Purchase Button */}
|
||||
<View style={styles.actions}>
|
||||
<TouchableOpacity
|
||||
<Pressable
|
||||
style={[
|
||||
styles.purchaseButton,
|
||||
(!selectedPackage || isPurchasing) &&
|
||||
@@ -274,21 +334,23 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
||||
]}
|
||||
onPress={handlePurchase}
|
||||
disabled={!selectedPackage || isPurchasing}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
{isPurchasing ? (
|
||||
<ActivityIndicator size="small" color={colors.text} />
|
||||
<View style={styles.purchaseButtonLoading}>
|
||||
<ActivityIndicator size="small" color={colors.background} />
|
||||
</View>
|
||||
) : (
|
||||
<LinearGradient
|
||||
colors={gradients.primary as [string, string]}
|
||||
colors={[colors.premium.gold, colors.premium.goldDark]}
|
||||
style={styles.purchaseButtonGradient}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
>
|
||||
<Text style={styles.purchaseButtonText}>Subscribe Now</Text>
|
||||
<Ionicons name="diamond" size={18} color={colors.background} style={{ marginRight: spacing.sm }} />
|
||||
<Text style={styles.purchaseButtonText}>Unlock Pro</Text>
|
||||
</LinearGradient>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</Pressable>
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.restoreButton}
|
||||
@@ -317,17 +379,36 @@ export function Paywall({ visible, onClose, onSuccess }: PaywallProps) {
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.85)",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
content: {
|
||||
backgroundColor: colors.backgroundLight,
|
||||
backgroundColor: colors.background,
|
||||
borderTopLeftRadius: radius.xxl,
|
||||
borderTopRightRadius: radius.xxl,
|
||||
maxHeight: "90%",
|
||||
borderWidth: 1,
|
||||
borderBottomWidth: 0,
|
||||
borderColor: colors.glass.border,
|
||||
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
silkHighlight: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 150,
|
||||
borderTopLeftRadius: radius.xxl,
|
||||
borderTopRightRadius: radius.xxl,
|
||||
},
|
||||
premiumBorderAccent: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 2,
|
||||
backgroundColor: colors.premium.gold,
|
||||
opacity: 0.4,
|
||||
},
|
||||
scrollContent: {
|
||||
padding: spacing.lg,
|
||||
@@ -341,49 +422,96 @@ const styles = StyleSheet.create({
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: colors.glass.background,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.1)',
|
||||
},
|
||||
header: {
|
||||
alignItems: "center",
|
||||
marginBottom: spacing.xl,
|
||||
},
|
||||
label: {
|
||||
...typography.caption1,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 3,
|
||||
color: colors.premium.gold,
|
||||
textAlign: 'center',
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
overflow: 'hidden',
|
||||
marginBottom: spacing.md,
|
||||
shadowColor: colors.premium.gold,
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: 0.6,
|
||||
shadowRadius: 20,
|
||||
elevation: 12,
|
||||
},
|
||||
title: {
|
||||
...typography.title1,
|
||||
color: colors.text,
|
||||
marginBottom: spacing.xs,
|
||||
textShadowColor: 'rgba(212, 175, 55, 0.3)',
|
||||
textShadowOffset: { width: 0, height: 0 },
|
||||
textShadowRadius: 10,
|
||||
},
|
||||
separator: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginVertical: spacing.md,
|
||||
width: 140,
|
||||
},
|
||||
separatorLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: 'rgba(212, 175, 55, 0.3)',
|
||||
},
|
||||
separatorDiamond: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
backgroundColor: colors.premium.gold,
|
||||
transform: [{ rotate: '45deg' }],
|
||||
marginHorizontal: spacing.md,
|
||||
opacity: 0.8,
|
||||
},
|
||||
subtitle: {
|
||||
...typography.body,
|
||||
color: colors.textSecondary,
|
||||
textAlign: "center",
|
||||
lineHeight: 24,
|
||||
},
|
||||
features: {
|
||||
marginBottom: spacing.xl,
|
||||
backgroundColor: 'rgba(212, 175, 55, 0.05)',
|
||||
borderRadius: radius.lg,
|
||||
padding: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(212, 175, 55, 0.1)',
|
||||
},
|
||||
featureRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: spacing.md,
|
||||
paddingVertical: spacing.sm,
|
||||
},
|
||||
featureIcon: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
backgroundColor: colors.primaryLight,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
backgroundColor: 'rgba(212, 175, 55, 0.1)',
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: spacing.md,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||
},
|
||||
featureText: {
|
||||
flex: 1,
|
||||
@@ -411,30 +539,38 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
packageCard: {
|
||||
position: "relative",
|
||||
backgroundColor: colors.glass.background,
|
||||
backgroundColor: 'rgba(22, 33, 62, 0.4)',
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.glass.border,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255, 255, 255, 0.08)',
|
||||
padding: spacing.md,
|
||||
overflow: "visible",
|
||||
overflow: "hidden",
|
||||
},
|
||||
packageCardSelected: {
|
||||
borderColor: colors.primary,
|
||||
backgroundColor: colors.primaryLight,
|
||||
borderColor: colors.premium.gold,
|
||||
borderWidth: 2,
|
||||
},
|
||||
packageCardAnnual: {
|
||||
borderColor: 'rgba(212, 175, 55, 0.3)',
|
||||
},
|
||||
savingsBadge: {
|
||||
position: "absolute",
|
||||
top: -10,
|
||||
top: -1,
|
||||
right: spacing.md,
|
||||
backgroundColor: colors.primary,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 4,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: 4,
|
||||
borderRadius: radius.xs,
|
||||
borderBottomLeftRadius: radius.sm,
|
||||
borderBottomRightRadius: radius.sm,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
savingsText: {
|
||||
...typography.caption2,
|
||||
color: colors.text,
|
||||
fontWeight: "700",
|
||||
color: colors.background,
|
||||
fontWeight: "800",
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
packageContent: {
|
||||
flexDirection: "row",
|
||||
@@ -445,30 +581,55 @@ const styles = StyleSheet.create({
|
||||
height: 24,
|
||||
borderRadius: 12,
|
||||
borderWidth: 2,
|
||||
borderColor: colors.textSecondary,
|
||||
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
marginRight: spacing.md,
|
||||
},
|
||||
radioOuterSelected: {
|
||||
borderColor: colors.primary,
|
||||
borderColor: colors.premium.gold,
|
||||
},
|
||||
radioInner: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
borderRadius: 6,
|
||||
backgroundColor: colors.primary,
|
||||
backgroundColor: colors.premium.gold,
|
||||
},
|
||||
packageInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
packageTitleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
packageTitle: {
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
},
|
||||
packageTitleAnnual: {
|
||||
color: colors.premium.champagne,
|
||||
},
|
||||
bestValueBadge: {
|
||||
backgroundColor: 'rgba(212, 175, 55, 0.2)',
|
||||
paddingHorizontal: spacing.xs,
|
||||
paddingVertical: 2,
|
||||
borderRadius: radius.xs,
|
||||
},
|
||||
bestValueText: {
|
||||
...typography.caption2,
|
||||
color: colors.premium.gold,
|
||||
fontWeight: '700',
|
||||
letterSpacing: 0.5,
|
||||
},
|
||||
packagePrice: {
|
||||
...typography.subhead,
|
||||
color: colors.textSecondary,
|
||||
marginTop: 2,
|
||||
},
|
||||
packagePriceAnnual: {
|
||||
color: colors.premium.champagne,
|
||||
opacity: 0.8,
|
||||
},
|
||||
errorContainer: {
|
||||
alignItems: "center",
|
||||
@@ -481,16 +642,16 @@ const styles = StyleSheet.create({
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
retryButton: {
|
||||
backgroundColor: colors.glass.background,
|
||||
backgroundColor: 'rgba(212, 175, 55, 0.1)',
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: radius.md,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.glass.border,
|
||||
borderColor: 'rgba(212, 175, 55, 0.2)',
|
||||
},
|
||||
retryButtonText: {
|
||||
...typography.subhead,
|
||||
color: colors.primary,
|
||||
color: colors.premium.gold,
|
||||
fontWeight: "600",
|
||||
},
|
||||
errorBanner: {
|
||||
@@ -510,17 +671,31 @@ const styles = StyleSheet.create({
|
||||
purchaseButton: {
|
||||
borderRadius: radius.lg,
|
||||
overflow: "hidden",
|
||||
shadowColor: colors.premium.gold,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: 0.3,
|
||||
shadowRadius: 8,
|
||||
elevation: 8,
|
||||
},
|
||||
purchaseButtonDisabled: {
|
||||
opacity: 0.5,
|
||||
shadowOpacity: 0,
|
||||
},
|
||||
purchaseButtonGradient: {
|
||||
purchaseButtonLoading: {
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: "center",
|
||||
backgroundColor: colors.premium.gold,
|
||||
},
|
||||
purchaseButtonGradient: {
|
||||
flexDirection: 'row',
|
||||
paddingVertical: spacing.md,
|
||||
alignItems: "center",
|
||||
justifyContent: 'center',
|
||||
},
|
||||
purchaseButtonText: {
|
||||
...typography.headline,
|
||||
color: colors.text,
|
||||
color: colors.background,
|
||||
fontWeight: '700',
|
||||
},
|
||||
restoreButton: {
|
||||
alignItems: "center",
|
||||
@@ -529,7 +704,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
restoreText: {
|
||||
...typography.subhead,
|
||||
color: colors.primary,
|
||||
color: colors.premium.gold,
|
||||
opacity: 0.8,
|
||||
},
|
||||
legal: {
|
||||
paddingHorizontal: spacing.md,
|
||||
|
||||
@@ -0,0 +1,513 @@
|
||||
/**
|
||||
* SwipeCardPremium - PoC of premium/boutique styling for the swipe experience
|
||||
*
|
||||
* Inspired by the "Coming Soon" Ann Summers aesthetic
|
||||
*/
|
||||
import { useState, useEffect } from "react";
|
||||
import { View, Text, StyleSheet, Platform, useWindowDimensions, Pressable } from "react-native";
|
||||
import { Gesture, GestureDetector, TouchableOpacity } from "react-native-gesture-handler";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedStyle,
|
||||
withSpring,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
runOnJS,
|
||||
interpolate,
|
||||
Extrapolate,
|
||||
withTiming,
|
||||
interpolateColor,
|
||||
Easing,
|
||||
FadeIn,
|
||||
} from "react-native-reanimated";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { colors, gradients, featureColors, radius, shadows, blur, typography, spacing, animations } from "../theme";
|
||||
|
||||
// Use feature colors for consistency
|
||||
const { accent: ACCENT } = featureColors.dares;
|
||||
const { accent: ROSE } = featureColors.quiz;
|
||||
|
||||
// RGBA helpers for custom opacity levels (derived from theme colors)
|
||||
// Gold: #D4AF37 = rgb(212, 175, 55)
|
||||
// Rose: #E8A4AE = rgb(232, 164, 174)
|
||||
const rgba = {
|
||||
gold: (opacity: number) => `rgba(212, 175, 55, ${opacity})`,
|
||||
rose: (opacity: number) => `rgba(232, 164, 174, ${opacity})`,
|
||||
};
|
||||
|
||||
// Haptics helper
|
||||
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 MAX_CARD_WIDTH = 400;
|
||||
const SWIPE_THRESHOLD = 100;
|
||||
|
||||
interface Props {
|
||||
question: { id: string; text: string; intensity: number; partner_text?: string | null; is_two_part?: boolean };
|
||||
onSwipe: (direction: "left" | "right" | "up" | "down") => void;
|
||||
}
|
||||
|
||||
export default function SwipeCardPremium({ question, onSwipe }: Props) {
|
||||
const { width: screenWidth } = useWindowDimensions();
|
||||
const cardWidth = Math.min(screenWidth - 48, MAX_CARD_WIDTH);
|
||||
|
||||
const translateX = useSharedValue(0);
|
||||
const translateY = useSharedValue(0);
|
||||
const rotation = useSharedValue(0);
|
||||
const scale = useSharedValue(1);
|
||||
const shadowExpand = useSharedValue(0);
|
||||
const idleBreathing = useSharedValue(0);
|
||||
const isGesturing = useSharedValue(false);
|
||||
|
||||
const context = useSharedValue({ x: 0, y: 0 });
|
||||
|
||||
// Subtle idle breathing animation
|
||||
useEffect(() => {
|
||||
idleBreathing.value = withRepeat(
|
||||
withSequence(
|
||||
withTiming(1, { duration: 3000, easing: Easing.inOut(Easing.sin) }),
|
||||
withTiming(0, { duration: 3000, easing: Easing.inOut(Easing.sin) })
|
||||
),
|
||||
-1,
|
||||
true
|
||||
);
|
||||
}, []);
|
||||
|
||||
const gesture = Gesture.Pan()
|
||||
.activeOffsetX([-15, 15])
|
||||
.activeOffsetY([-15, 15])
|
||||
.onStart(() => {
|
||||
context.value = { x: translateX.value, y: translateY.value };
|
||||
scale.value = withTiming(1.02, { duration: animations.timing.fast });
|
||||
shadowExpand.value = withTiming(1, { duration: animations.timing.fast });
|
||||
isGesturing.value = true;
|
||||
})
|
||||
.onUpdate((event) => {
|
||||
translateX.value = event.translationX + context.value.x;
|
||||
translateY.value = event.translationY + context.value.y;
|
||||
rotation.value = interpolate(
|
||||
translateX.value,
|
||||
[-cardWidth / 2, cardWidth / 2],
|
||||
[-8, 8], // Reduced rotation for elegance
|
||||
Extrapolate.CLAMP
|
||||
);
|
||||
})
|
||||
.onEnd(() => {
|
||||
scale.value = withTiming(1, { duration: animations.timing.fast });
|
||||
shadowExpand.value = withTiming(0, { duration: animations.timing.fast });
|
||||
isGesturing.value = false;
|
||||
|
||||
if (Math.abs(translateX.value) > SWIPE_THRESHOLD) {
|
||||
const direction = translateX.value > 0 ? "right" : "left";
|
||||
runOnJS(triggerHaptic)('medium');
|
||||
translateX.value = withSpring(
|
||||
direction === "right" ? screenWidth * 1.5 : -screenWidth * 1.5,
|
||||
animations.springGentle
|
||||
);
|
||||
runOnJS(onSwipe)(direction);
|
||||
} else if (translateY.value < -SWIPE_THRESHOLD) {
|
||||
runOnJS(triggerHaptic)('medium');
|
||||
translateY.value = withSpring(-screenWidth * 1.5, animations.springGentle);
|
||||
runOnJS(onSwipe)("up");
|
||||
} else if (translateY.value > SWIPE_THRESHOLD) {
|
||||
runOnJS(triggerHaptic)('light');
|
||||
translateY.value = withSpring(screenWidth * 1.5, animations.springGentle);
|
||||
runOnJS(onSwipe)("down");
|
||||
} else {
|
||||
translateX.value = withSpring(0, animations.spring);
|
||||
translateY.value = withSpring(0, animations.spring);
|
||||
rotation.value = withSpring(0, animations.spring);
|
||||
}
|
||||
});
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => {
|
||||
const idleOffset = isGesturing.value ? 0 : interpolate(idleBreathing.value, [0, 1], [0, -4]);
|
||||
return {
|
||||
transform: [
|
||||
{ translateX: translateX.value },
|
||||
{ translateY: translateY.value + idleOffset },
|
||||
{ rotate: `${rotation.value}deg` },
|
||||
{ scale: scale.value },
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const cardShadowStyle = useAnimatedStyle(() => ({
|
||||
shadowOpacity: interpolate(shadowExpand.value, [0, 1], [0.2, 0.4]),
|
||||
shadowRadius: interpolate(shadowExpand.value, [0, 1], [20, 32]),
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: interpolate(shadowExpand.value, [0, 1], [10, 16]),
|
||||
},
|
||||
}));
|
||||
|
||||
const overlayStyle = (direction: "left" | "right" | "up" | "down") =>
|
||||
useAnimatedStyle(() => {
|
||||
let opacity = 0;
|
||||
if (direction === "right") {
|
||||
opacity = interpolate(translateX.value, [0, SWIPE_THRESHOLD], [0, 1]);
|
||||
} else if (direction === "left") {
|
||||
opacity = interpolate(translateX.value, [0, -SWIPE_THRESHOLD], [0, 1]);
|
||||
} else if (direction === "up") {
|
||||
opacity = interpolate(translateY.value, [0, -SWIPE_THRESHOLD], [0, 1]);
|
||||
} else {
|
||||
opacity = interpolate(translateY.value, [0, SWIPE_THRESHOLD], [0, 1]);
|
||||
}
|
||||
return { opacity };
|
||||
});
|
||||
|
||||
const handleButtonPress = async (direction: "left" | "right" | "up") => {
|
||||
await triggerHaptic('light');
|
||||
onSwipe(direction);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.cardWrapper, { width: cardWidth }]}>
|
||||
<GestureDetector gesture={gesture}>
|
||||
<Animated.View style={[styles.cardOuter, animatedStyle, cardShadowStyle, { shadowColor: ACCENT }]}>
|
||||
{/* Premium dark background */}
|
||||
<View style={styles.cardBackground}>
|
||||
{/* Subtle gradient overlay */}
|
||||
<LinearGradient
|
||||
colors={['rgba(22, 33, 62, 0.95)', 'rgba(13, 13, 26, 0.98)']}
|
||||
style={StyleSheet.absoluteFill}
|
||||
start={{ x: 0.5, y: 0 }}
|
||||
end={{ x: 0.5, y: 1 }}
|
||||
/>
|
||||
|
||||
{/* Top silk highlight */}
|
||||
<LinearGradient
|
||||
colors={[rgba.gold(0.08), 'transparent']}
|
||||
style={styles.silkHighlight}
|
||||
start={{ x: 0.5, y: 0 }}
|
||||
end={{ x: 0.5, y: 1 }}
|
||||
/>
|
||||
|
||||
{/* Swipe Overlays - Premium style */}
|
||||
<Animated.View style={[styles.overlay, styles.overlayYes, overlayStyle("right")]} pointerEvents="none">
|
||||
<View style={styles.overlayBadgePremium}>
|
||||
<Text style={styles.overlayTextPremium}>YES</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
<Animated.View style={[styles.overlay, styles.overlayNo, overlayStyle("left")]} pointerEvents="none">
|
||||
<View style={[styles.overlayBadgePremium, styles.overlayBadgeNo]}>
|
||||
<Text style={[styles.overlayTextPremium, styles.overlayTextNo]}>NO</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
<Animated.View style={[styles.overlay, styles.overlayMaybe, overlayStyle("up")]} pointerEvents="none">
|
||||
<View style={[styles.overlayBadgePremium, styles.overlayBadgeMaybe]}>
|
||||
<Text style={[styles.overlayTextPremium, styles.overlayTextMaybe]}>MAYBE</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
<Animated.View style={[styles.overlay, styles.overlaySkip, overlayStyle("down")]} pointerEvents="none">
|
||||
<View style={[styles.overlayBadgePremium, styles.overlayBadgeSkip]}>
|
||||
<Text style={[styles.overlayTextPremium, styles.overlayTextSkip]}>SKIP</Text>
|
||||
</View>
|
||||
</Animated.View>
|
||||
|
||||
{/* Content */}
|
||||
<View style={styles.content}>
|
||||
{/* Intensity badge - more prominent */}
|
||||
<View style={styles.intensityBadge}>
|
||||
<Text style={styles.intensityLabel}>INTENSITY</Text>
|
||||
<View style={styles.intensityRow}>
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Ionicons
|
||||
key={i}
|
||||
name={i < question.intensity ? "flame" : "flame-outline"}
|
||||
size={18}
|
||||
color={i < question.intensity ? ACCENT : 'rgba(255, 255, 255, 0.2)'}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Decorative separator */}
|
||||
<View style={styles.separator}>
|
||||
<View style={styles.separatorLine} />
|
||||
<View style={styles.separatorDiamond} />
|
||||
<View style={styles.separatorLine} />
|
||||
</View>
|
||||
|
||||
{/* Question text */}
|
||||
<Text style={styles.questionText}>
|
||||
{question.text}
|
||||
</Text>
|
||||
|
||||
{/* Bottom separator */}
|
||||
<View style={styles.separator}>
|
||||
<View style={styles.separatorLine} />
|
||||
<View style={styles.separatorDiamond} />
|
||||
<View style={styles.separatorLine} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Premium Action Buttons */}
|
||||
<View style={styles.footer}>
|
||||
<View style={styles.buttonContainer}>
|
||||
<PremiumButton
|
||||
onPress={() => handleButtonPress("left")}
|
||||
icon="close"
|
||||
variant="no"
|
||||
/>
|
||||
<PremiumButton
|
||||
onPress={() => handleButtonPress("up")}
|
||||
icon="help"
|
||||
variant="maybe"
|
||||
small
|
||||
/>
|
||||
<PremiumButton
|
||||
onPress={() => handleButtonPress("right")}
|
||||
icon="heart"
|
||||
variant="yes"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Premium border */}
|
||||
<View style={styles.premiumBorder} pointerEvents="none" />
|
||||
</Animated.View>
|
||||
</GestureDetector>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function PremiumButton({
|
||||
onPress,
|
||||
icon,
|
||||
variant,
|
||||
small = false,
|
||||
}: {
|
||||
onPress: () => void;
|
||||
icon: string;
|
||||
variant: 'yes' | 'no' | 'maybe';
|
||||
small?: boolean;
|
||||
}) {
|
||||
const buttonScale = useSharedValue(1);
|
||||
|
||||
const handlePressIn = () => {
|
||||
buttonScale.value = withSpring(0.92, animations.spring);
|
||||
};
|
||||
|
||||
const handlePressOut = () => {
|
||||
buttonScale.value = withSpring(1, animations.spring);
|
||||
};
|
||||
|
||||
const buttonAnimatedStyle = useAnimatedStyle(() => ({
|
||||
transform: [{ scale: buttonScale.value }],
|
||||
}));
|
||||
|
||||
const getColors = () => {
|
||||
switch (variant) {
|
||||
case 'yes': return { bg: rgba.gold(0.15), border: rgba.gold(0.3), icon: ACCENT };
|
||||
case 'no': return { bg: rgba.rose(0.15), border: rgba.rose(0.3), icon: ROSE };
|
||||
case 'maybe': return { bg: 'rgba(255, 255, 255, 0.08)', border: 'rgba(255, 255, 255, 0.15)', icon: colors.textSecondary };
|
||||
}
|
||||
};
|
||||
|
||||
const c = getColors();
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
activeOpacity={1}
|
||||
>
|
||||
<Animated.View style={[
|
||||
styles.premiumButton,
|
||||
small && styles.premiumButtonSmall,
|
||||
{ backgroundColor: c.bg, borderColor: c.border },
|
||||
buttonAnimatedStyle,
|
||||
]}>
|
||||
<Ionicons
|
||||
name={icon as any}
|
||||
size={small ? 18 : 24}
|
||||
color={c.icon}
|
||||
/>
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
cardWrapper: {
|
||||
position: "absolute",
|
||||
maxWidth: MAX_CARD_WIDTH,
|
||||
height: 500,
|
||||
},
|
||||
cardOuter: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
borderRadius: radius.xl,
|
||||
overflow: "hidden",
|
||||
},
|
||||
cardBackground: {
|
||||
flex: 1,
|
||||
backgroundColor: '#0d0d1a',
|
||||
borderRadius: radius.xl,
|
||||
overflow: "hidden",
|
||||
},
|
||||
silkHighlight: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 120,
|
||||
},
|
||||
premiumBorder: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
borderRadius: radius.xl,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(212, 175, 55, 0.2)', // gold @ 0.2
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: spacing.xl,
|
||||
},
|
||||
intensityBadge: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(212, 175, 55, 0.08)', // gold @ 0.08
|
||||
paddingHorizontal: spacing.lg,
|
||||
paddingVertical: spacing.sm,
|
||||
borderRadius: radius.lg,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(212, 175, 55, 0.15)', // gold @ 0.15
|
||||
marginBottom: spacing.md,
|
||||
},
|
||||
intensityLabel: {
|
||||
...typography.caption2,
|
||||
fontWeight: '600',
|
||||
letterSpacing: 2,
|
||||
color: 'rgba(212, 175, 55, 0.7)', // gold @ 0.7
|
||||
marginBottom: spacing.xs,
|
||||
},
|
||||
intensityRow: {
|
||||
flexDirection: 'row',
|
||||
gap: 4,
|
||||
},
|
||||
separator: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginVertical: spacing.lg,
|
||||
width: 120,
|
||||
},
|
||||
separatorLine: {
|
||||
flex: 1,
|
||||
height: 1,
|
||||
backgroundColor: 'rgba(212, 175, 55, 0.2)', // gold @ 0.2
|
||||
},
|
||||
separatorDiamond: {
|
||||
width: 6,
|
||||
height: 6,
|
||||
backgroundColor: ACCENT,
|
||||
transform: [{ rotate: '45deg' }],
|
||||
marginHorizontal: spacing.md,
|
||||
opacity: 0.5,
|
||||
},
|
||||
questionText: {
|
||||
...typography.title2,
|
||||
color: colors.text,
|
||||
textAlign: "center",
|
||||
lineHeight: 32,
|
||||
paddingHorizontal: spacing.sm,
|
||||
},
|
||||
footer: {
|
||||
paddingBottom: spacing.xl,
|
||||
paddingHorizontal: spacing.lg,
|
||||
},
|
||||
buttonContainer: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
gap: spacing.lg,
|
||||
},
|
||||
premiumButton: {
|
||||
width: 60,
|
||||
height: 60,
|
||||
borderRadius: 30,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
borderWidth: 1,
|
||||
},
|
||||
premiumButtonSmall: {
|
||||
width: 48,
|
||||
height: 48,
|
||||
borderRadius: 24,
|
||||
},
|
||||
// Overlays
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: 10,
|
||||
borderRadius: radius.xl,
|
||||
},
|
||||
overlayYes: {
|
||||
backgroundColor: 'rgba(212, 175, 55, 0.15)', // gold @ 0.15
|
||||
},
|
||||
overlayNo: {
|
||||
backgroundColor: 'rgba(232, 164, 174, 0.15)', // rose @ 0.15
|
||||
},
|
||||
overlayMaybe: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
||||
},
|
||||
overlaySkip: {
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.3)',
|
||||
},
|
||||
overlayBadgePremium: {
|
||||
paddingHorizontal: spacing.xl,
|
||||
paddingVertical: spacing.md,
|
||||
borderRadius: radius.full,
|
||||
backgroundColor: 'rgba(212, 175, 55, 0.2)', // gold @ 0.2
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(212, 175, 55, 0.4)', // gold @ 0.4
|
||||
transform: [{ rotate: "-12deg" }],
|
||||
},
|
||||
overlayBadgeNo: {
|
||||
backgroundColor: 'rgba(232, 164, 174, 0.2)', // rose @ 0.2
|
||||
borderColor: 'rgba(232, 164, 174, 0.4)', // rose @ 0.4
|
||||
},
|
||||
overlayBadgeMaybe: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||
},
|
||||
overlayBadgeSkip: {
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
borderColor: 'rgba(255, 255, 255, 0.15)',
|
||||
},
|
||||
overlayTextPremium: {
|
||||
fontSize: 28,
|
||||
fontWeight: "600",
|
||||
letterSpacing: 4,
|
||||
color: ACCENT,
|
||||
},
|
||||
overlayTextNo: {
|
||||
color: ROSE,
|
||||
},
|
||||
overlayTextMaybe: {
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
overlayTextSkip: {
|
||||
color: colors.textTertiary,
|
||||
fontSize: 24,
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user