Files
digital-pilates/app/(tabs)/challenges.tsx
2025-09-28 08:29:10 +08:00

231 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { IconSymbol } from '@/components/ui/IconSymbol';
import { Colors } from '@/constants/Colors';
import { useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { selectChallengeCards, type ChallengeViewModel } from '@/store/challengesSlice';
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from 'expo-router';
import React from 'react';
import { Image, ScrollView, StatusBar, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
const AVATAR_SIZE = 36;
const CARD_IMAGE_WIDTH = 132;
const CARD_IMAGE_HEIGHT = 96;
export default function ChallengesScreen() {
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const router = useRouter();
const challenges = useAppSelector(selectChallengeCards);
const gradientColors: [string, string] =
theme === 'dark'
? ['#1f2230', '#10131e']
: [colorTokens.backgroundGradientStart, colorTokens.backgroundGradientEnd];
return (
<View style={[styles.screen, { backgroundColor: colorTokens.pageBackgroundEmphasis }]}>
<StatusBar barStyle={theme === 'dark' ? 'light-content' : 'dark-content'} />
<LinearGradient colors={gradientColors} style={StyleSheet.absoluteFillObject} />
<SafeAreaView style={styles.safeArea} edges={['top']}>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
bounces
>
<View style={styles.headerRow}>
<View>
<Text style={[styles.title, { color: colorTokens.text }]}></Text>
<Text style={[styles.subtitle, { color: colorTokens.textSecondary }]}></Text>
</View>
<TouchableOpacity activeOpacity={0.9} style={styles.giftShadow}>
<LinearGradient
colors={[colorTokens.primary, colorTokens.accentPurple]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.giftButton}
>
<IconSymbol name="gift.fill" size={22} color={colorTokens.onPrimary} />
</LinearGradient>
</TouchableOpacity>
</View>
<View style={styles.cardsContainer}>
{challenges.map((challenge) => (
<ChallengeCard
key={challenge.id}
challenge={challenge}
surfaceColor={colorTokens.surface}
textColor={colorTokens.text}
mutedColor={colorTokens.textSecondary}
onPress={() =>
router.push({ pathname: '/challenges/[id]', params: { id: challenge.id } })
}
/>
))}
</View>
</ScrollView>
</SafeAreaView>
</View>
);
}
type ChallengeCardProps = {
challenge: ChallengeViewModel;
surfaceColor: string;
textColor: string;
mutedColor: string;
onPress: () => void;
};
function ChallengeCard({ challenge, surfaceColor, textColor, mutedColor, onPress }: ChallengeCardProps) {
return (
<TouchableOpacity
activeOpacity={0.92}
onPress={onPress}
style={[
styles.card,
{
backgroundColor: surfaceColor,
shadowColor: 'rgba(15, 23, 42, 0.18)',
},
]}
>
<Image
source={{ uri: challenge.image }}
style={styles.cardImage}
resizeMode="cover"
/>
<View style={styles.cardContent}>
<Text style={[styles.cardTitle, { color: textColor }]} numberOfLines={1}>
{challenge.title}
</Text>
<Text style={[styles.cardDate, { color: mutedColor }]}>{challenge.dateRange}</Text>
<Text style={[styles.cardParticipants, { color: mutedColor }]}>{challenge.participantsLabel}</Text>
<AvatarStack avatars={challenge.avatars} borderColor={surfaceColor} />
</View>
</TouchableOpacity>
);
}
type AvatarStackProps = {
avatars: string[];
borderColor: string;
};
function AvatarStack({ avatars, borderColor }: AvatarStackProps) {
return (
<View style={styles.avatarRow}>
{avatars.map((avatar, index) => (
<Image
key={`${avatar}-${index}`}
source={{ uri: avatar }}
style={[
styles.avatar,
{ borderColor },
index === 0 ? null : styles.avatarOffset,
]}
/>
))}
</View>
);
}
const styles = StyleSheet.create({
screen: {
flex: 1,
},
safeArea: {
flex: 1,
},
scrollContent: {
paddingHorizontal: 20,
paddingBottom: 32,
},
headerRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginTop: 8,
marginBottom: 26,
},
title: {
fontSize: 32,
fontWeight: '700',
letterSpacing: 1,
},
subtitle: {
marginTop: 6,
fontSize: 14,
fontWeight: '500',
opacity: 0.8,
},
giftShadow: {
shadowColor: 'rgba(94, 62, 199, 0.45)',
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.35,
shadowRadius: 12,
elevation: 8,
borderRadius: 26,
},
giftButton: {
width: 52,
height: 52,
borderRadius: 26,
alignItems: 'center',
justifyContent: 'center',
},
cardsContainer: {
gap: 18,
},
card: {
flexDirection: 'row',
borderRadius: 28,
padding: 18,
alignItems: 'center',
shadowOffset: { width: 0, height: 16 },
shadowOpacity: 0.18,
shadowRadius: 24,
elevation: 6,
},
cardImage: {
width: CARD_IMAGE_WIDTH,
height: CARD_IMAGE_HEIGHT,
borderRadius: 22,
},
cardContent: {
flex: 1,
marginLeft: 16,
},
cardTitle: {
fontSize: 18,
fontWeight: '700',
marginBottom: 4,
},
cardDate: {
fontSize: 13,
fontWeight: '500',
marginBottom: 4,
},
cardParticipants: {
fontSize: 13,
fontWeight: '500',
},
avatarRow: {
flexDirection: 'row',
marginTop: 16,
alignItems: 'center',
},
avatar: {
width: AVATAR_SIZE,
height: AVATAR_SIZE,
borderRadius: AVATAR_SIZE / 2,
borderWidth: 2,
},
avatarOffset: {
marginLeft: -12,
},
});