fitaiProto/apps/mobile/src/components/GoalProgressCard.tsx
echo 5d6166df1b redesign take 2 complete
fix artefacts from previous dessign
2026-03-12 17:56:46 +01:00

334 lines
9.0 KiB
TypeScript

import React, { useEffect, useRef } from "react";
import {
View,
Text,
StyleSheet,
TouchableOpacity,
Alert,
Animated,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import type { FitnessGoal } from "../services/fitnessGoals";
import { useTheme } from "../contexts/ThemeContext";
import { MinimalCard } from "./MinimalCard";
import { Badge } from "./Badge";
import { ProgressBar } from "./ProgressBar";
import { IconContainer } from "./IconContainer";
interface GoalProgressCardProps {
goal: FitnessGoal;
onPress?: () => void;
onComplete?: () => void;
onDelete?: () => void;
}
export function GoalProgressCard({
goal,
onPress,
onComplete,
onDelete,
}: GoalProgressCardProps) {
const { colors, typography } = useTheme();
const isCompleted = goal.status === "completed";
const progress = (goal.progress || 0) / 100; // Convert to 0-1 scale
const scaleAnim = useRef(new Animated.Value(1)).current;
// Celebration animation when goal is completed
useEffect(() => {
if (isCompleted) {
Animated.sequence([
Animated.spring(scaleAnim, {
toValue: 1.05,
friction: 3,
tension: 40,
useNativeDriver: true,
}),
Animated.spring(scaleAnim, {
toValue: 1,
friction: 3,
tension: 40,
useNativeDriver: true,
}),
]).start();
}
}, [isCompleted]);
const handleComplete = () => {
if (onComplete) {
// Trigger celebration animation
Animated.sequence([
Animated.spring(scaleAnim, {
toValue: 1.1,
friction: 3,
tension: 40,
useNativeDriver: true,
}),
Animated.spring(scaleAnim, {
toValue: 1,
friction: 3,
tension: 40,
useNativeDriver: true,
}),
]).start(() => {
onComplete();
});
}
};
// Calculate days remaining
const daysRemaining = goal.targetDate
? Math.ceil(
(new Date(goal.targetDate).getTime() - Date.now()) /
(1000 * 60 * 60 * 24),
)
: null;
const getGoalTypeIcon = (type: string) => {
switch (type) {
case "weight_target":
return "scale-outline";
case "strength_milestone":
return "barbell-outline";
case "endurance_target":
return "bicycle-outline";
case "flexibility_goal":
return "body-outline";
case "habit_building":
return "calendar-outline";
default:
return "flag-outline";
}
};
const getPriorityColor = (priority: string) => {
switch (priority) {
case "high":
return colors.danger;
case "medium":
return colors.warning;
case "low":
return colors.success;
default:
return colors.primary;
}
};
const handleDelete = () => {
Alert.alert("Delete Goal", "Are you sure you want to delete this goal?", [
{ text: "Cancel", style: "cancel" },
{ text: "Delete", style: "destructive", onPress: onDelete },
]);
};
return (
<Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
<TouchableOpacity onPress={onPress} activeOpacity={0.85}>
<MinimalCard
variant="elevated"
style={[
styles.card,
isCompleted && {
backgroundColor: colors.surfaceElevated,
opacity: 0.8,
},
]}
padding={20}
>
{/* Header */}
<View style={styles.header}>
<View style={styles.titleRow}>
<IconContainer
variant="colored"
backgroundColor={
isCompleted ? colors.success : getPriorityColor(goal.priority)
}
>
<Ionicons
name={getGoalTypeIcon(goal.goalType) as any}
size={20}
color={colors.white}
/>
</IconContainer>
<View style={styles.titleContainer}>
<Text
style={[
typography.h3,
{ color: colors.textPrimary },
isCompleted && {
color: colors.textSecondary,
textDecorationLine: "line-through",
},
]}
>
{goal.title}
</Text>
{goal.description && (
<Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
numberOfLines={2}
>
{goal.description}
</Text>
)}
</View>
</View>
{/* Action Buttons */}
<View style={styles.actions}>
{!isCompleted && onComplete && (
<TouchableOpacity
onPress={handleComplete}
style={styles.actionButton}
>
<Ionicons
name="checkmark-circle-outline"
size={24}
color={colors.success}
/>
</TouchableOpacity>
)}
{onDelete && (
<TouchableOpacity
onPress={handleDelete}
style={styles.actionButton}
>
<Ionicons
name="trash-outline"
size={22}
color={colors.danger}
/>
</TouchableOpacity>
)}
</View>
</View>
{/* Progress Section */}
{goal.targetValue && (
<View style={styles.progressSection}>
<View style={styles.progressInfo}>
<Text
style={[typography.caption, { color: colors.textSecondary }]}
>
{goal.currentValue || 0} / {goal.targetValue}{" "}
{goal.unit || ""}
</Text>
<Text
style={[
typography.bodyEmphasis,
{
color: isCompleted ? colors.success : colors.primary,
},
]}
>
{(progress * 100).toFixed(0)}%
</Text>
</View>
<ProgressBar
progress={progress}
color={
isCompleted ? colors.success : getPriorityColor(goal.priority)
}
/>
</View>
)}
{/* Footer */}
<View style={styles.footer}>
{isCompleted ? (
<Badge variant="success" label="COMPLETED" />
) : (
<View>
{goal.priority === "high" && (
<Badge variant="danger" label={goal.priority.toUpperCase()} />
)}
{goal.priority === "medium" && (
<Badge
variant="warning"
label={goal.priority.toUpperCase()}
/>
)}
{goal.priority === "low" && (
<Badge
variant="success"
label={goal.priority.toUpperCase()}
/>
)}
</View>
)}
{daysRemaining !== null && !isCompleted && (
<Text
style={[
typography.caption,
{
color:
daysRemaining < 0 ? colors.danger : colors.textTertiary,
},
]}
>
{daysRemaining < 0
? `${Math.abs(daysRemaining)} days overdue`
: `${daysRemaining} days remaining`}
</Text>
)}
{isCompleted && goal.completedDate && (
<Text style={[typography.caption, { color: colors.success }]}>
Completed {new Date(goal.completedDate).toLocaleDateString()}
</Text>
)}
</View>
</MinimalCard>
</TouchableOpacity>
</Animated.View>
);
}
const styles = StyleSheet.create({
card: {
marginBottom: 16,
borderRadius: 20,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
marginBottom: 16,
},
titleRow: {
flexDirection: "row",
alignItems: "flex-start",
flex: 1,
},
titleContainer: {
flex: 1,
marginLeft: 14,
},
actions: {
flexDirection: "row",
gap: 12,
},
actionButton: {
padding: 6,
},
progressSection: {
marginBottom: 16,
},
progressInfo: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 10,
},
footer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
},
});