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,9 +1,12 @@
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;
@ -12,220 +15,240 @@ interface GoalProgressCardProps {
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(
(new Date(goal.targetDate).getTime() - Date.now()) /
(1000 * 60 * 60 * 24),
)
: null; : 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"
>
<LinearGradient
colors={isCompleted
? ['rgba(16, 185, 129, 0.05)', 'rgba(5, 150, 105, 0.02)'] as const
: ['rgba(255, 255, 255, 1)', 'rgba(249, 250, 251, 1)'] as const
}
style={[ style={[
styles.card, styles.card,
theme.shadows.medium, isCompleted && {
isCompleted && styles.cardCompleted backgroundColor: colors.overlayLight,
},
]} ]}
> >
{/* Priority Accent Bar */} {/* Header */}
<LinearGradient
colors={getPriorityGradient(goal.priority)}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
style={styles.priorityAccent}
/>
<View style={styles.header}> <View style={styles.header}>
<View style={styles.titleRow}> <View style={styles.titleRow}>
<LinearGradient <IconContainer
colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)} variant="colored"
style={styles.iconContainer} backgroundColor={
isCompleted ? colors.success : getPriorityColor(goal.priority)
}
> >
<Ionicons <Ionicons
name={getGoalTypeIcon(goal.goalType) as any} name={getGoalTypeIcon(goal.goalType) as any}
size={20} size={20}
color="#fff" color={colors.white}
/> />
</LinearGradient> </IconContainer>
<View style={styles.titleContainer}> <View style={styles.titleContainer}>
<Text style={[styles.title, isCompleted && styles.titleCompleted]}> <Text
style={[
typography.h3,
{ color: colors.textPrimary },
isCompleted && {
color: colors.textSecondary,
textDecorationLine: "line-through",
},
]}
>
{goal.title} {goal.title}
</Text> </Text>
{goal.description && ( {goal.description && (
<Text style={styles.description} numberOfLines={2}> <Text
style={[
typography.caption,
{ color: colors.textTertiary, marginTop: 2 },
]}
numberOfLines={2}
>
{goal.description} {goal.description}
</Text> </Text>
)} )}
</View> </View>
</View> </View>
{/* Action Buttons */}
<View style={styles.actions}> <View style={styles.actions}>
{!isCompleted && onComplete && ( {!isCompleted && onComplete && (
<TouchableOpacity onPress={onComplete} style={styles.actionButton}> <TouchableOpacity
<Ionicons name="checkmark-circle-outline" size={24} color={theme.colors.success} /> onPress={onComplete}
style={styles.actionButton}
>
<Ionicons
name="checkmark-circle-outline"
size={24}
color={colors.success}
/>
</TouchableOpacity> </TouchableOpacity>
)} )}
{onDelete && ( {onDelete && (
<TouchableOpacity onPress={handleDelete} style={styles.actionButton}> <TouchableOpacity
<Ionicons name="trash-outline" size={22} color={theme.colors.danger} /> onPress={handleDelete}
style={styles.actionButton}
>
<Ionicons
name="trash-outline"
size={22}
color={colors.danger}
/>
</TouchableOpacity> </TouchableOpacity>
)} )}
</View> </View>
</View> </View>
{/* Progress Section */}
{goal.targetValue && ( {goal.targetValue && (
<View style={styles.progressSection}> <View style={styles.progressSection}>
<View style={styles.progressInfo}> <View style={styles.progressInfo}>
<Text style={styles.progressText}> <Text
{goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''} style={[typography.caption, { color: colors.textSecondary }]}
>
{goal.currentValue || 0} / {goal.targetValue} {goal.unit || ""}
</Text> </Text>
<Text style={[styles.progressPercentage, isCompleted && { color: theme.colors.success }]}> <Text
{progress.toFixed(0)}% style={[
typography.bodyEmphasis,
{
color: isCompleted ? colors.success : colors.primary,
},
]}
>
{(progress * 100).toFixed(0)}%
</Text> </Text>
</View> </View>
<View style={styles.progressBarContainer}> <ProgressBar
<LinearGradient progress={progress}
colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)} color={
start={{ x: 0, y: 0 }} isCompleted ? colors.success : getPriorityColor(goal.priority)
end={{ x: 1, y: 0 }} }
style={[
styles.progressBar,
{ width: `${Math.min(progress, 100)}%` }
]}
/> />
</View> </View>
</View>
)} )}
{/* Footer */}
<View style={styles.footer}> <View style={styles.footer}>
<LinearGradient {isCompleted ? (
colors={getPriorityGradient(goal.priority)} <Badge variant="success" label="COMPLETED" />
style={styles.priorityBadge} ) : (
> <View>
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text> {goal.priority === "high" && (
</LinearGradient> <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 && ( {daysRemaining !== null && !isCompleted && (
<Text style={[styles.daysRemaining, daysRemaining < 0 && styles.overdue]}> <Text
style={[
typography.caption,
{
color:
daysRemaining < 0 ? colors.danger : colors.textTertiary,
},
]}
>
{daysRemaining < 0 {daysRemaining < 0
? `${Math.abs(daysRemaining)} days overdue` ? `${Math.abs(daysRemaining)} days overdue`
: `${daysRemaining} days remaining` : `${daysRemaining} days remaining`}
}
</Text> </Text>
)} )}
{isCompleted && goal.completedDate && ( {isCompleted && goal.completedDate && (
<Text style={styles.completedDate}> <Text style={[typography.caption, { color: colors.success }]}>
Completed {new Date(goal.completedDate).toLocaleDateString()} Completed {new Date(goal.completedDate).toLocaleDateString()}
</Text> </Text>
)} )}
</View> </View>
</LinearGradient> </MinimalCard>
</TouchableOpacity> </TouchableOpacity>
); );
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
card: { card: {
borderRadius: theme.borderRadius.xl,
padding: 16,
marginBottom: 12, marginBottom: 12,
borderWidth: 1,
borderColor: 'rgba(59, 130, 246, 0.1)',
overflow: 'hidden',
},
cardCompleted: {
borderColor: 'rgba(16, 185, 129, 0.2)',
},
priorityAccent: {
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: 4,
}, },
header: { header: {
flexDirection: 'row', flexDirection: "row",
justifyContent: 'space-between', justifyContent: "space-between",
alignItems: 'flex-start', alignItems: "flex-start",
marginBottom: 12, marginBottom: 12,
marginLeft: 8,
}, },
titleRow: { titleRow: {
flexDirection: 'row', flexDirection: "row",
alignItems: 'flex-start', alignItems: "flex-start",
flex: 1, flex: 1,
}, },
iconContainer: {
width: 40,
height: 40,
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
marginRight: 12,
},
titleContainer: { titleContainer: {
flex: 1, flex: 1,
}, marginLeft: 12,
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: { actions: {
flexDirection: 'row', flexDirection: "row",
gap: 8, gap: 8,
}, },
actionButton: { actionButton: {
@ -233,61 +256,16 @@ const styles = StyleSheet.create({
}, },
progressSection: { progressSection: {
marginBottom: 12, marginBottom: 12,
marginLeft: 8,
}, },
progressInfo: { progressInfo: {
flexDirection: 'row', flexDirection: "row",
justifyContent: 'space-between', justifyContent: "space-between",
alignItems: "center",
marginBottom: 8, 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: { footer: {
flexDirection: 'row', flexDirection: "row",
alignItems: 'center', alignItems: "center",
justifyContent: 'space-between', 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,
}, },
}); });