/** * MatchNotificationModal - Premium celebration modal for new matches * * Features: * - Glass-morphism styling following boutique design aesthetic * - Animated floating hearts celebration effect * - Shows what each partner answered (You / Partner) * - Uses brand gradients and premium styling */ import React, { useEffect, useRef } from 'react'; import { View, Text, StyleSheet, Pressable, Animated, Keyboard, Platform, } from 'react-native'; import * as Haptics from 'expo-haptics'; import { LinearGradient } from 'expo-linear-gradient'; import { BlurView } from 'expo-blur'; import { Ionicons } from '@expo/vector-icons'; import { colors, gradients, spacing, radius, typography, shadows } from '../theme'; import { DecorativeSeparator } from './ui'; import type { MatchType, AnswerType } from '../types'; export interface MatchNotificationData { id: string; match_type: MatchType; question: { id: string; text: string; partner_text?: string | null; }; userResponse?: AnswerType; partnerResponse?: AnswerType; partnerName?: string | null; } interface Props { visible: boolean; match: MatchNotificationData | null; onDismiss: () => void; onChat: () => void; } // Helper to get response styling const getResponseStyle = (response: AnswerType, isPartner: boolean = false) => { switch (response) { case 'yes': return { bg: isPartner ? 'rgba(46, 204, 113, 0.15)' : 'rgba(233, 69, 96, 0.15)', border: isPartner ? 'rgba(46, 204, 113, 0.3)' : 'rgba(233, 69, 96, 0.3)', color: isPartner ? colors.success : colors.primary, text: 'YES', icon: 'heart' as const, }; case 'maybe': return { bg: 'rgba(243, 156, 18, 0.15)', border: 'rgba(243, 156, 18, 0.3)', color: colors.warning, text: 'MAYBE', icon: 'help-circle' as const, }; default: return { bg: 'rgba(255, 255, 255, 0.1)', border: 'rgba(255, 255, 255, 0.2)', color: colors.textSecondary, text: response.toUpperCase(), icon: 'ellipse' as const, }; } }; // Compact response badge (for two-part questions) const ResponseBadge: React.FC<{ response: AnswerType; isPartner?: boolean; }> = ({ response, isPartner = false }) => { const style = getResponseStyle(response, isPartner); return ( {style.text} ); }; export const MatchNotificationModal: React.FC = ({ visible, match, onDismiss, onChat, }) => { const modalScale = useRef(new Animated.Value(0.8)).current; const modalOpacity = useRef(new Animated.Value(0)).current; const overlayOpacity = useRef(new Animated.Value(0)).current; useEffect(() => { if (visible) { // Dismiss keyboard so it doesn't block the modal buttons Keyboard.dismiss(); // Trigger celebration haptic pattern if (Platform.OS !== 'web') { // Success notification followed by a heavy impact for extra punch Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); setTimeout(() => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy); }, 150); } // Reset animations modalScale.setValue(0.8); modalOpacity.setValue(0); overlayOpacity.setValue(0); // Animate in Animated.parallel([ Animated.timing(overlayOpacity, { toValue: 1, duration: 250, useNativeDriver: true, }), Animated.spring(modalScale, { toValue: 1, tension: 80, friction: 10, useNativeDriver: true, }), Animated.timing(modalOpacity, { toValue: 1, duration: 200, useNativeDriver: true, }), ]).start(); } }, [visible]); const handleDismiss = () => { Animated.parallel([ Animated.timing(overlayOpacity, { toValue: 0, duration: 200, useNativeDriver: true, }), Animated.timing(modalScale, { toValue: 0.8, duration: 200, useNativeDriver: true, }), Animated.timing(modalOpacity, { toValue: 0, duration: 150, useNativeDriver: true, }), ]).start(() => onDismiss()); }; const handleChat = () => { Animated.parallel([ Animated.timing(overlayOpacity, { toValue: 0, duration: 200, useNativeDriver: true, }), Animated.timing(modalScale, { toValue: 0.8, duration: 200, useNativeDriver: true, }), Animated.timing(modalOpacity, { toValue: 0, duration: 150, useNativeDriver: true, }), ]).start(() => onChat()); }; if (!visible || !match || !match.question) return null; // Determine responses based on match_type if not explicitly provided const getUserResponse = (): AnswerType => { if (match.userResponse) return match.userResponse; // Infer from match_type (user typically triggers the match) if (match.match_type === 'yes_yes') return 'yes'; if (match.match_type === 'maybe_maybe') return 'maybe'; // yes_maybe - user could be either return 'yes'; }; const getPartnerResponse = (): AnswerType => { if (match.partnerResponse) return match.partnerResponse; // Infer from match_type if (match.match_type === 'yes_yes') return 'yes'; if (match.match_type === 'maybe_maybe') return 'maybe'; // yes_maybe - partner could be either return 'maybe'; }; const userResponse = getUserResponse(); const partnerResponse = getPartnerResponse(); // Get match type styling const getMatchTypeLabel = () => { switch (match.match_type) { case 'yes_yes': return { text: 'Perfect Match', color: colors.primary }; case 'yes_maybe': return { text: 'Curious Match', color: colors.warning }; case 'maybe_maybe': return { text: 'Exploring Together', color: colors.premium.rose }; default: return { text: 'Match', color: colors.primary }; } }; const matchTypeInfo = getMatchTypeLabel(); // Use partner's name or fallback to "Partner" const partnerDisplayName = match.partnerName || 'Partner'; return ( {/* Background press to dismiss */} {/* Modal card with glow */} {/* Outer glow effect */} {/* Glass background */} {/* Content */} {/* Title section */} It's a Match! {/* Match type badge */} {matchTypeInfo.text} {/* Question text and responses */} {match.question.partner_text ? ( // Two-part question - show what each person saw // The current user triggered the match (answered second), so they saw partner_text // The partner answered first, so they saw text You said "{match.question.partner_text}" {partnerDisplayName} said "{match.question.text}" ) : ( // Single question - show shared text with inline responses "{match.question.text}" You said {userResponse.toUpperCase()} ยท {partnerDisplayName} said {partnerResponse.toUpperCase()} )} {/* Action buttons */} Chat Continue {/* Border */} ); }; const styles = StyleSheet.create({ overlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0, 0, 0, 0.75)', justifyContent: 'center', alignItems: 'center', zIndex: 9999, }, glowContainer: { width: '88%', maxWidth: 360, alignItems: 'center', justifyContent: 'center', }, glowOuter: { position: 'absolute', top: -3, left: -3, right: -3, bottom: -3, borderRadius: radius.xl + 3, backgroundColor: 'transparent', borderWidth: 1, borderColor: 'rgba(233, 69, 96, 0.15)', shadowColor: colors.primary, shadowOffset: { width: 0, height: 0 }, shadowOpacity: 0.2, shadowRadius: 20, elevation: 8, }, modalContainer: { width: '100%', borderRadius: radius.xl, overflow: 'hidden', }, blurContainer: { ...StyleSheet.absoluteFillObject, }, gradientOverlay: { ...StyleSheet.absoluteFillObject, }, content: { padding: spacing.xl, paddingTop: spacing.lg, alignItems: 'center', }, title: { ...typography.title2, color: colors.text, textAlign: 'center', marginBottom: spacing.sm, }, matchTypeBadge: { backgroundColor: 'rgba(255, 255, 255, 0.05)', paddingHorizontal: spacing.lg, paddingVertical: spacing.sm, borderRadius: radius.full, borderWidth: 1, marginBottom: spacing.lg, }, matchTypeText: { ...typography.caption1, fontWeight: '600', letterSpacing: 1, }, questionContainer: { alignItems: 'center', }, questionText: { ...typography.body, color: colors.text, textAlign: 'center', fontStyle: 'italic', lineHeight: 24, paddingHorizontal: spacing.sm, marginBottom: spacing.md, }, responsesInline: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', }, responseInlineText: { ...typography.caption1, color: colors.textSecondary, }, responseInlineAnswer: { ...typography.caption1, fontWeight: '700', }, responseBadgeCompact: { flexDirection: 'row', alignItems: 'center', gap: spacing.xs, paddingHorizontal: spacing.sm, paddingVertical: spacing.xs, borderRadius: radius.md, borderWidth: 1, }, responseBadgeTextCompact: { ...typography.caption1, fontWeight: '600', letterSpacing: 0.5, }, // Two-part question styles twoPartContainer: { width: '100%', marginBottom: spacing.md, }, responseSection: { alignItems: 'center', paddingHorizontal: spacing.sm, }, responseLabelRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, marginBottom: spacing.xs, }, responsePersonLabel: { ...typography.caption1, color: colors.textSecondary, fontWeight: '500', }, questionTextSmall: { ...typography.subhead, color: colors.text, textAlign: 'center', fontStyle: 'italic', lineHeight: 20, }, matchConnector: { flexDirection: 'row', alignItems: 'center', marginVertical: spacing.md, paddingHorizontal: spacing.xl, }, connectorLine: { flex: 1, height: 1, backgroundColor: 'rgba(233, 69, 96, 0.2)', }, connectorHeart: { marginHorizontal: spacing.sm, opacity: 0.8, }, buttonRow: { flexDirection: 'row', gap: spacing.md, width: '100%', }, chatButton: { flex: 1, borderRadius: radius.lg, overflow: 'hidden', ...shadows.md, }, chatButtonGradient: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: spacing.sm, paddingVertical: spacing.md, }, chatButtonText: { ...typography.headline, color: colors.text, }, dismissButton: { flex: 1, borderRadius: radius.lg, backgroundColor: colors.glass.background, borderWidth: 1, borderColor: colors.glass.border, paddingVertical: spacing.md, alignItems: 'center', justifyContent: 'center', }, dismissButtonText: { ...typography.headline, color: colors.textSecondary, }, border: { ...StyleSheet.absoluteFillObject, borderRadius: radius.xl, borderWidth: 1, borderColor: 'rgba(233, 69, 96, 0.2)', }, });