import { useState, useRef } from "react"; import { View, Text, StyleSheet, Dimensions, Animated, PanResponder, TouchableOpacity } from "react-native"; import { Ionicons } from "@expo/vector-icons"; const SCREEN_WIDTH = Dimensions.get("window").width; const SWIPE_THRESHOLD = SCREEN_WIDTH * 0.25; interface Props { question: { id: string; text: string; intensity: number; partner_text?: string | null }; onSwipe: (direction: "left" | "right" | "up") => void; } /** * Web-compatible SwipeCard using React Native's built-in PanResponder * instead of react-native-gesture-handler which doesn't work on web. */ export default function SwipeCard({ question, onSwipe }: Props) { const [dismissed, setDismissed] = useState(false); const [hoveredButton, setHoveredButton] = useState(null); const translateX = useRef(new Animated.Value(0)).current; const translateY = useRef(new Animated.Value(0)).current; const scale = useRef(new Animated.Value(1)).current; const rotation = translateX.interpolate({ inputRange: [-SCREEN_WIDTH / 2, SCREEN_WIDTH / 2], outputRange: ["-15deg", "15deg"], extrapolate: "clamp", }); const rightOverlayOpacity = translateX.interpolate({ inputRange: [0, SWIPE_THRESHOLD], outputRange: [0, 1], extrapolate: "clamp", }); const leftOverlayOpacity = translateX.interpolate({ inputRange: [-SWIPE_THRESHOLD, 0], outputRange: [1, 0], extrapolate: "clamp", }); const upOverlayOpacity = translateY.interpolate({ inputRange: [-SWIPE_THRESHOLD, 0], outputRange: [1, 0], extrapolate: "clamp", }); const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: (_, gestureState) => { // Don't activate if touch is in footer area (bottom 100px) const CARD_HEIGHT = 500; const FOOTER_START_Y = CARD_HEIGHT - 100; return gestureState.y0 < FOOTER_START_Y; }, onMoveShouldSetPanResponder: (_, gestureState) => { // Don't activate if touch is in footer area const CARD_HEIGHT = 500; const FOOTER_START_Y = CARD_HEIGHT - 100; return gestureState.y0 < FOOTER_START_Y && (Math.abs(gestureState.dx) > 5 || Math.abs(gestureState.dy) > 5); }, onPanResponderGrant: () => { Animated.timing(scale, { toValue: 1.05, duration: 100, useNativeDriver: true, }).start(); }, onPanResponderMove: (_, gestureState) => { translateX.setValue(gestureState.dx); translateY.setValue(gestureState.dy); }, onPanResponderRelease: (_, gestureState) => { Animated.timing(scale, { toValue: 1, duration: 100, useNativeDriver: true, }).start(); if (Math.abs(gestureState.dx) > SWIPE_THRESHOLD) { // Horizontal swipe const direction = gestureState.dx > 0 ? "right" : "left"; const targetX = direction === "right" ? SCREEN_WIDTH * 1.5 : -SCREEN_WIDTH * 1.5; Animated.spring(translateX, { toValue: targetX, useNativeDriver: true, }).start(() => { setDismissed(true); onSwipe(direction); }); } else if (gestureState.dy < -SWIPE_THRESHOLD) { // Vertical swipe up Animated.spring(translateY, { toValue: -SCREEN_WIDTH * 1.5, useNativeDriver: true, }).start(() => { setDismissed(true); onSwipe("up"); }); } else { // Reset position Animated.parallel([ Animated.spring(translateX, { toValue: 0, useNativeDriver: true, }), Animated.spring(translateY, { toValue: 0, useNativeDriver: true, }), ]).start(); } }, }) ).current; if (dismissed) { return null; } return ( {/* Overlays */} YES NO MAYBE {[...Array(question.intensity)].map((_, i) => ( ))} {question.text} onSwipe("left")} onMouseEnter={() => setHoveredButton("no")} onMouseLeave={() => setHoveredButton(null)} activeOpacity={0.7} > onSwipe("up")} onMouseEnter={() => setHoveredButton("maybe")} onMouseLeave={() => setHoveredButton(null)} activeOpacity={0.7} > onSwipe("right")} onMouseEnter={() => setHoveredButton("yes")} onMouseLeave={() => setHoveredButton(null)} activeOpacity={0.7} > ); } const styles = StyleSheet.create({ card: { position: "absolute", width: SCREEN_WIDTH - 48, height: 500, backgroundColor: "#16213e", borderRadius: 24, borderWidth: 1, borderColor: "#0f3460", shadowColor: "#000", shadowOffset: { width: 0, height: 4, }, shadowOpacity: 0.3, shadowRadius: 4.65, elevation: 8, overflow: "hidden", cursor: "grab", }, content: { flex: 1, justifyContent: "center", alignItems: "center", padding: 32, }, intensityContainer: { flexDirection: "row", marginBottom: 24, backgroundColor: "rgba(233, 69, 96, 0.1)", paddingHorizontal: 12, paddingVertical: 6, borderRadius: 12, }, text: { fontSize: 24, fontWeight: "bold", color: "#fff", textAlign: "center", lineHeight: 32, }, twoPartText: { fontSize: 20, lineHeight: 28, }, footer: { paddingTop: 8, paddingBottom: 16, paddingHorizontal: 8, alignItems: "center", backgroundColor: "rgba(0,0,0,0.2)", }, hint: { color: "#666", fontSize: 12, marginBottom: 4, }, overlay: { ...StyleSheet.absoluteFillObject, justifyContent: "center", alignItems: "center", zIndex: 10, }, overlayText: { fontSize: 48, fontWeight: "900", color: "#fff", textShadowColor: "rgba(0,0,0,0.5)", textShadowOffset: { width: 0, height: 2 }, textShadowRadius: 4, transform: [{ rotate: "-15deg" }], borderWidth: 4, borderColor: "#fff", paddingHorizontal: 16, paddingVertical: 8, borderRadius: 12, }, partnerContainer: { width: "100%", alignItems: "center", }, divider: { height: 1, width: "80%", backgroundColor: "rgba(255,255,255,0.1)", marginVertical: 24, position: "relative", justifyContent: "center", alignItems: "center", }, dividerText: { color: "#666", backgroundColor: "#16213e", paddingHorizontal: 10, fontSize: 12, fontWeight: "bold", position: "absolute", }, buttonContainer: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", paddingHorizontal: 32, paddingVertical: 16, gap: 20, }, actionButton: { alignItems: "center", justifyContent: "center", width: 64, height: 64, borderRadius: 32, shadowColor: "#000", shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.4, shadowRadius: 8, elevation: 8, borderWidth: 2, overflow: "hidden", transition: "transform 0.2s ease, box-shadow 0.2s ease", }, noButton: { backgroundColor: "#E74C3C", borderColor: "rgba(255, 255, 255, 0.3)", }, maybeButton: { backgroundColor: "#F39C12", borderColor: "rgba(255, 255, 255, 0.3)", }, yesButton: { backgroundColor: "#2ECC71", borderColor: "rgba(255, 255, 255, 0.3)", }, buttonInnerGlow: { position: "absolute", top: 0, left: 0, right: 0, height: "50%", backgroundColor: "rgba(255, 255, 255, 0.15)", borderTopLeftRadius: 32, borderTopRightRadius: 32, }, buttonIcon: { zIndex: 1, textShadowColor: "rgba(0, 0, 0, 0.3)", textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 2, }, buttonHovered: { transform: [{ scale: 1.1 }], shadowOpacity: 0.6, shadowRadius: 12, }, });