mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Send push notifications when partner disconnects, resets progress, or deletes relationship. Update Matches screen with new styling and remove legacy premium elements. Remove message content preview from admin User Activity page.
518 lines
18 KiB
TypeScript
Executable File
518 lines
18 KiB
TypeScript
Executable File
import { View, Text, StyleSheet, FlatList, RefreshControl, TouchableOpacity, ActivityIndicator, Platform, useWindowDimensions } from "react-native";
|
|
import { useMatchStore, useAuthStore } from "../../src/store";
|
|
import { useEffect, useCallback, useState } from "react";
|
|
import { useFocusEffect } from "expo-router";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { useRouter } from "expo-router";
|
|
import { LinearGradient } from "expo-linear-gradient";
|
|
import Animated, {
|
|
FadeIn,
|
|
FadeInDown,
|
|
FadeInRight,
|
|
FadeInUp,
|
|
} from "react-native-reanimated";
|
|
import { GradientBackground, GlassCard, GlassButton, DecorativeSeparator } from "../../src/components/ui";
|
|
import { useAmbientOrbAnimation } from "../../src/hooks";
|
|
import { colors, gradients, spacing, typography, radius, shadows } from "../../src/theme";
|
|
import { MatchesTutorial } from "../../src/components/tutorials";
|
|
import { hasSeenMatchesTutorial, markMatchesTutorialSeen } from "../../src/lib/matchesTutorialSeen";
|
|
|
|
// Premium color palette
|
|
const ACCENT = colors.premium.gold;
|
|
const ACCENT_RGBA = 'rgba(212, 175, 55, ';
|
|
const ROSE = colors.premium.rose;
|
|
const ROSE_RGBA = 'rgba(232, 164, 174, ';
|
|
|
|
const MAX_CONTENT_WIDTH = 500;
|
|
|
|
export default function MatchesScreen() {
|
|
const { matches, fetchMatches, markAllAsSeen } = useMatchStore();
|
|
const { user } = useAuthStore();
|
|
const router = useRouter();
|
|
const { width } = useWindowDimensions();
|
|
const isWideScreen = width > MAX_CONTENT_WIDTH;
|
|
const [showTutorial, setShowTutorial] = useState(false);
|
|
|
|
// Ambient orb breathing animations
|
|
const { orbStyle1, orbStyle2 } = useAmbientOrbAnimation();
|
|
|
|
// Check if tutorial should be shown when screen is focused or matches change
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
const checkTutorial = async () => {
|
|
if (matches.length > 0) {
|
|
const seen = await hasSeenMatchesTutorial();
|
|
if (!seen) {
|
|
setShowTutorial(true);
|
|
}
|
|
}
|
|
};
|
|
checkTutorial();
|
|
}, [matches.length])
|
|
);
|
|
|
|
const handleTutorialComplete = async () => {
|
|
await markMatchesTutorialSeen();
|
|
setShowTutorial(false);
|
|
};
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
fetchMatches();
|
|
}, [])
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (matches.length > 0) {
|
|
markAllAsSeen();
|
|
}
|
|
}, [matches.length]);
|
|
|
|
const renderItem = ({ item, index }: { item: any; index: number }) => {
|
|
const userResponse = item.responses?.find((r: any) => r.user_id === user?.id);
|
|
const partnerResponse = item.responses?.find((r: any) => r.user_id !== user?.id);
|
|
|
|
let userText = item.question.text;
|
|
let partnerText = item.question.partner_text;
|
|
|
|
if (item.question.partner_text && userResponse && partnerResponse) {
|
|
const userTime = new Date(userResponse.created_at).getTime();
|
|
const partnerTime = new Date(partnerResponse.created_at).getTime();
|
|
|
|
if (userTime > partnerTime) {
|
|
userText = item.question.partner_text;
|
|
partnerText = item.question.text;
|
|
}
|
|
}
|
|
|
|
const isYesYes = item.match_type === "yes_yes";
|
|
|
|
return (
|
|
<Animated.View entering={FadeInRight.delay(index * 50).duration(300)}>
|
|
<TouchableOpacity
|
|
onPress={() => router.push(`/chat/${item.id}`)}
|
|
activeOpacity={0.8}
|
|
>
|
|
<View style={styles.matchCardPremium}>
|
|
{/* Subtle gradient background */}
|
|
<LinearGradient
|
|
colors={['rgba(22, 33, 62, 0.6)', 'rgba(13, 13, 26, 0.8)']}
|
|
style={StyleSheet.absoluteFill}
|
|
start={{ x: 0, y: 0 }}
|
|
end={{ x: 1, y: 1 }}
|
|
/>
|
|
{/* Top silk highlight */}
|
|
<LinearGradient
|
|
colors={[`${ACCENT_RGBA}0.06)`, 'transparent']}
|
|
style={styles.cardSilkHighlight}
|
|
start={{ x: 0.5, y: 0 }}
|
|
end={{ x: 0.5, y: 1 }}
|
|
/>
|
|
|
|
<View style={styles.matchRow}>
|
|
{/* Premium icon container */}
|
|
<View style={[
|
|
styles.iconContainerPremium,
|
|
isYesYes && styles.iconContainerYesYes
|
|
]}>
|
|
<Ionicons
|
|
name={isYesYes ? "heart" : "heart-half"}
|
|
size={20}
|
|
color={isYesYes ? ACCENT : ROSE}
|
|
/>
|
|
</View>
|
|
|
|
<View style={styles.content}>
|
|
<Text style={styles.questionTextPremium} numberOfLines={2}>
|
|
{userText}
|
|
</Text>
|
|
{item.question.partner_text && (
|
|
<Text style={styles.partnerTextPremium} numberOfLines={1}>
|
|
Partner: {partnerText}
|
|
</Text>
|
|
)}
|
|
<View style={styles.metaRow}>
|
|
<View style={[
|
|
styles.tagPremium,
|
|
isYesYes && styles.tagPremiumHighlight
|
|
]}>
|
|
<Text style={[
|
|
styles.tagTextPremium,
|
|
isYesYes && styles.tagTextPremiumHighlight
|
|
]}>
|
|
{isYesYes ? "YES + YES" : "YES + MAYBE"}
|
|
</Text>
|
|
</View>
|
|
<Text style={styles.datePremium}>
|
|
{new Date(item.created_at).toLocaleDateString()}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.rightSection}>
|
|
{item.unreadCount > 0 && (
|
|
<View style={styles.unreadBadgePremium}>
|
|
<Ionicons name="chatbubble" size={10} color={colors.text} />
|
|
</View>
|
|
)}
|
|
<View style={styles.chevronContainerPremium}>
|
|
<Ionicons name="chevron-forward" size={16} color={`${ACCENT_RGBA}0.6)`} />
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Premium border */}
|
|
<View style={styles.cardPremiumBorder} pointerEvents="none" />
|
|
</View>
|
|
</TouchableOpacity>
|
|
</Animated.View>
|
|
);
|
|
};
|
|
|
|
if (!user) {
|
|
return (
|
|
<GradientBackground>
|
|
<View style={styles.loadingContainer}>
|
|
<ActivityIndicator color={colors.primary} size="large" />
|
|
</View>
|
|
</GradientBackground>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<GradientBackground>
|
|
{/* Ambient Orbs - Premium gold/rose */}
|
|
<Animated.View style={[styles.ambientOrb, styles.orbTopRight, orbStyle1]} pointerEvents="none">
|
|
<LinearGradient
|
|
colors={[colors.premium.goldGlow, 'transparent']}
|
|
style={styles.orbGradient}
|
|
start={{ x: 0.5, y: 0.5 }}
|
|
end={{ x: 1, y: 1 }}
|
|
/>
|
|
</Animated.View>
|
|
<Animated.View style={[styles.ambientOrb, styles.orbBottomLeft, orbStyle2]} pointerEvents="none">
|
|
<LinearGradient
|
|
colors={[`${ROSE_RGBA}0.2)`, 'transparent']}
|
|
style={styles.orbGradient}
|
|
start={{ x: 0.5, y: 0.5 }}
|
|
end={{ x: 0, y: 0 }}
|
|
/>
|
|
</Animated.View>
|
|
|
|
<View style={styles.container}>
|
|
{/* Premium Header */}
|
|
<Animated.View
|
|
entering={FadeIn.duration(400)}
|
|
style={[styles.header, isWideScreen && styles.headerWide]}
|
|
>
|
|
<View style={styles.headerContent}>
|
|
{/* Premium label */}
|
|
<Text style={styles.headerLabel}>DISCOVER</Text>
|
|
<Text style={styles.headerTitle}>Matches</Text>
|
|
|
|
{/* Decorative separator */}
|
|
<DecorativeSeparator variant="gold" />
|
|
|
|
{/* Count badge */}
|
|
<View style={styles.countBadgePremium}>
|
|
<Ionicons name="heart" size={12} color={ACCENT} />
|
|
<Text style={styles.countTextPremium}>{matches.length} {matches.length === 1 ? 'MATCH' : 'MATCHES'}</Text>
|
|
</View>
|
|
</View>
|
|
</Animated.View>
|
|
|
|
<FlatList
|
|
data={matches}
|
|
renderItem={renderItem}
|
|
keyExtractor={(item) => item.id}
|
|
contentContainerStyle={[styles.list, isWideScreen && styles.listWide]}
|
|
showsVerticalScrollIndicator={false}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={false}
|
|
onRefresh={fetchMatches}
|
|
tintColor={colors.primary}
|
|
colors={[colors.primary]}
|
|
/>
|
|
}
|
|
ListEmptyComponent={
|
|
<Animated.View
|
|
entering={FadeInUp.duration(600).springify()}
|
|
style={styles.emptyContent}
|
|
>
|
|
{/* Description */}
|
|
<Text style={styles.emptyDescription}>
|
|
Answer questions together to discover what you both enjoy. Matches appear when you agree!
|
|
</Text>
|
|
|
|
{/* Feature hints */}
|
|
<View style={styles.emptyFeatures}>
|
|
<View style={styles.emptyFeatureItem}>
|
|
<Ionicons name="heart" size={16} color={ACCENT} />
|
|
<Text style={styles.emptyFeatureText}>Swipe right for yes</Text>
|
|
</View>
|
|
<View style={styles.emptyFeatureItem}>
|
|
<Ionicons name="sparkles" size={16} color={ACCENT} />
|
|
<Text style={styles.emptyFeatureText}>Match when you both agree</Text>
|
|
</View>
|
|
<View style={styles.emptyFeatureItem}>
|
|
<Ionicons name="chatbubbles-outline" size={16} color={ACCENT} />
|
|
<Text style={styles.emptyFeatureText}>Chat about your matches</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Bottom teaser */}
|
|
<Text style={styles.emptyTeaser}>Your first match is just a swipe away</Text>
|
|
|
|
<GlassButton
|
|
onPress={() => router.push("/(app)/swipe")}
|
|
style={{ marginTop: spacing.lg }}
|
|
>
|
|
Start Swiping
|
|
</GlassButton>
|
|
</Animated.View>
|
|
}
|
|
/>
|
|
</View>
|
|
|
|
{/* Matches Tutorial Overlay */}
|
|
{showTutorial && (
|
|
<MatchesTutorial onComplete={handleTutorialComplete} />
|
|
)}
|
|
</GradientBackground>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
flex: 1,
|
|
},
|
|
loadingContainer: {
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
// Ambient orbs
|
|
ambientOrb: {
|
|
position: 'absolute',
|
|
width: 300,
|
|
height: 300,
|
|
borderRadius: 150,
|
|
},
|
|
orbTopRight: {
|
|
top: 60,
|
|
right: -40,
|
|
},
|
|
orbBottomLeft: {
|
|
bottom: 180,
|
|
left: -40,
|
|
},
|
|
orbGradient: {
|
|
width: '100%',
|
|
height: '100%',
|
|
borderRadius: 150,
|
|
},
|
|
// Premium Header
|
|
header: {
|
|
paddingTop: 60,
|
|
paddingHorizontal: spacing.lg,
|
|
paddingBottom: spacing.md,
|
|
},
|
|
headerWide: {
|
|
alignSelf: 'center',
|
|
width: '100%',
|
|
maxWidth: MAX_CONTENT_WIDTH,
|
|
},
|
|
headerContent: {
|
|
alignItems: 'center',
|
|
},
|
|
headerLabel: {
|
|
...typography.caption2,
|
|
fontWeight: '600',
|
|
letterSpacing: 3,
|
|
color: ACCENT,
|
|
marginBottom: spacing.xs,
|
|
},
|
|
headerTitle: {
|
|
...typography.largeTitle,
|
|
color: colors.text,
|
|
textAlign: 'center',
|
|
},
|
|
countBadgePremium: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
backgroundColor: `${ACCENT_RGBA}0.1)`,
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.sm,
|
|
borderRadius: radius.full,
|
|
borderWidth: 1,
|
|
borderColor: `${ACCENT_RGBA}0.2)`,
|
|
gap: spacing.xs,
|
|
},
|
|
countTextPremium: {
|
|
...typography.caption2,
|
|
fontWeight: '600',
|
|
letterSpacing: 2,
|
|
color: ACCENT,
|
|
},
|
|
// List
|
|
list: {
|
|
padding: spacing.lg,
|
|
paddingTop: spacing.sm,
|
|
paddingBottom: Platform.OS === 'ios' ? 120 : 100,
|
|
},
|
|
listWide: {
|
|
alignSelf: 'center',
|
|
width: '100%',
|
|
maxWidth: MAX_CONTENT_WIDTH,
|
|
},
|
|
// Premium Match Card
|
|
matchCardPremium: {
|
|
marginBottom: spacing.sm,
|
|
borderRadius: radius.lg,
|
|
overflow: 'hidden',
|
|
padding: spacing.md,
|
|
},
|
|
cardSilkHighlight: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
height: 60,
|
|
},
|
|
cardPremiumBorder: {
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
borderRadius: radius.lg,
|
|
borderWidth: 1,
|
|
borderColor: `${ACCENT_RGBA}0.15)`,
|
|
},
|
|
matchRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
},
|
|
iconContainerPremium: {
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 22,
|
|
backgroundColor: `${ROSE_RGBA}0.1)`,
|
|
borderWidth: 1,
|
|
borderColor: `${ROSE_RGBA}0.2)`,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
marginRight: spacing.md,
|
|
},
|
|
iconContainerYesYes: {
|
|
backgroundColor: `${ACCENT_RGBA}0.1)`,
|
|
borderColor: `${ACCENT_RGBA}0.2)`,
|
|
},
|
|
content: {
|
|
flex: 1,
|
|
},
|
|
questionTextPremium: {
|
|
...typography.subhead,
|
|
color: colors.text,
|
|
fontWeight: "600",
|
|
marginBottom: spacing.xs,
|
|
lineHeight: 20,
|
|
},
|
|
partnerTextPremium: {
|
|
...typography.caption1,
|
|
color: colors.textSecondary,
|
|
fontStyle: "italic",
|
|
marginBottom: spacing.sm,
|
|
},
|
|
metaRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
},
|
|
tagPremium: {
|
|
backgroundColor: `${ROSE_RGBA}0.1)`,
|
|
paddingHorizontal: spacing.sm,
|
|
paddingVertical: 3,
|
|
borderRadius: radius.sm,
|
|
borderWidth: 1,
|
|
borderColor: `${ROSE_RGBA}0.15)`,
|
|
},
|
|
tagPremiumHighlight: {
|
|
backgroundColor: `${ACCENT_RGBA}0.1)`,
|
|
borderColor: `${ACCENT_RGBA}0.2)`,
|
|
},
|
|
tagTextPremium: {
|
|
...typography.caption2,
|
|
color: ROSE,
|
|
fontWeight: "600",
|
|
letterSpacing: 1,
|
|
},
|
|
tagTextPremiumHighlight: {
|
|
color: ACCENT,
|
|
},
|
|
datePremium: {
|
|
...typography.caption2,
|
|
color: colors.textTertiary,
|
|
fontStyle: 'italic',
|
|
},
|
|
rightSection: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: spacing.sm,
|
|
marginLeft: spacing.sm,
|
|
},
|
|
unreadBadgePremium: {
|
|
width: 22,
|
|
height: 22,
|
|
borderRadius: 11,
|
|
backgroundColor: ACCENT,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
chevronContainerPremium: {
|
|
width: 28,
|
|
height: 28,
|
|
borderRadius: 14,
|
|
backgroundColor: `${ACCENT_RGBA}0.08)`,
|
|
borderWidth: 1,
|
|
borderColor: `${ACCENT_RGBA}0.15)`,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
// Premium Empty State
|
|
emptyContent: {
|
|
width: '100%',
|
|
alignItems: 'center',
|
|
paddingHorizontal: spacing.md,
|
|
paddingVertical: spacing.xl,
|
|
},
|
|
emptyDescription: {
|
|
...typography.body,
|
|
color: colors.textSecondary,
|
|
textAlign: 'center',
|
|
marginBottom: spacing.xl,
|
|
paddingHorizontal: spacing.md,
|
|
},
|
|
emptyFeatures: {
|
|
marginBottom: spacing.xl,
|
|
},
|
|
emptyFeatureItem: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginBottom: spacing.sm,
|
|
},
|
|
emptyFeatureText: {
|
|
...typography.subhead,
|
|
color: colors.text,
|
|
marginLeft: spacing.sm,
|
|
},
|
|
emptyTeaser: {
|
|
...typography.footnote,
|
|
fontStyle: 'italic',
|
|
color: colors.textTertiary,
|
|
textAlign: 'center',
|
|
},
|
|
});
|