redesign checkpoint
This commit is contained in:
1 parent
a5f761062e
commit
981208ab7b
6 files changed
+583
-298
No files matched your search
@@ -1,5 +1,12 @@
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert } from "react-native";
|
||||
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";
|
||||
@@ -24,6 +31,49 @@ export function GoalProgressCard({
|
||||
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
|
||||
@@ -71,160 +121,169 @@ export function GoalProgressCard({
|
||||
};
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
|
||||
<MinimalCard
|
||||
variant="bordered"
|
||||
style={[
|
||||
styles.card,
|
||||
isCompleted && {
|
||||
backgroundColor: colors.overlayLight,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* 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",
|
||||
},
|
||||
]}
|
||||
<Animated.View style={{ transform: [{ scale: scaleAnim }] }}>
|
||||
<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
|
||||
<MinimalCard
|
||||
variant="bordered"
|
||||
style={[
|
||||
styles.card,
|
||||
isCompleted && {
|
||||
backgroundColor: colors.overlayLight,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleRow}>
|
||||
<IconContainer
|
||||
variant="colored"
|
||||
backgroundColor={
|
||||
isCompleted ? colors.success : getPriorityColor(goal.priority)
|
||||
}
|
||||
>
|
||||
{goal.title}
|
||||
</Text>
|
||||
{goal.description && (
|
||||
<Ionicons
|
||||
name={getGoalTypeIcon(goal.goalType) as any}
|
||||
size={20}
|
||||
color={colors.white}
|
||||
/>
|
||||
</IconContainer>
|
||||
|
||||
<View style={styles.titleContainer}>
|
||||
<Text
|
||||
style={[
|
||||
typography.caption,
|
||||
{ color: colors.textTertiary, marginTop: 2 },
|
||||
typography.h3,
|
||||
{ color: colors.textPrimary },
|
||||
isCompleted && {
|
||||
color: colors.textSecondary,
|
||||
textDecorationLine: "line-through",
|
||||
},
|
||||
]}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{goal.description}
|
||||
{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>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<View style={styles.actions}>
|
||||
{!isCompleted && onComplete && (
|
||||
<TouchableOpacity
|
||||
onPress={onComplete}
|
||||
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>
|
||||
|
||||
{/* 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>
|
||||
<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.bodyEmphasis,
|
||||
typography.caption,
|
||||
{
|
||||
color: isCompleted ? colors.success : colors.primary,
|
||||
color:
|
||||
daysRemaining < 0 ? colors.danger : colors.textTertiary,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{(progress * 100).toFixed(0)}%
|
||||
{daysRemaining < 0
|
||||
? `${Math.abs(daysRemaining)} days overdue`
|
||||
: `${daysRemaining} days remaining`}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
color={
|
||||
isCompleted ? colors.success : getPriorityColor(goal.priority)
|
||||
}
|
||||
/>
|
||||
{isCompleted && goal.completedDate && (
|
||||
<Text style={[typography.caption, { color: colors.success }]}>
|
||||
Completed {new Date(goal.completedDate).toLocaleDateString()}
|
||||
</Text>
|
||||
)}
|
||||
</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>
|
||||
</MinimalCard>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user