context and goals definition added
This commit is contained in:
1 parent
28b5b52a8f
commit
803c205994
10 files changed
+1073
-170
No files matched your search
@@ -0,0 +1,414 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Modal,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
ScrollView,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||
import type { CreateGoalData } from '../services/fitnessGoals';
|
||||
|
||||
interface GoalCreationModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (goalData: CreateGoalData) => Promise<void>;
|
||||
}
|
||||
|
||||
const GOAL_TYPES = [
|
||||
{ value: 'weight_target', label: 'Weight Target' },
|
||||
{ value: 'strength_milestone', label: 'Strength Milestone' },
|
||||
{ value: 'endurance_target', label: 'Endurance Target' },
|
||||
{ value: 'flexibility_goal', label: 'Flexibility Goal' },
|
||||
{ value: 'habit_building', label: 'Habit Building' },
|
||||
{ value: 'custom', label: 'Custom Goal' },
|
||||
] as const;
|
||||
|
||||
const PRIORITIES = [
|
||||
{ value: 'low', label: 'Low', color: '#10b981' },
|
||||
{ value: 'medium', label: 'Medium', color: '#f59e0b' },
|
||||
{ value: 'high', label: 'High', color: '#ef4444' },
|
||||
] as const;
|
||||
|
||||
export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationModalProps) {
|
||||
const [goalType, setGoalType] = useState<CreateGoalData['goalType']>('weight_target');
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [targetValue, setTargetValue] = useState('');
|
||||
const [currentValue, setCurrentValue] = useState('');
|
||||
const [unit, setUnit] = useState('');
|
||||
const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium');
|
||||
const [targetDate, setTargetDate] = useState<Date | undefined>();
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const resetForm = () => {
|
||||
setGoalType('weight_target');
|
||||
setTitle('');
|
||||
setDescription('');
|
||||
setTargetValue('');
|
||||
setCurrentValue('');
|
||||
setUnit('');
|
||||
setPriority('medium');
|
||||
setTargetDate(undefined);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!title.trim()) {
|
||||
alert('Please enter a goal title');
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const goalData: CreateGoalData = {
|
||||
goalType,
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
targetValue: targetValue ? parseFloat(targetValue) : undefined,
|
||||
currentValue: currentValue ? parseFloat(currentValue) : undefined,
|
||||
unit: unit.trim() || undefined,
|
||||
targetDate: targetDate?.toISOString(),
|
||||
priority,
|
||||
};
|
||||
|
||||
await onSubmit(goalData);
|
||||
resetForm();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error creating goal:', error);
|
||||
alert('Failed to create goal. Please try again.');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
resetForm();
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
visible={visible}
|
||||
animationType="slide"
|
||||
presentationStyle="pageSheet"
|
||||
onRequestClose={handleClose}
|
||||
>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.headerTitle}>Create Fitness Goal</Text>
|
||||
<TouchableOpacity onPress={handleClose} style={styles.closeButton}>
|
||||
<Ionicons name="close" size={28} color="#111827" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
|
||||
{/* Goal Type */}
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>Goal Type *</Text>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.typeScroll}>
|
||||
{GOAL_TYPES.map((type) => (
|
||||
<TouchableOpacity
|
||||
key={type.value}
|
||||
style={[
|
||||
styles.typeButton,
|
||||
goalType === type.value && styles.typeButtonActive,
|
||||
]}
|
||||
onPress={() => setGoalType(type.value)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.typeButtonText,
|
||||
goalType === type.value && styles.typeButtonTextActive,
|
||||
]}
|
||||
>
|
||||
{type.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
|
||||
{/* Title */}
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>Title *</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={title}
|
||||
onChangeText={setTitle}
|
||||
placeholder="e.g., Lose 5kg"
|
||||
placeholderTextColor="#9ca3af"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Description */}
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>Description</Text>
|
||||
<TextInput
|
||||
style={[styles.input, styles.textArea]}
|
||||
value={description}
|
||||
onChangeText={setDescription}
|
||||
placeholder="Optional description"
|
||||
placeholderTextColor="#9ca3af"
|
||||
multiline
|
||||
numberOfLines={3}
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Target Value & Unit */}
|
||||
<View style={styles.row}>
|
||||
<View style={[styles.field, styles.flex1]}>
|
||||
<Text style={styles.label}>Target Value</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={targetValue}
|
||||
onChangeText={setTargetValue}
|
||||
placeholder="e.g., 70"
|
||||
placeholderTextColor="#9ca3af"
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.field, styles.flex1]}>
|
||||
<Text style={styles.label}>Unit</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={unit}
|
||||
onChangeText={setUnit}
|
||||
placeholder="e.g., kg"
|
||||
placeholderTextColor="#9ca3af"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Current Value */}
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>Current Value</Text>
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
value={currentValue}
|
||||
onChangeText={setCurrentValue}
|
||||
placeholder="Starting value (optional)"
|
||||
placeholderTextColor="#9ca3af"
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Target Date */}
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>Target Date</Text>
|
||||
<TouchableOpacity
|
||||
style={styles.dateButton}
|
||||
onPress={() => setShowDatePicker(true)}
|
||||
>
|
||||
<Text style={targetDate ? styles.dateText : styles.datePlaceholder}>
|
||||
{targetDate ? targetDate.toLocaleDateString() : 'Select target date'}
|
||||
</Text>
|
||||
<Ionicons name="calendar-outline" size={20} color="#6b7280" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{showDatePicker && (
|
||||
<DateTimePicker
|
||||
value={targetDate || new Date()}
|
||||
mode="date"
|
||||
display={Platform.OS === 'ios' ? 'spinner' : 'default'}
|
||||
onChange={(event, selectedDate) => {
|
||||
setShowDatePicker(Platform.OS === 'ios');
|
||||
if (selectedDate) {
|
||||
setTargetDate(selectedDate);
|
||||
}
|
||||
}}
|
||||
minimumDate={new Date()}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Priority */}
|
||||
<View style={styles.field}>
|
||||
<Text style={styles.label}>Priority</Text>
|
||||
<View style={styles.priorityContainer}>
|
||||
{PRIORITIES.map((p) => (
|
||||
<TouchableOpacity
|
||||
key={p.value}
|
||||
style={[
|
||||
styles.priorityButton,
|
||||
priority === p.value && { backgroundColor: p.color },
|
||||
]}
|
||||
onPress={() => setPriority(p.value)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.priorityButtonText,
|
||||
priority === p.value && styles.priorityButtonTextActive,
|
||||
]}
|
||||
>
|
||||
{p.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.submitButton, submitting && styles.submitButtonDisabled]}
|
||||
onPress={handleSubmit}
|
||||
disabled={submitting}
|
||||
>
|
||||
<Text style={styles.submitButtonText}>
|
||||
{submitting ? 'Creating...' : 'Create Goal'}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f9fafb',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 20,
|
||||
paddingTop: Platform.OS === 'ios' ? 60 : 20,
|
||||
backgroundColor: '#fff',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e5e7eb',
|
||||
},
|
||||
headerTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
},
|
||||
closeButton: {
|
||||
padding: 4,
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
padding: 20,
|
||||
},
|
||||
field: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#374151',
|
||||
marginBottom: 8,
|
||||
},
|
||||
input: {
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
fontSize: 16,
|
||||
color: '#111827',
|
||||
},
|
||||
textArea: {
|
||||
height: 80,
|
||||
textAlignVertical: 'top',
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
flex1: {
|
||||
flex: 1,
|
||||
},
|
||||
typeScroll: {
|
||||
flexGrow: 0,
|
||||
},
|
||||
typeButton: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
backgroundColor: '#fff',
|
||||
marginRight: 8,
|
||||
},
|
||||
typeButtonActive: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderColor: '#2563eb',
|
||||
},
|
||||
typeButtonText: {
|
||||
fontSize: 14,
|
||||
color: '#374151',
|
||||
},
|
||||
typeButtonTextActive: {
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
dateButton: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
},
|
||||
dateText: {
|
||||
fontSize: 16,
|
||||
color: '#111827',
|
||||
},
|
||||
datePlaceholder: {
|
||||
fontSize: 16,
|
||||
color: '#9ca3af',
|
||||
},
|
||||
priorityContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
},
|
||||
priorityButton: {
|
||||
flex: 1,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#d1d5db',
|
||||
backgroundColor: '#fff',
|
||||
alignItems: 'center',
|
||||
},
|
||||
priorityButtonText: {
|
||||
fontSize: 14,
|
||||
color: '#374151',
|
||||
fontWeight: '500',
|
||||
},
|
||||
priorityButtonTextActive: {
|
||||
color: '#fff',
|
||||
fontWeight: '600',
|
||||
},
|
||||
footer: {
|
||||
padding: 20,
|
||||
paddingBottom: Platform.OS === 'ios' ? 40 : 20,
|
||||
backgroundColor: '#fff',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#e5e7eb',
|
||||
},
|
||||
submitButton: {
|
||||
backgroundColor: '#2563eb',
|
||||
borderRadius: 8,
|
||||
padding: 16,
|
||||
alignItems: 'center',
|
||||
},
|
||||
submitButtonDisabled: {
|
||||
opacity: 0.6,
|
||||
},
|
||||
submitButtonText: {
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,251 @@
|
||||
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';
|
||||
|
||||
interface GoalProgressCardProps {
|
||||
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;
|
||||
|
||||
// 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 getPriorityColor = (priority: string) => {
|
||||
switch (priority) {
|
||||
case 'high': return '#ef4444';
|
||||
case 'medium': return '#f59e0b';
|
||||
case 'low': return '#10b981';
|
||||
default: return '#6b7280';
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
style={[styles.card, isCompleted && styles.cardCompleted]}
|
||||
onPress={onPress}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleRow}>
|
||||
<Ionicons
|
||||
name={getGoalTypeIcon(goal.goalType) as any}
|
||||
size={24}
|
||||
color={isCompleted ? '#9ca3af' : '#2563eb'}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<View style={styles.actions}>
|
||||
{!isCompleted && onComplete && (
|
||||
<TouchableOpacity onPress={onComplete} style={styles.actionButton}>
|
||||
<Ionicons name="checkmark-circle-outline" size={24} color="#10b981" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{onDelete && (
|
||||
<TouchableOpacity onPress={handleDelete} style={styles.actionButton}>
|
||||
<Ionicons name="trash-outline" size={22} color="#ef4444" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</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}>{progress.toFixed(0)}%</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.progressBarContainer}>
|
||||
<View
|
||||
style={[
|
||||
styles.progressBar,
|
||||
{ width: `${Math.min(progress, 100)}%` },
|
||||
isCompleted && styles.progressBarCompleted
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.footer}>
|
||||
<View style={[styles.priorityBadge, { backgroundColor: getPriorityColor(goal.priority) }]}>
|
||||
<Text style={styles.priorityText}>{goal.priority.toUpperCase()}</Text>
|
||||
</View>
|
||||
|
||||
{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>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 12,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
cardCompleted: {
|
||||
backgroundColor: '#f0fdf4',
|
||||
borderColor: '#bbf7d0',
|
||||
borderWidth: 1,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 12,
|
||||
},
|
||||
titleRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'flex-start',
|
||||
flex: 1,
|
||||
},
|
||||
titleContainer: {
|
||||
marginLeft: 12,
|
||||
flex: 1,
|
||||
},
|
||||
title: {
|
||||
fontSize: 18,
|
||||
fontWeight: '600',
|
||||
color: '#111827',
|
||||
marginBottom: 4,
|
||||
},
|
||||
titleCompleted: {
|
||||
color: '#9ca3af',
|
||||
textDecorationLine: 'line-through',
|
||||
},
|
||||
description: {
|
||||
fontSize: 14,
|
||||
color: '#6b7280',
|
||||
lineHeight: 20,
|
||||
},
|
||||
actions: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
},
|
||||
actionButton: {
|
||||
padding: 4,
|
||||
},
|
||||
progressSection: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
progressInfo: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 8,
|
||||
},
|
||||
progressText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '500',
|
||||
color: '#374151',
|
||||
},
|
||||
progressPercentage: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#2563eb',
|
||||
},
|
||||
progressBarContainer: {
|
||||
height: 8,
|
||||
backgroundColor: '#e5e7eb',
|
||||
borderRadius: 4,
|
||||
overflow: 'hidden',
|
||||
},
|
||||
progressBar: {
|
||||
height: '100%',
|
||||
backgroundColor: '#2563eb',
|
||||
borderRadius: 4,
|
||||
},
|
||||
progressBarCompleted: {
|
||||
backgroundColor: '#10b981',
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
priorityBadge: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 4,
|
||||
},
|
||||
priorityText: {
|
||||
fontSize: 10,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
},
|
||||
daysRemaining: {
|
||||
fontSize: 12,
|
||||
color: '#6b7280',
|
||||
},
|
||||
overdue: {
|
||||
color: '#ef4444',
|
||||
fontWeight: '600',
|
||||
},
|
||||
completedDate: {
|
||||
fontSize: 12,
|
||||
color: '#10b981',
|
||||
fontWeight: '500',
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user