fitness goals ui fix
This commit is contained in:
parent
df08ff8950
commit
b1439f059a
@ -256,7 +256,12 @@ export default function GoalsScreen() {
|
||||
</TouchableOpacity>
|
||||
|
||||
{showAnalytics && (
|
||||
<View style={styles.analyticsContent}>
|
||||
<View
|
||||
style={[
|
||||
styles.analyticsContent,
|
||||
{ borderTopColor: colors.border },
|
||||
]}
|
||||
>
|
||||
{statistics.weeklyTrend.length > 0 && (
|
||||
<WeeklyProgressChart
|
||||
weeklyData={statistics.weeklyTrend}
|
||||
@ -414,7 +419,6 @@ const styles = StyleSheet.create({
|
||||
paddingTop: 16,
|
||||
marginTop: 16,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: "rgba(0, 0, 0, 0.05)",
|
||||
},
|
||||
goalsList: {
|
||||
gap: 12,
|
||||
|
||||
@ -1,293 +1,271 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import type { FitnessGoal } from '../services/fitnessGoals';
|
||||
import { theme } from '../styles/theme';
|
||||
import React from "react";
|
||||
import { View, Text, StyleSheet, TouchableOpacity, Alert } 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;
|
||||
goal: FitnessGoal;
|
||||
onPress?: () => void;
|
||||
onComplete?: () => void;
|
||||
onDelete?: () => void;
|
||||
}
|
||||
|
||||
export function GoalProgressCard({ goal, onPress, onComplete, onDelete }: GoalProgressCardProps) {
|
||||
const isCompleted = goal.status === 'completed';
|
||||
const progress = goal.progress || 0;
|
||||
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
|
||||
|
||||
// Calculate days remaining
|
||||
const daysRemaining = goal.targetDate
|
||||
? Math.ceil((new Date(goal.targetDate).getTime() - Date.now()) / (1000 * 60 * 60 * 24))
|
||||
: null;
|
||||
// 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 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 getPriorityGradient = (priority: string): readonly [string, string] => {
|
||||
switch (priority) {
|
||||
case 'high': return theme.gradients.danger;
|
||||
case 'medium': return theme.gradients.warning;
|
||||
case 'low': return theme.gradients.success;
|
||||
default: return theme.gradients.primary;
|
||||
}
|
||||
};
|
||||
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 },
|
||||
]
|
||||
);
|
||||
};
|
||||
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 (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<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={[
|
||||
styles.card,
|
||||
theme.shadows.medium,
|
||||
isCompleted && styles.cardCompleted
|
||||
]}
|
||||
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)
|
||||
}
|
||||
>
|
||||
{/* Priority Accent Bar */}
|
||||
<LinearGradient
|
||||
colors={getPriorityGradient(goal.priority)}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
style={styles.priorityAccent}
|
||||
<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={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}>
|
||||
<View style={styles.titleRow}>
|
||||
<LinearGradient
|
||||
colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)}
|
||||
style={styles.iconContainer}
|
||||
>
|
||||
<Ionicons
|
||||
name={getGoalTypeIcon(goal.goalType) as any}
|
||||
size={20}
|
||||
color="#fff"
|
||||
/>
|
||||
</LinearGradient>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={[styles.title, isCompleted && styles.titleCompleted]}>
|
||||
{goal.title}
|
||||
</Text>
|
||||
{goal.description && (
|
||||
<Text style={styles.description} numberOfLines={2}>
|
||||
{goal.description}
|
||||
</Text>
|
||||
)}
|
||||
</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>
|
||||
|
||||
<View style={styles.actions}>
|
||||
{!isCompleted && onComplete && (
|
||||
<TouchableOpacity onPress={onComplete} style={styles.actionButton}>
|
||||
<Ionicons name="checkmark-circle-outline" size={24} color={theme.colors.success} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{onDelete && (
|
||||
<TouchableOpacity onPress={handleDelete} style={styles.actionButton}>
|
||||
<Ionicons name="trash-outline" size={22} color={theme.colors.danger} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
<ProgressBar
|
||||
progress={progress}
|
||||
color={
|
||||
isCompleted ? colors.success : getPriorityColor(goal.priority)
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{goal.targetValue && (
|
||||
<View style={styles.progressSection}>
|
||||
<View style={styles.progressInfo}>
|
||||
<Text style={styles.progressText}>
|
||||
{goal.currentValue || 0} / {goal.targetValue} {goal.unit || ''}
|
||||
</Text>
|
||||
<Text style={[styles.progressPercentage, isCompleted && { color: theme.colors.success }]}>
|
||||
{progress.toFixed(0)}%
|
||||
</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>
|
||||
)}
|
||||
|
||||
<View style={styles.progressBarContainer}>
|
||||
<LinearGradient
|
||||
colors={isCompleted ? theme.gradients.success : getPriorityGradient(goal.priority)}
|
||||
start={{ x: 0, y: 0 }}
|
||||
end={{ x: 1, y: 0 }}
|
||||
style={[
|
||||
styles.progressBar,
|
||||
{ width: `${Math.min(progress, 100)}%` }
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</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>
|
||||
)}
|
||||
|
||||
<View style={styles.footer}>
|
||||
<LinearGradient
|
||||
colors={getPriorityGradient(goal.priority)}
|
||||
style={styles.priorityBadge}
|
||||
>
|
||||
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
|
||||
</LinearGradient>
|
||||
|
||||
{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>
|
||||
);
|
||||
{isCompleted && goal.completedDate && (
|
||||
<Text style={[typography.caption, { color: colors.success }]}>
|
||||
Completed {new Date(goal.completedDate).toLocaleDateString()}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</MinimalCard>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
borderRadius: theme.borderRadius.xl,
|
||||
padding: 16,
|
||||
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: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 12,
|
||||
marginLeft: 8,
|
||||
},
|
||||
titleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
flex: 1,
|
||||
},
|
||||
iconContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: 12,
|
||||
},
|
||||
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,
|
||||
},
|
||||
card: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
marginBottom: 12,
|
||||
},
|
||||
titleRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "flex-start",
|
||||
flex: 1,
|
||||
},
|
||||
titleContainer: {
|
||||
flex: 1,
|
||||
marginLeft: 12,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: "row",
|
||||
gap: 8,
|
||||
},
|
||||
actionButton: {
|
||||
padding: 4,
|
||||
},
|
||||
progressSection: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
progressInfo: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 8,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
},
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user