fitness goals ui fix
This commit is contained in:
parent
df08ff8950
commit
b1439f059a
@ -256,7 +256,12 @@ export default function GoalsScreen() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{showAnalytics && (
|
{showAnalytics && (
|
||||||
<View style={styles.analyticsContent}>
|
<View
|
||||||
|
style={[
|
||||||
|
styles.analyticsContent,
|
||||||
|
{ borderTopColor: colors.border },
|
||||||
|
]}
|
||||||
|
>
|
||||||
{statistics.weeklyTrend.length > 0 && (
|
{statistics.weeklyTrend.length > 0 && (
|
||||||
<WeeklyProgressChart
|
<WeeklyProgressChart
|
||||||
weeklyData={statistics.weeklyTrend}
|
weeklyData={statistics.weeklyTrend}
|
||||||
@ -414,7 +419,6 @@ const styles = StyleSheet.create({
|
|||||||
paddingTop: 16,
|
paddingTop: 16,
|
||||||
marginTop: 16,
|
marginTop: 16,
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
borderTopColor: "rgba(0, 0, 0, 0.05)",
|
|
||||||
},
|
},
|
||||||
goalsList: {
|
goalsList: {
|
||||||
gap: 12,
|
gap: 12,
|
||||||
|
|||||||
@ -1,293 +1,271 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
import { View, Text, StyleSheet, TouchableOpacity, Alert } from "react-native";
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from "@expo/vector-icons";
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import type { FitnessGoal } from "../services/fitnessGoals";
|
||||||
import type { FitnessGoal } from '../services/fitnessGoals';
|
import { useTheme } from "../contexts/ThemeContext";
|
||||||
import { theme } from '../styles/theme';
|
import { MinimalCard } from "./MinimalCard";
|
||||||
|
import { Badge } from "./Badge";
|
||||||
|
import { ProgressBar } from "./ProgressBar";
|
||||||
|
import { IconContainer } from "./IconContainer";
|
||||||
|
|
||||||
interface GoalProgressCardProps {
|
interface GoalProgressCardProps {
|
||||||
goal: FitnessGoal;
|
goal: FitnessGoal;
|
||||||
onPress?: () => void;
|
onPress?: () => void;
|
||||||
onComplete?: () => void;
|
onComplete?: () => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GoalProgressCard({ goal, onPress, onComplete, onDelete }: GoalProgressCardProps) {
|
export function GoalProgressCard({
|
||||||
const isCompleted = goal.status === 'completed';
|
goal,
|
||||||
const progress = goal.progress || 0;
|
onPress,
|
||||||
|
onComplete,
|
||||||
|
onDelete,
|
||||||
|
}: GoalProgressCardProps) {
|
||||||
|
const { colors, typography } = useTheme();
|
||||||
|
const isCompleted = goal.status === "completed";
|
||||||
|
const progress = (goal.progress || 0) / 100; // Convert to 0-1 scale
|
||||||
|
|
||||||
// Calculate days remaining
|
// Calculate days remaining
|
||||||
const daysRemaining = goal.targetDate
|
const daysRemaining = goal.targetDate
|
||||||
? Math.ceil((new Date(goal.targetDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
|
? Math.ceil(
|
||||||
: null;
|
(new Date(goal.targetDate).getTime() - Date.now()) /
|
||||||
|
(1000 * 60 * 60 * 24),
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
const getGoalTypeIcon = (type: string) => {
|
const getGoalTypeIcon = (type: string) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'weight_target': return 'scale-outline';
|
case "weight_target":
|
||||||
case 'strength_milestone': return 'barbell-outline';
|
return "scale-outline";
|
||||||
case 'endurance_target': return 'bicycle-outline';
|
case "strength_milestone":
|
||||||
case 'flexibility_goal': return 'body-outline';
|
return "barbell-outline";
|
||||||
case 'habit_building': return 'calendar-outline';
|
case "endurance_target":
|
||||||
default: return 'flag-outline';
|
return "bicycle-outline";
|
||||||
}
|
case "flexibility_goal":
|
||||||
};
|
return "body-outline";
|
||||||
|
case "habit_building":
|
||||||
|
return "calendar-outline";
|
||||||
|
default:
|
||||||
|
return "flag-outline";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getPriorityGradient = (priority: string): readonly [string, string] => {
|
const getPriorityColor = (priority: string) => {
|
||||||
switch (priority) {
|
switch (priority) {
|
||||||
case 'high': return theme.gradients.danger;
|
case "high":
|
||||||
case 'medium': return theme.gradients.warning;
|
return colors.danger;
|
||||||
case 'low': return theme.gradients.success;
|
case "medium":
|
||||||
default: return theme.gradients.primary;
|
return colors.warning;
|
||||||
}
|
case "low":
|
||||||
};
|
return colors.success;
|
||||||
|
default:
|
||||||
|
return colors.primary;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
Alert.alert(
|
Alert.alert("Delete Goal", "Are you sure you want to delete this goal?", [
|
||||||
'Delete Goal',
|
{ text: "Cancel", style: "cancel" },
|
||||||
'Are you sure you want to delete this goal?',
|
{ text: "Delete", style: "destructive", onPress: onDelete },
|
||||||
[
|
]);
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
};
|
||||||
{ text: 'Delete', style: 'destructive', onPress: onDelete },
|
|
||||||
]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity onPress={onPress} activeOpacity={0.7}>
|
||||||
onPress={onPress}
|
<MinimalCard
|
||||||
activeOpacity={0.7}
|
variant="bordered"
|
||||||
>
|
style={[
|
||||||
<LinearGradient
|
styles.card,
|
||||||
colors={isCompleted
|
isCompleted && {
|
||||||
? ['rgba(16, 185, 129, 0.05)', 'rgba(5, 150, 105, 0.02)'] as const
|
backgroundColor: colors.overlayLight,
|
||||||
: ['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const
|
},
|
||||||
}
|
]}
|
||||||
style={[
|
>
|
||||||
styles.card,
|
{/* Header */}
|
||||||
theme.shadows.medium,
|
<View style={styles.header}>
|
||||||
isCompleted && styles.cardCompleted
|
<View style={styles.titleRow}>
|
||||||
]}
|
<IconContainer
|
||||||
|
variant="colored"
|
||||||
|
backgroundColor={
|
||||||
|
isCompleted ? colors.success : getPriorityColor(goal.priority)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{/* Priority Accent Bar */}
|
<Ionicons
|
||||||
<LinearGradient
|
name={getGoalTypeIcon(goal.goalType) as any}
|
||||||
colors={getPriorityGradient(goal.priority)}
|
size={20}
|
||||||
start={{ x: 0, y: 0 }}
|
color={colors.white}
|
||||||
end={{ x: 0, y: 1 }}
|
/>
|
||||||
style={styles.priorityAccent}
|
</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={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>
|
||||||
|
|
||||||
<View style={styles.header}>
|
{/* Progress Section */}
|
||||||
<View style={styles.titleRow}>
|
{goal.targetValue && (
|
||||||
<LinearGradient
|
<View style={styles.progressSection}>
|
||||||
colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)}
|
<View style={styles.progressInfo}>
|
||||||
style={styles.iconContainer}
|
<Text
|
||||||
>
|
style={[typography.caption, { color: colors.textSecondary }]}
|
||||||
<Ionicons
|
>
|
||||||
name={getGoalTypeIcon(goal.goalType) as any}
|
{goal.currentValue || 0} / {goal.targetValue} {goal.unit || ""}
|
||||||
size={20}
|
</Text>
|
||||||
color="#fff"
|
<Text
|
||||||
/>
|
style={[
|
||||||
</LinearGradient>
|
typography.bodyEmphasis,
|
||||||
<View style={styles.titleContainer}>
|
{
|
||||||
<Text style={[styles.title, isCompleted && styles.titleCompleted]}>
|
color: isCompleted ? colors.success : colors.primary,
|
||||||
{goal.title}
|
},
|
||||||
</Text>
|
]}
|
||||||
{goal.description && (
|
>
|
||||||
<Text style={styles.description} numberOfLines={2}>
|
{(progress * 100).toFixed(0)}%
|
||||||
{goal.description}
|
</Text>
|
||||||
</Text>
|
</View>
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.actions}>
|
<ProgressBar
|
||||||
{!isCompleted && onComplete && (
|
progress={progress}
|
||||||
<TouchableOpacity onPress={onComplete} style={styles.actionButton}>
|
color={
|
||||||
<Ionicons name="checkmark-circle-outline" size={24} color={theme.colors.success} />
|
isCompleted ? colors.success : getPriorityColor(goal.priority)
|
||||||
</TouchableOpacity>
|
}
|
||||||
)}
|
/>
|
||||||
{onDelete && (
|
</View>
|
||||||
<TouchableOpacity onPress={handleDelete} style={styles.actionButton}>
|
)}
|
||||||
<Ionicons name="trash-outline" size={22} color={theme.colors.danger} />
|
|
||||||
</TouchableOpacity>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{goal.targetValue && (
|
{/* Footer */}
|
||||||
<View style={styles.progressSection}>
|
<View style={styles.footer}>
|
||||||
<View style={styles.progressInfo}>
|
{isCompleted ? (
|
||||||
<Text style={styles.progressText}>
|
<Badge variant="success" label="COMPLETED" />
|
||||||
{goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''}
|
) : (
|
||||||
</Text>
|
<View>
|
||||||
<Text style={[styles.progressPercentage, isCompleted && { color: theme.colors.success }]}>
|
{goal.priority === "high" && (
|
||||||
{progress.toFixed(0)}%
|
<Badge variant="danger" label={goal.priority.toUpperCase()} />
|
||||||
</Text>
|
)}
|
||||||
</View>
|
{goal.priority === "medium" && (
|
||||||
|
<Badge variant="warning" label={goal.priority.toUpperCase()} />
|
||||||
|
)}
|
||||||
|
{goal.priority === "low" && (
|
||||||
|
<Badge variant="success" label={goal.priority.toUpperCase()} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={styles.progressBarContainer}>
|
{daysRemaining !== null && !isCompleted && (
|
||||||
<LinearGradient
|
<Text
|
||||||
colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)}
|
style={[
|
||||||
start={{ x: 0, y: 0 }}
|
typography.caption,
|
||||||
end={{ x: 1, y: 0 }}
|
{
|
||||||
style={[
|
color:
|
||||||
styles.progressBar,
|
daysRemaining < 0 ? colors.danger : colors.textTertiary,
|
||||||
{ width: `${Math.min(progress, 100)}%` }
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
>
|
||||||
</View>
|
{daysRemaining < 0
|
||||||
</View>
|
? `${Math.abs(daysRemaining)} days overdue`
|
||||||
)}
|
: `${daysRemaining} days remaining`}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
<View style={styles.footer}>
|
{isCompleted && goal.completedDate && (
|
||||||
<LinearGradient
|
<Text style={[typography.caption, { color: colors.success }]}>
|
||||||
colors={getPriorityGradient(goal.priority)}
|
Completed {new Date(goal.completedDate).toLocaleDateString()}
|
||||||
style={styles.priorityBadge}
|
</Text>
|
||||||
>
|
)}
|
||||||
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
|
</View>
|
||||||
</LinearGradient>
|
</MinimalCard>
|
||||||
|
</TouchableOpacity>
|
||||||
{daysRemaining !== null && !isCompleted && (
|
);
|
||||||
<Text style={[styles.daysRemaining, daysRemaining < 0 && styles.overdue]}>
|
|
||||||
{daysRemaining < 0
|
|
||||||
? `${Math.abs(daysRemaining)} days overdue`
|
|
||||||
: `${daysRemaining} days remaining`
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isCompleted && goal.completedDate && (
|
|
||||||
<Text style={styles.completedDate}>
|
|
||||||
Completed {new Date(goal.completedDate).toLocaleDateString()}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</LinearGradient>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
card: {
|
card: {
|
||||||
borderRadius: theme.borderRadius.xl,
|
marginBottom: 12,
|
||||||
padding: 16,
|
},
|
||||||
marginBottom: 12,
|
header: {
|
||||||
borderWidth: 1,
|
flexDirection: "row",
|
||||||
borderColor: 'rgba(59, 130, 246, 0.1)',
|
justifyContent: "space-between",
|
||||||
overflow: 'hidden',
|
alignItems: "flex-start",
|
||||||
},
|
marginBottom: 12,
|
||||||
cardCompleted: {
|
},
|
||||||
borderColor: 'rgba(16, 185, 129, 0.2)',
|
titleRow: {
|
||||||
},
|
flexDirection: "row",
|
||||||
priorityAccent: {
|
alignItems: "flex-start",
|
||||||
position: 'absolute',
|
flex: 1,
|
||||||
left: 0,
|
},
|
||||||
top: 0,
|
titleContainer: {
|
||||||
bottom: 0,
|
flex: 1,
|
||||||
width: 4,
|
marginLeft: 12,
|
||||||
},
|
},
|
||||||
header: {
|
actions: {
|
||||||
flexDirection: 'row',
|
flexDirection: "row",
|
||||||
justifyContent: 'space-between',
|
gap: 8,
|
||||||
alignItems: 'flex-start',
|
},
|
||||||
marginBottom: 12,
|
actionButton: {
|
||||||
marginLeft: 8,
|
padding: 4,
|
||||||
},
|
},
|
||||||
titleRow: {
|
progressSection: {
|
||||||
flexDirection: 'row',
|
marginBottom: 12,
|
||||||
alignItems: 'flex-start',
|
},
|
||||||
flex: 1,
|
progressInfo: {
|
||||||
},
|
flexDirection: "row",
|
||||||
iconContainer: {
|
justifyContent: "space-between",
|
||||||
width: 40,
|
alignItems: "center",
|
||||||
height: 40,
|
marginBottom: 8,
|
||||||
borderRadius: 20,
|
},
|
||||||
justifyContent: 'center',
|
footer: {
|
||||||
alignItems: 'center',
|
flexDirection: "row",
|
||||||
marginRight: 12,
|
alignItems: "center",
|
||||||
},
|
justifyContent: "space-between",
|
||||||
titleContainer: {
|
},
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
fontSize: theme.typography.fontSize.lg,
|
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
|
||||||
color: theme.colors.gray900,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
titleCompleted: {
|
|
||||||
color: theme.colors.gray600,
|
|
||||||
textDecorationLine: 'line-through',
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
fontSize: theme.typography.fontSize.sm,
|
|
||||||
color: theme.colors.gray600,
|
|
||||||
lineHeight: 18,
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
actionButton: {
|
|
||||||
padding: 4,
|
|
||||||
},
|
|
||||||
progressSection: {
|
|
||||||
marginBottom: 12,
|
|
||||||
marginLeft: 8,
|
|
||||||
},
|
|
||||||
progressInfo: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
marginBottom: 8,
|
|
||||||
},
|
|
||||||
progressText: {
|
|
||||||
fontSize: theme.typography.fontSize.sm,
|
|
||||||
fontWeight: theme.typography.fontWeight.medium,
|
|
||||||
color: theme.colors.gray700,
|
|
||||||
},
|
|
||||||
progressPercentage: {
|
|
||||||
fontSize: theme.typography.fontSize.sm,
|
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
|
||||||
color: theme.colors.primary,
|
|
||||||
},
|
|
||||||
progressBarContainer: {
|
|
||||||
height: 8,
|
|
||||||
backgroundColor: theme.colors.gray200,
|
|
||||||
borderRadius: 4,
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
progressBar: {
|
|
||||||
height: '100%',
|
|
||||||
borderRadius: 4,
|
|
||||||
},
|
|
||||||
footer: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
marginLeft: 8,
|
|
||||||
},
|
|
||||||
priorityBadge: {
|
|
||||||
paddingHorizontal: 10,
|
|
||||||
paddingVertical: 5,
|
|
||||||
borderRadius: theme.borderRadius.md,
|
|
||||||
},
|
|
||||||
priorityText: {
|
|
||||||
fontSize: theme.typography.fontSize.xs,
|
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
|
||||||
color: theme.colors.white,
|
|
||||||
},
|
|
||||||
daysRemaining: {
|
|
||||||
fontSize: theme.typography.fontSize.xs,
|
|
||||||
color: theme.colors.gray600,
|
|
||||||
fontWeight: theme.typography.fontWeight.medium,
|
|
||||||
},
|
|
||||||
overdue: {
|
|
||||||
color: theme.colors.danger,
|
|
||||||
fontWeight: theme.typography.fontWeight.semibold,
|
|
||||||
},
|
|
||||||
completedDate: {
|
|
||||||
fontSize: theme.typography.fontSize.xs,
|
|
||||||
color: theme.colors.success,
|
|
||||||
fontWeight: theme.typography.fontWeight.medium,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user