goal creation style fix

This commit is contained in:
echo 2026-03-11 08:55:05 +01:00
parent b1439f059a
commit a5f761062e
2 changed files with 499 additions and 399 deletions

Binary file not shown.

View File

@ -1,426 +1,526 @@
import React, { useState } from 'react'; import React, { useState } from "react";
import { import {
View, View,
Text, Text,
StyleSheet, StyleSheet,
Modal, Modal,
TextInput, TextInput,
TouchableOpacity, TouchableOpacity,
ScrollView, ScrollView,
Platform, Platform,
Alert, Alert,
} from 'react-native'; } from "react-native";
import { Ionicons } from '@expo/vector-icons'; import { Ionicons } from "@expo/vector-icons";
import DateTimePicker from '@react-native-community/datetimepicker'; import DateTimePicker from "@react-native-community/datetimepicker";
import type { CreateGoalData } from '../services/fitnessGoals'; import type { CreateGoalData } from "../services/fitnessGoals";
import { useTheme } from "../contexts/ThemeContext";
import { MinimalButton } from "./MinimalButton";
interface GoalCreationModalProps { interface GoalCreationModalProps {
visible: boolean; visible: boolean;
onClose: () => void; onClose: () => void;
onSubmit: (goalData: CreateGoalData) => Promise<void>; onSubmit: (goalData: CreateGoalData) => Promise<void>;
} }
const GOAL_TYPES = [ const GOAL_TYPES = [
{ value: 'weight_target', label: 'Weight Target' }, { value: "weight_target", label: "Weight Target" },
{ value: 'strength_milestone', label: 'Strength Milestone' }, { value: "strength_milestone", label: "Strength Milestone" },
{ value: 'endurance_target', label: 'Endurance Target' }, { value: "endurance_target", label: "Endurance Target" },
{ value: 'flexibility_goal', label: 'Flexibility Goal' }, { value: "flexibility_goal", label: "Flexibility Goal" },
{ value: 'habit_building', label: 'Habit Building' }, { value: "habit_building", label: "Habit Building" },
{ value: 'custom', label: 'Custom Goal' }, { value: "custom", label: "Custom Goal" },
] as const; ] as const;
const PRIORITIES = [ const PRIORITIES = [
{ value: 'low', label: 'Low', color: '#10b981' }, { value: "low", label: "Low" },
{ value: 'medium', label: 'Medium', color: '#f59e0b' }, { value: "medium", label: "Medium" },
{ value: 'high', label: 'High', color: '#ef4444' }, { value: "high", label: "High" },
] as const; ] as const;
export function GoalCreationModal({ visible, onClose, onSubmit }: GoalCreationModalProps) { export function GoalCreationModal({
const [goalType, setGoalType] = useState<CreateGoalData['goalType']>('weight_target'); visible,
const [title, setTitle] = useState(''); onClose,
const [description, setDescription] = useState(''); onSubmit,
const [targetValue, setTargetValue] = useState(''); }: GoalCreationModalProps) {
const [currentValue, setCurrentValue] = useState(''); const { colors, typography } = useTheme();
const [unit, setUnit] = useState(''); const [goalType, setGoalType] =
const [priority, setPriority] = useState<'low' | 'medium' | 'high'>('medium'); useState<CreateGoalData["goalType"]>("weight_target");
const [targetDate, setTargetDate] = useState<Date | undefined>(); const [title, setTitle] = useState("");
const [showDatePicker, setShowDatePicker] = useState(false); const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false); 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 = () => { const resetForm = () => {
setGoalType('weight_target'); setGoalType("weight_target");
setTitle(''); setTitle("");
setDescription(''); setDescription("");
setTargetValue(''); setTargetValue("");
setCurrentValue(''); setCurrentValue("");
setUnit(''); setUnit("");
setPriority('medium'); setPriority("medium");
setTargetDate(undefined); setTargetDate(undefined);
}; };
const handleSubmit = async () => { const handleSubmit = async () => {
if (!title.trim()) { if (!title.trim()) {
Alert.alert('Error', 'Please enter a goal title'); Alert.alert("Error", "Please enter a goal title");
return; return;
} }
setSubmitting(true); setSubmitting(true);
try { try {
const goalData: CreateGoalData = { const goalData: CreateGoalData = {
goalType, goalType,
title: title.trim(), title: title.trim(),
description: description.trim() || undefined, description: description.trim() || undefined,
targetValue: targetValue ? parseFloat(targetValue) : undefined, targetValue: targetValue ? parseFloat(targetValue) : undefined,
currentValue: currentValue ? parseFloat(currentValue) : undefined, currentValue: currentValue ? parseFloat(currentValue) : undefined,
unit: unit.trim() || undefined, unit: unit.trim() || undefined,
targetDate: targetDate?.toISOString(), targetDate: targetDate?.toISOString(),
priority, priority,
}; };
await onSubmit(goalData); await onSubmit(goalData);
resetForm(); resetForm();
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('Error creating goal:', error); console.error("Error creating goal:", error);
Alert.alert('Error', 'Failed to create goal. Please try again.'); Alert.alert("Error", "Failed to create goal. Please try again.");
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
}; };
const handleClose = () => { const handleClose = () => {
resetForm(); resetForm();
onClose(); onClose();
}; };
return ( const getPriorityColor = (p: "low" | "medium" | "high") => {
<Modal switch (p) {
visible={visible} case "high":
animationType="slide" return colors.danger;
presentationStyle="pageSheet" case "medium":
onRequestClose={handleClose} return colors.warning;
case "low":
return colors.success;
}
};
return (
<Modal
visible={visible}
animationType="slide"
presentationStyle="pageSheet"
onRequestClose={handleClose}
>
<View style={[styles.container, { backgroundColor: colors.background }]}>
<View
style={[
styles.header,
{
backgroundColor: colors.surface,
borderBottomColor: colors.border,
},
]}
> >
<View style={styles.container}> <Text style={[typography.h2, { color: colors.textPrimary }]}>
<View style={styles.header}> Create Fitness Goal
<Text style={styles.headerTitle}>Create Fitness Goal</Text> </Text>
<TouchableOpacity onPress={handleClose} style={styles.closeButton}> <TouchableOpacity onPress={handleClose} style={styles.closeButton}>
<Ionicons name="close" size={28} color="#111827" /> <Ionicons name="close" size={28} color={colors.textPrimary} />
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}> <ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
{/* Goal Type */} {/* Goal Type */}
<View style={styles.field}> <View style={styles.field}>
<Text style={styles.label}>Goal Type *</Text> <Text
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.typeScroll}> style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
{GOAL_TYPES.map((type) => ( >
<TouchableOpacity Goal Type *
key={type.value} </Text>
style={[ <ScrollView
styles.typeButton, horizontal
goalType === type.value && styles.typeButtonActive, showsHorizontalScrollIndicator={false}
]} style={styles.typeScroll}
onPress={() => setGoalType(type.value)} >
> {GOAL_TYPES.map((type) => (
<Text <TouchableOpacity
style={[ key={type.value}
styles.typeButtonText, style={[
goalType === type.value && styles.typeButtonTextActive, styles.typeButton,
]} {
> borderColor: colors.border,
{type.label} backgroundColor: colors.surface,
</Text> },
</TouchableOpacity> goalType === type.value && {
))} backgroundColor: colors.primary,
</ScrollView> borderColor: colors.primary,
</View> },
]}
onPress={() => setGoalType(type.value)}
>
<Text
style={[
typography.caption,
{ color: colors.textSecondary },
goalType === type.value && {
color: colors.white,
fontWeight: "600",
},
]}
>
{type.label}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
{/* Title */} {/* Title */}
<View style={styles.field}> <View style={styles.field}>
<Text style={styles.label}>Title *</Text> <Text
<TextInput style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
style={styles.input} >
value={title} Title *
onChangeText={setTitle} </Text>
placeholder="e.g., Lose 5kg" <TextInput
placeholderTextColor="#9ca3af" style={[
/> styles.input,
</View> {
backgroundColor: colors.surface,
borderColor: colors.border,
color: colors.textPrimary,
},
]}
value={title}
onChangeText={setTitle}
placeholder="e.g., Lose 5kg"
placeholderTextColor={colors.textTertiary}
/>
</View>
{/* Description */} {/* Description */}
<View style={styles.field}> <View style={styles.field}>
<Text style={styles.label}>Description</Text> <Text
<TextInput style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
style={[styles.input, styles.textArea]} >
value={description} Description
onChangeText={setDescription} </Text>
placeholder="Optional description" <TextInput
placeholderTextColor="#9ca3af" style={[
multiline styles.input,
numberOfLines={3} styles.textArea,
/> {
</View> backgroundColor: colors.surface,
borderColor: colors.border,
color: colors.textPrimary,
},
]}
value={description}
onChangeText={setDescription}
placeholder="Optional description"
placeholderTextColor={colors.textTertiary}
multiline
numberOfLines={3}
/>
</View>
{/* Target Value & Unit */} {/* Target Value & Unit */}
<View style={styles.row}> <View style={styles.row}>
<View style={[styles.field, styles.flex1]}> <View style={[styles.field, styles.flex1]}>
<Text style={styles.label}>Target Value</Text> <Text
<TextInput style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
style={styles.input} >
value={targetValue} Target Value
onChangeText={setTargetValue} </Text>
placeholder="e.g., 70" <TextInput
placeholderTextColor="#9ca3af" style={[
keyboardType="numeric" styles.input,
/> {
</View> backgroundColor: colors.surface,
<View style={[styles.field, styles.flex1]}> borderColor: colors.border,
<Text style={styles.label}>Unit</Text> color: colors.textPrimary,
<TextInput },
style={styles.input} ]}
value={unit} value={targetValue}
onChangeText={setUnit} onChangeText={setTargetValue}
placeholder="e.g., kg" placeholder="e.g., 70"
placeholderTextColor="#9ca3af" placeholderTextColor={colors.textTertiary}
/> keyboardType="numeric"
</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(!showDatePicker)}
>
<Text style={targetDate ? styles.dateText : styles.datePlaceholder}>
{targetDate ? targetDate.toLocaleDateString() : 'Select target date'}
</Text>
<Ionicons name={showDatePicker ? "chevron-up" : "calendar-outline"} size={20} color="#6b7280" />
</TouchableOpacity>
</View>
{showDatePicker && (
<View style={Platform.OS === 'ios' ? styles.datePickerContainer : undefined}>
<DateTimePicker
value={targetDate || new Date()}
mode="date"
display={Platform.OS === 'ios' ? 'inline' : 'default'}
onChange={(event, selectedDate) => {
setShowDatePicker(Platform.OS === 'ios');
if (selectedDate) {
setTargetDate(selectedDate);
}
}}
minimumDate={new Date()}
themeVariant="light"
/>
</View>
)}
{/* 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> </View>
</Modal> <View style={[styles.field, styles.flex1]}>
); <Text
style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
>
Unit
</Text>
<TextInput
style={[
styles.input,
{
backgroundColor: colors.surface,
borderColor: colors.border,
color: colors.textPrimary,
},
]}
value={unit}
onChangeText={setUnit}
placeholder="e.g., kg"
placeholderTextColor={colors.textTertiary}
/>
</View>
</View>
{/* Current Value */}
<View style={styles.field}>
<Text
style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
>
Current Value
</Text>
<TextInput
style={[
styles.input,
{
backgroundColor: colors.surface,
borderColor: colors.border,
color: colors.textPrimary,
},
]}
value={currentValue}
onChangeText={setCurrentValue}
placeholder="Starting value (optional)"
placeholderTextColor={colors.textTertiary}
keyboardType="numeric"
/>
</View>
{/* Target Date */}
<View style={styles.field}>
<Text
style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
>
Target Date
</Text>
<TouchableOpacity
style={[
styles.dateButton,
{
backgroundColor: colors.surface,
borderColor: colors.border,
},
]}
onPress={() => setShowDatePicker(!showDatePicker)}
>
<Text
style={[
typography.body,
{
color: targetDate
? colors.textPrimary
: colors.textTertiary,
},
]}
>
{targetDate
? targetDate.toLocaleDateString()
: "Select target date"}
</Text>
<Ionicons
name={showDatePicker ? "chevron-up" : "calendar-outline"}
size={20}
color={colors.textSecondary}
/>
</TouchableOpacity>
</View>
{showDatePicker && (
<View
style={
Platform.OS === "ios"
? [
styles.datePickerContainer,
{
backgroundColor: colors.surface,
borderColor: colors.border,
},
]
: undefined
}
>
<DateTimePicker
value={targetDate || new Date()}
mode="date"
display={Platform.OS === "ios" ? "inline" : "default"}
onChange={(event, selectedDate) => {
setShowDatePicker(Platform.OS === "ios");
if (selectedDate) {
setTargetDate(selectedDate);
}
}}
minimumDate={new Date()}
themeVariant="light"
/>
</View>
)}
{/* Priority */}
<View style={styles.field}>
<Text
style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
>
Priority
</Text>
<View style={styles.priorityContainer}>
{PRIORITIES.map((p) => (
<TouchableOpacity
key={p.value}
style={[
styles.priorityButton,
{
borderColor: colors.border,
backgroundColor: colors.surface,
},
priority === p.value && {
backgroundColor: getPriorityColor(p.value),
borderColor: getPriorityColor(p.value),
},
]}
onPress={() => setPriority(p.value)}
>
<Text
style={[
typography.body,
{ color: colors.textSecondary },
priority === p.value && {
color: colors.white,
fontWeight: "600",
},
]}
>
{p.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
</ScrollView>
<View
style={[
styles.footer,
{
backgroundColor: colors.surface,
borderTopColor: colors.border,
},
]}
>
<MinimalButton
variant="primary"
title={submitting ? "Creating..." : "Create Goal"}
onPress={handleSubmit}
disabled={submitting}
loading={submitting}
style={styles.submitButton}
/>
</View>
</View>
</Modal>
);
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
backgroundColor: '#f9fafb', },
}, header: {
header: { flexDirection: "row",
flexDirection: 'row', justifyContent: "space-between",
justifyContent: 'space-between', alignItems: "center",
alignItems: 'center', padding: 20,
padding: 20, paddingTop: Platform.OS === "ios" ? 60 : 20,
paddingTop: Platform.OS === 'ios' ? 60 : 20, borderBottomWidth: 1,
backgroundColor: '#fff', },
borderBottomWidth: 1, closeButton: {
borderBottomColor: '#e5e7eb', padding: 4,
}, },
headerTitle: { content: {
fontSize: 20, flex: 1,
fontWeight: '600', padding: 20,
color: '#111827', },
}, field: {
closeButton: { marginBottom: 20,
padding: 4, },
}, input: {
content: { borderWidth: 1,
flex: 1, borderRadius: 8,
padding: 20, padding: 12,
}, fontSize: 16,
field: { marginTop: 8,
marginBottom: 20, },
}, textArea: {
label: { height: 80,
fontSize: 14, textAlignVertical: "top",
fontWeight: '600', },
color: '#374151', row: {
marginBottom: 8, flexDirection: "row",
}, gap: 12,
input: { },
backgroundColor: '#fff', flex1: {
borderWidth: 1, flex: 1,
borderColor: '#d1d5db', },
borderRadius: 8, typeScroll: {
padding: 12, flexGrow: 0,
fontSize: 16, marginTop: 8,
color: '#111827', },
}, typeButton: {
textArea: { paddingHorizontal: 16,
height: 80, paddingVertical: 8,
textAlignVertical: 'top', borderRadius: 8,
}, borderWidth: 1,
row: { marginRight: 8,
flexDirection: 'row', },
gap: 12, dateButton: {
}, flexDirection: "row",
flex1: { justifyContent: "space-between",
flex: 1, alignItems: "center",
}, borderWidth: 1,
typeScroll: { borderRadius: 8,
flexGrow: 0, padding: 12,
}, marginTop: 8,
typeButton: { },
paddingHorizontal: 16, priorityContainer: {
paddingVertical: 8, flexDirection: "row",
borderRadius: 8, gap: 12,
borderWidth: 1, marginTop: 8,
borderColor: '#d1d5db', },
backgroundColor: '#fff', priorityButton: {
marginRight: 8, flex: 1,
}, paddingVertical: 12,
typeButtonActive: { borderRadius: 8,
backgroundColor: '#2563eb', borderWidth: 1,
borderColor: '#2563eb', alignItems: "center",
}, },
typeButtonText: { footer: {
fontSize: 14, padding: 20,
color: '#374151', paddingBottom: Platform.OS === "ios" ? 40 : 20,
}, borderTopWidth: 1,
typeButtonTextActive: { },
color: '#fff', submitButton: {
fontWeight: '600', width: "100%",
}, },
dateButton: { datePickerContainer: {
flexDirection: 'row', borderRadius: 8,
justifyContent: 'space-between', borderWidth: 1,
alignItems: 'center', marginTop: 8,
backgroundColor: '#fff', overflow: "hidden",
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',
},
datePickerContainer: {
backgroundColor: '#fff',
borderRadius: 8,
borderWidth: 1,
borderColor: '#d1d5db',
marginTop: 8,
overflow: 'hidden',
},
}); });