import { useState, useEffect } from "react";
import { View, Text, StyleSheet, Platform, TouchableOpacity } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
withRepeat,
withSequence,
withTiming,
interpolate,
Easing,
FadeInDown,
} from "react-native-reanimated";
import { Ionicons } from "@expo/vector-icons";
import { colors, gradients, radius, shadows, typography, spacing, animations } from "../../theme";
import { QuestionFeedbackModal } from "../feedback";
import { SharePreviewModal } from "../share";
const ACTION_BUTTON_SIZE = 72;
const ACTION_BUTTON_OVERHANG_PERCENT = 0.5; // 50% of button hangs below the card
const actionButtonOffset = ACTION_BUTTON_SIZE * ACTION_BUTTON_OVERHANG_PERCENT;
const actionButtonBackground = '#3a3a42';
// Haptics helper - not supported on web
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);
};
export interface QuestionCardProps {
question: {
id: string;
text: string;
intensity: number;
partner_text?: string | null;
is_two_part?: boolean;
partner_answered?: boolean;
question_type?: string;
};
packInfo?: { name: string; icon: string; color?: string } | null;
onAnswer: (answer: 'yes' | 'no' | 'skip') => void;
onReport?: () => void;
}
export default function QuestionCard({ question, packInfo, onAnswer, onReport }: QuestionCardProps) {
const [showFeedbackModal, setShowFeedbackModal] = useState(false);
const [showShareModal, setShowShareModal] = useState(false);
const idleBreathing = useSharedValue(0);
const handleSharePress = async () => {
await triggerHaptic('light');
setShowShareModal(true);
};
// Subtle idle breathing animation
useEffect(() => {
idleBreathing.value = withRepeat(
withSequence(
withTiming(1, { duration: 2000, easing: Easing.inOut(Easing.sin) }),
withTiming(0, { duration: 2000, easing: Easing.inOut(Easing.sin) })
),
-1,
true
);
}, []);
const animatedStyle = useAnimatedStyle(() => {
const idleOffset = interpolate(idleBreathing.value, [0, 1], [0, -4]);
return {
transform: [{ translateY: idleOffset }],
};
});
const handleButtonPress = async (answer: 'yes' | 'no' | 'skip') => {
await triggerHaptic(answer === 'skip' ? 'light' : 'medium');
onAnswer(answer);
};
const handleFeedbackPress = () => {
if (onReport) {
onReport();
} else {
setShowFeedbackModal(true);
}
};
return (
<>
setShowFeedbackModal(false)}
questionId={question.id}
questionText={question.text}
/>
setShowShareModal(false)}
question={question}
packName={packInfo?.name}
cardColor={packInfo?.color}
/>
>
);
}
function CardContent({
question,
packInfo,
handleButtonPress,
onFeedbackPress,
onSharePress,
}: {
question: QuestionCardProps['question'];
packInfo?: { name: string; icon: string } | null;
handleButtonPress: (answer: 'yes' | 'no' | 'skip') => void;
onFeedbackPress: () => void;
onSharePress: () => void;
}) {
return (
{/* Header Row - Pack badge and action buttons */}
{/* Pack Badge */}
{packInfo && (
{packInfo.name}
)}
{!packInfo && }
{/* Action Buttons */}
{/* Content - Question text centered */}
{question.text}
{/* Partner's version of the question - only show when user is answering first */}
{question.partner_text && !question.partner_answered && (
Partner sees
{question.partner_text}
)}
{/* Footer - Action Buttons pinned to bottom */}
handleButtonPress("no")}
icon="close"
iconColor={colors.error}
label="NO"
/>
{/* Skip button in between */}
handleButtonPress("skip")}
activeOpacity={0.7}
>
Skip
handleButtonPress("yes")}
icon="checkmark"
iconColor={colors.success}
label="YES"
/>
);
}
function ActionButton({
onPress,
icon,
iconColor,
label,
small = false,
}: {
onPress: () => void;
icon: string;
iconColor: string;
label: string;
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 }],
}));
return (
);
}
const styles = StyleSheet.create({
cardWrapper: {
width: "90%",
maxWidth: 400,
flex: 1,
maxHeight: 580 - actionButtonOffset, // Account for button overhang
},
cardOuter: {
flex: 1,
borderRadius: radius.xxl,
overflow: "visible",
},
gradientContainer: {
flex: 1,
borderRadius: radius.xxl,
overflow: "visible",
},
cardInner: {
flex: 1,
paddingTop: spacing.md,
paddingBottom: spacing.sm,
paddingHorizontal: spacing.lg,
},
// Header row styles
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: spacing.md,
},
headerSpacer: {
flex: 1,
},
packBadge: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.15)',
paddingHorizontal: spacing.md,
paddingVertical: spacing.sm,
borderRadius: radius.full,
gap: spacing.xs,
maxWidth: '60%',
},
packBadgeText: {
...typography.caption1,
fontWeight: '600',
color: colors.text,
},
headerActions: {
flexDirection: 'row',
alignItems: 'center',
gap: spacing.xs,
},
headerButton: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: 'rgba(255,255,255,0.12)',
justifyContent: 'center',
alignItems: 'center',
},
// Content styles
content: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: spacing.sm,
},
text: {
...typography.title1,
color: colors.text,
textAlign: "center",
lineHeight: 36,
},
twoPartText: {
...typography.title2,
lineHeight: 32,
},
partnerTextContainer: {
marginTop: spacing.lg,
width: '100%',
paddingHorizontal: spacing.sm,
},
partnerDivider: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: spacing.sm,
},
partnerDividerLine: {
flex: 1,
height: 1,
backgroundColor: 'rgba(255,255,255,0.2)',
},
partnerLabelContainer: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: spacing.md,
paddingVertical: spacing.xs,
backgroundColor: 'rgba(255,255,255,0.15)',
borderRadius: radius.full,
gap: spacing.xs,
},
partnerLabel: {
...typography.caption1,
color: 'rgba(255,255,255,0.8)',
fontWeight: '500',
letterSpacing: 0.3,
},
partnerText: {
...typography.subhead,
color: 'rgba(255,255,255,0.7)',
textAlign: 'center',
fontStyle: 'italic',
},
// Footer styles
footer: {
paddingTop: spacing.md,
transform: [{ translateY: actionButtonOffset }],
},
buttonContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-end",
width: '100%',
paddingHorizontal: spacing.md,
},
actionButtonWrapper: {
alignItems: 'center',
gap: spacing.xs,
},
actionButton: {
width: 72,
height: 72,
borderRadius: 36,
justifyContent: "center",
alignItems: "center",
overflow: "hidden",
elevation: 4,
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 4,
},
actionButtonSmall: {
width: 52,
height: 52,
borderRadius: 26,
},
buttonIcon: {
zIndex: 1,
},
skipButtonCenter: {
paddingVertical: spacing.sm,
paddingHorizontal: spacing.md,
marginBottom: 30, // Align above center of 72px buttons
},
skipText: {
...typography.caption1,
color: 'rgba(255,255,255,0.5)',
fontWeight: '600',
},
});