224 lines
7.8 KiB
TypeScript
224 lines
7.8 KiB
TypeScript
import React, { useRef } from 'react';
|
|
import { View, Text, StyleSheet, Dimensions, ScrollView, TouchableOpacity, Animated } from 'react-native';
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { theme } from '../styles/theme';
|
|
import { CircularProgress } from './CircularProgress';
|
|
import type { FitnessGoal } from '../services/fitnessGoals';
|
|
|
|
const { width } = Dimensions.get('window');
|
|
const CARD_WIDTH = width * 0.8;
|
|
const SPACING = 20;
|
|
|
|
interface GoalCarouselProps {
|
|
goals: FitnessGoal[];
|
|
onGoalPress: (goal: FitnessGoal) => void;
|
|
}
|
|
|
|
export function GoalCarousel({ goals, onGoalPress }: GoalCarouselProps) {
|
|
const scrollX = useRef(new Animated.Value(0)).current;
|
|
|
|
if (goals.length === 0) {
|
|
return (
|
|
<View style={styles.emptyContainer}>
|
|
<Text style={styles.emptyText}>No active goals</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<View>
|
|
<Animated.ScrollView
|
|
horizontal
|
|
showsHorizontalScrollIndicator={false}
|
|
snapToInterval={CARD_WIDTH + SPACING}
|
|
decelerationRate="fast"
|
|
contentContainerStyle={styles.scrollContent}
|
|
onScroll={Animated.event(
|
|
[{ nativeEvent: { contentOffset: { x: scrollX } } }],
|
|
{ useNativeDriver: true }
|
|
)}
|
|
scrollEventThrottle={16}
|
|
>
|
|
{goals.map((goal, index) => {
|
|
const inputRange = [
|
|
(index - 1) * (CARD_WIDTH + SPACING),
|
|
index * (CARD_WIDTH + SPACING),
|
|
(index + 1) * (CARD_WIDTH + SPACING),
|
|
];
|
|
|
|
const scale = scrollX.interpolate({
|
|
inputRange,
|
|
outputRange: [0.9, 1, 0.9],
|
|
extrapolate: 'clamp',
|
|
});
|
|
|
|
const opacity = scrollX.interpolate({
|
|
inputRange,
|
|
outputRange: [0.7, 1, 0.7],
|
|
extrapolate: 'clamp',
|
|
});
|
|
|
|
return (
|
|
<TouchableOpacity
|
|
key={goal.id}
|
|
activeOpacity={0.9}
|
|
onPress={() => onGoalPress(goal)}
|
|
>
|
|
<Animated.View
|
|
style={[
|
|
styles.cardContainer,
|
|
{ transform: [{ scale }], opacity },
|
|
]}
|
|
>
|
|
<LinearGradient
|
|
colors={['#ffffff', '#f9fafb']}
|
|
style={[styles.card, theme.shadows.medium]}
|
|
>
|
|
<View style={styles.cardHeader}>
|
|
<View style={styles.iconContainer}>
|
|
<LinearGradient
|
|
colors={theme.gradients.primary}
|
|
style={styles.iconGradient}
|
|
>
|
|
<Ionicons name="trophy" size={24} color="#fff" />
|
|
</LinearGradient>
|
|
</View>
|
|
<View style={styles.priorityBadge}>
|
|
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<Text style={styles.title} numberOfLines={1}>{goal.title}</Text>
|
|
<Text style={styles.description} numberOfLines={2}>{goal.description}</Text>
|
|
|
|
<View style={styles.progressContainer}>
|
|
<CircularProgress
|
|
progress={goal.progress || 0}
|
|
size={80}
|
|
strokeWidth={8}
|
|
color={theme.colors.primary}
|
|
/>
|
|
<View style={styles.progressDetails}>
|
|
<Text style={styles.progressValue}>
|
|
{goal.currentValue} / {goal.targetValue}
|
|
</Text>
|
|
<Text style={styles.progressUnit}>{goal.unit}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
<View style={styles.footer}>
|
|
<Text style={styles.date}>
|
|
Target: {goal.targetDate ? new Date(goal.targetDate).toLocaleDateString() : 'No date'}
|
|
</Text>
|
|
</View>
|
|
</LinearGradient>
|
|
</Animated.View>
|
|
</TouchableOpacity>
|
|
);
|
|
})}
|
|
</Animated.ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
scrollContent: {
|
|
paddingHorizontal: (width - CARD_WIDTH) / 2,
|
|
paddingVertical: 20,
|
|
},
|
|
emptyContainer: {
|
|
height: 200,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
emptyText: {
|
|
color: theme.colors.gray500,
|
|
fontSize: 16,
|
|
},
|
|
cardContainer: {
|
|
width: CARD_WIDTH,
|
|
marginRight: SPACING,
|
|
},
|
|
card: {
|
|
borderRadius: 24,
|
|
padding: 24,
|
|
height: 320,
|
|
justifyContent: 'space-between',
|
|
borderWidth: 1,
|
|
borderColor: 'rgba(0,0,0,0.05)',
|
|
},
|
|
cardHeader: {
|
|
flexDirection: 'row',
|
|
justifyContent: 'space-between',
|
|
alignItems: 'flex-start',
|
|
marginBottom: 16,
|
|
},
|
|
iconContainer: {
|
|
shadowColor: theme.colors.primary,
|
|
shadowOffset: { width: 0, height: 4 },
|
|
shadowOpacity: 0.3,
|
|
shadowRadius: 8,
|
|
elevation: 4,
|
|
},
|
|
iconGradient: {
|
|
width: 48,
|
|
height: 48,
|
|
borderRadius: 16,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
},
|
|
priorityBadge: {
|
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
paddingHorizontal: 12,
|
|
paddingVertical: 6,
|
|
borderRadius: 12,
|
|
},
|
|
priorityText: {
|
|
color: theme.colors.primary,
|
|
fontSize: 12,
|
|
fontWeight: '700',
|
|
},
|
|
title: {
|
|
fontSize: 24,
|
|
fontWeight: '800',
|
|
color: theme.colors.gray900,
|
|
marginBottom: 8,
|
|
},
|
|
description: {
|
|
fontSize: 14,
|
|
color: theme.colors.gray500,
|
|
marginBottom: 24,
|
|
lineHeight: 20,
|
|
},
|
|
progressContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: 24,
|
|
},
|
|
progressDetails: {
|
|
alignItems: 'flex-end',
|
|
},
|
|
progressValue: {
|
|
fontSize: 24,
|
|
fontWeight: '700',
|
|
color: theme.colors.gray900,
|
|
},
|
|
progressUnit: {
|
|
fontSize: 14,
|
|
color: theme.colors.gray500,
|
|
fontWeight: '500',
|
|
},
|
|
footer: {
|
|
borderTopWidth: 1,
|
|
borderTopColor: theme.colors.gray100,
|
|
paddingTop: 16,
|
|
},
|
|
date: {
|
|
fontSize: 12,
|
|
color: theme.colors.gray400,
|
|
fontWeight: '500',
|
|
},
|
|
});
|