import { useEffect, useState } from "react"; import { View, Text, StyleSheet, Pressable, useWindowDimensions, Platform } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { BlurView } from "expo-blur"; import { LinearGradient } from "expo-linear-gradient"; import { Ionicons } from "@expo/vector-icons"; import Animated, { useSharedValue, useAnimatedStyle, withTiming, withRepeat, withSequence, withDelay, withSpring, FadeIn, FadeOut, interpolate, Easing, runOnJS, } from "react-native-reanimated"; import { colors, gradients, radius, shadows, blur, typography, spacing, animations } from "../../theme"; import { Events } from "../../lib/analytics"; interface Props { onComplete: () => void; } interface TutorialStep { key: string; label: string; description: string; icon: string; color: string; gradient: [string, string]; translateX: number; translateY: number; isSwipe: boolean; } const STEPS: TutorialStep[] = [ { key: "right", label: "YES", description: "Swipe right if you're interested", icon: "heart", color: colors.success, gradient: gradients.success as [string, string], translateX: 80, translateY: 0, isSwipe: true, }, { key: "left", label: "NO", description: "Swipe left if you're not interested", icon: "close", color: colors.error, gradient: gradients.error as [string, string], translateX: -80, translateY: 0, isSwipe: true, }, { key: "up", label: "MAYBE", description: "Swipe up if you're unsure", icon: "help", color: colors.warning, gradient: gradients.warning as [string, string], translateX: 0, translateY: -80, isSwipe: true, }, { key: "down", label: "SKIP", description: "Swipe down to skip for now", icon: "arrow-down", color: colors.textTertiary, gradient: ["#6c757d", "#495057"], translateX: 0, translateY: 80, isSwipe: true, }, { key: "feedback", label: "FEEDBACK", description: "Tap the flag to report issues with a question", icon: "flag-outline", color: colors.primary, gradient: gradients.primary as [string, string], translateX: 0, translateY: 0, isSwipe: false, }, ]; export default function SwipeTutorial({ onComplete }: Props) { const { width: screenWidth, height: screenHeight } = useWindowDimensions(); const insets = useSafeAreaInsets(); const [currentStep, setCurrentStep] = useState(0); const cardTranslateX = useSharedValue(0); const cardTranslateY = useSharedValue(0); const cardRotation = useSharedValue(0); const cardScale = useSharedValue(1); const overlayOpacity = useSharedValue(0); const pulseScale = useSharedValue(1); const handOffset = useSharedValue(0); const currentStep_ = STEPS[currentStep]; const isSwipeStep = currentStep_.isSwipe; // Track tutorial started on mount useEffect(() => { Events.tutorialStarted("swipe"); Events.tutorialStepViewed("swipe", 0, STEPS[0].key); }, []); // Track step changes useEffect(() => { if (currentStep > 0) { Events.tutorialStepViewed("swipe", currentStep, STEPS[currentStep].key); } }, [currentStep]); useEffect(() => { // Reset animations for current step cardTranslateX.value = 0; cardTranslateY.value = 0; cardRotation.value = 0; overlayOpacity.value = 0; handOffset.value = 0; // Start the swipe demonstration animation const animateSwipe = () => { if (!isSwipeStep) { // For non-swipe steps (feedback), just pulse the card cardScale.value = withRepeat( withSequence( withTiming(1.02, { duration: 600 }), withTiming(1, { duration: 600 }) ), -1, true ); return; } // Pulse the card first cardScale.value = withSequence( withTiming(1.02, { duration: 200 }), withTiming(1, { duration: 200 }) ); // Animate hand movement handOffset.value = withRepeat( withSequence( withTiming(1, { duration: 800, easing: Easing.out(Easing.cubic) }), withDelay(300, withTiming(0, { duration: 400 })) ), -1, false ); // Animate card swipe cardTranslateX.value = withRepeat( withSequence( withDelay(200, withTiming(currentStep_.translateX * 0.7, { duration: 600, easing: Easing.out(Easing.cubic) })), withDelay(400, withSpring(0, animations.spring)) ), -1, false ); cardTranslateY.value = withRepeat( withSequence( withDelay(200, withTiming(currentStep_.translateY * 0.7, { duration: 600, easing: Easing.out(Easing.cubic) })), withDelay(400, withSpring(0, animations.spring)) ), -1, false ); // Rotate card for horizontal swipes if (currentStep_.translateX !== 0) { cardRotation.value = withRepeat( withSequence( withDelay(200, withTiming(currentStep_.translateX > 0 ? 8 : -8, { duration: 600 })), withDelay(400, withSpring(0, animations.spring)) ), -1, false ); } // Show overlay during swipe overlayOpacity.value = withRepeat( withSequence( withDelay(400, withTiming(0.8, { duration: 400 })), withDelay(200, withTiming(0, { duration: 400 })) ), -1, false ); }; // Pulse animation for the indicator pulseScale.value = withRepeat( withSequence( withTiming(1.2, { duration: 800 }), withTiming(1, { duration: 800 }) ), -1, true ); const timeout = setTimeout(animateSwipe, 300); return () => clearTimeout(timeout); }, [currentStep]); const handleNext = () => { if (currentStep < STEPS.length - 1) { setCurrentStep(currentStep + 1); } else { Events.tutorialCompleted("swipe"); onComplete(); } }; const handleSkip = () => { Events.tutorialSkipped("swipe", currentStep); onComplete(); }; const cardAnimatedStyle = useAnimatedStyle(() => ({ transform: [ { translateX: cardTranslateX.value }, { translateY: cardTranslateY.value }, { rotate: `${cardRotation.value}deg` }, { scale: cardScale.value }, ], })); const overlayAnimatedStyle = useAnimatedStyle(() => ({ opacity: overlayOpacity.value, })); const pulseAnimatedStyle = useAnimatedStyle(() => ({ transform: [{ scale: pulseScale.value }], opacity: interpolate(pulseScale.value, [1, 1.2], [0.6, 0.2]), })); const handAnimatedStyle = useAnimatedStyle(() => { const progress = handOffset.value; return { transform: [ { translateX: currentStep_.translateX * progress * 0.5 }, { translateY: currentStep_.translateY * progress * 0.5 }, ], opacity: interpolate(progress, [0, 0.2, 0.8, 1], [0, 1, 1, 0]), }; }); return ( {/* Skip button */} Skip {/* Title */} How to Play Swipe to answer questions and find your matches {/* Demo Card Area */} {/* Direction indicators around the card - only for swipe steps */} {isSwipeStep && ( {STEPS.filter(s => s.isSwipe).map((dir, index) => ( {index === currentStep && ( )} ))} )} {/* Demo Card */} {/* Feedback button indicator */} {!isSwipeStep && ( )} {/* Overlay - only for swipe steps */} {isSwipeStep && ( {currentStep_.label} )} {/* Card content */} Would you try a new restaurant together? {/* Hand gesture indicator - only for swipe steps */} {isSwipeStep && ( )} {/* Current instruction */} {currentStep_.label} {currentStep_.description} {/* Progress dots */} {STEPS.map((_, index) => ( ))} {/* Next button */} [ styles.nextButton, pressed && styles.nextButtonPressed ]}> {currentStep < STEPS.length - 1 ? "Next" : "Got it!"} ); } const styles = StyleSheet.create({ container: { ...StyleSheet.absoluteFillObject, zIndex: 1000, }, blur: { flex: 1, backgroundColor: 'rgba(13, 13, 26, 0.85)', }, content: { flex: 1, paddingHorizontal: spacing.lg, paddingTop: 80, alignItems: "center", }, skipButton: { position: "absolute", top: 60, right: spacing.lg, paddingVertical: spacing.sm, paddingHorizontal: spacing.md, }, skipText: { ...typography.callout, color: colors.textSecondary, }, title: { ...typography.title1, color: colors.text, textAlign: "center", marginBottom: spacing.sm, }, subtitle: { ...typography.body, color: colors.textSecondary, textAlign: "center", marginBottom: spacing.xl, }, demoArea: { flex: 1, justifyContent: "center", alignItems: "center", width: "100%", maxHeight: 350, }, indicatorsContainer: { position: "absolute", width: 280, height: 280, }, directionIndicator: { position: "absolute", alignItems: "center", justifyContent: "center", }, indicatorRight: { right: -20, top: "50%", marginTop: -24, }, indicatorLeft: { left: -20, top: "50%", marginTop: -24, }, indicatorUp: { top: -20, left: "50%", marginLeft: -24, }, indicatorDown: { bottom: -20, left: "50%", marginLeft: -24, }, pulseRing: { position: "absolute", width: 64, height: 64, borderRadius: 32, justifyContent: "center", alignItems: "center", }, pulseInner: { width: "100%", height: "100%", borderRadius: 32, }, indicatorBadge: { width: 48, height: 48, borderRadius: 24, justifyContent: "center", alignItems: "center", borderWidth: 1, borderColor: colors.glass.border, }, indicatorActive: { borderColor: "transparent", }, demoCard: { width: 220, height: 280, borderRadius: radius.xl, overflow: "visible", }, demoCardInner: { flex: 1, borderRadius: radius.xl, borderWidth: 1, borderColor: colors.glass.border, overflow: "hidden", }, feedbackButtonDemo: { position: 'absolute', top: spacing.md, right: spacing.md, width: 36, height: 36, borderRadius: 18, backgroundColor: colors.glass.background, justifyContent: 'center', alignItems: 'center', zIndex: 100, borderWidth: 1, borderColor: colors.glass.border, }, feedbackButtonHighlight: { backgroundColor: colors.primary, borderColor: colors.primary, }, feedbackPulse: { position: "absolute", width: 56, height: 56, borderRadius: 28, justifyContent: "center", alignItems: "center", }, demoOverlay: { ...StyleSheet.absoluteFillObject, justifyContent: "center", alignItems: "center", zIndex: 10, borderRadius: radius.xl, }, overlayBadge: { paddingHorizontal: spacing.md, paddingVertical: spacing.xs, borderRadius: radius.sm, transform: [{ rotate: "-15deg" }], }, overlayText: { fontSize: 24, fontWeight: "900", color: colors.text, letterSpacing: 2, }, demoCardContent: { flex: 1, justifyContent: "center", alignItems: "center", padding: spacing.lg, }, intensityDemo: { flexDirection: "row", backgroundColor: colors.primaryLight, paddingHorizontal: spacing.sm, paddingVertical: spacing.xs, borderRadius: radius.full, gap: 2, marginBottom: spacing.md, }, demoQuestion: { ...typography.headline, color: colors.text, textAlign: "center", }, handIndicator: { position: "absolute", bottom: 40, left: "50%", marginLeft: -20, }, handCircle: { width: 40, height: 40, borderRadius: 20, backgroundColor: colors.primary, justifyContent: "center", alignItems: "center", ...shadows.md, }, instructionContainer: { alignItems: "center", marginTop: spacing.xl, marginBottom: spacing.lg, }, instructionBadge: { flexDirection: "row", alignItems: "center", paddingHorizontal: spacing.lg, paddingVertical: spacing.sm, borderRadius: radius.full, gap: spacing.sm, marginBottom: spacing.md, }, instructionLabel: { ...typography.headline, color: colors.text, fontWeight: "700", }, instructionText: { ...typography.body, color: colors.textSecondary, textAlign: "center", }, dotsContainer: { flexDirection: "row", gap: spacing.sm, marginBottom: spacing.xl, }, dot: { width: 8, height: 8, borderRadius: 4, backgroundColor: colors.glass.border, }, dotActive: { backgroundColor: colors.primary, width: 24, }, dotCompleted: { backgroundColor: colors.primary, }, nextButton: { width: "100%", maxWidth: 280, borderRadius: radius.lg, overflow: "hidden", }, nextButtonPressed: { opacity: 0.9, transform: [{ scale: 0.98 }], }, nextButtonGradient: { flexDirection: "row", alignItems: "center", justifyContent: "center", paddingVertical: spacing.md, gap: spacing.sm, }, nextButtonText: { ...typography.headline, color: colors.text, }, });