fitness goals ui fix

This commit is contained in:
echo 2026-03-11 08:47:14 +01:00
parent df08ff8950
commit b1439f059a
2 changed files with 257 additions and 275 deletions

View File

@ -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,

View File

@ -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,
},
}); });