Files
fitaiProto/apps/mobile/src/components/GoalCreationModal.tsx
T
2026-03-11 08:55:05 +01:00

527 lines
15 KiB
TypeScript

import React, { useState } from "react";
import {
View,
Text,
StyleSheet,
Modal,
TextInput,
TouchableOpacity,
ScrollView,
Platform,
Alert,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import DateTimePicker from "@react-native-community/datetimepicker";
import type { CreateGoalData } from "../services/fitnessGoals";
import { useTheme } from "../contexts/ThemeContext";
import { MinimalButton } from "./MinimalButton";
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" },
{ value: "medium", label: "Medium" },
{ value: "high", label: "High" },
] as const;
export function GoalCreationModal({
visible,
onClose,
onSubmit,
}: GoalCreationModalProps) {
const { colors, typography } = useTheme();
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.alert("Error", "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.alert("Error", "Failed to create goal. Please try again.");
} finally {
setSubmitting(false);
}
};
const handleClose = () => {
resetForm();
onClose();
};
const getPriorityColor = (p: "low" | "medium" | "high") => {
switch (p) {
case "high":
return colors.danger;
case "medium":
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,
},
]}
>
<Text style={[typography.h2, { color: colors.textPrimary }]}>
Create Fitness Goal
</Text>
<TouchableOpacity onPress={handleClose} style={styles.closeButton}>
<Ionicons name="close" size={28} color={colors.textPrimary} />
</TouchableOpacity>
</View>
<ScrollView style={styles.content} showsVerticalScrollIndicator={false}>
{/* Goal Type */}
<View style={styles.field}>
<Text
style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
>
Goal Type *
</Text>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
style={styles.typeScroll}
>
{GOAL_TYPES.map((type) => (
<TouchableOpacity
key={type.value}
style={[
styles.typeButton,
{
borderColor: colors.border,
backgroundColor: colors.surface,
},
goalType === type.value && {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
]}
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 */}
<View style={styles.field}>
<Text
style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
>
Title *
</Text>
<TextInput
style={[
styles.input,
{
backgroundColor: colors.surface,
borderColor: colors.border,
color: colors.textPrimary,
},
]}
value={title}
onChangeText={setTitle}
placeholder="e.g., Lose 5kg"
placeholderTextColor={colors.textTertiary}
/>
</View>
{/* Description */}
<View style={styles.field}>
<Text
style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
>
Description
</Text>
<TextInput
style={[
styles.input,
styles.textArea,
{
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 */}
<View style={styles.row}>
<View style={[styles.field, styles.flex1]}>
<Text
style={[typography.bodyEmphasis, { color: colors.textPrimary }]}
>
Target Value
</Text>
<TextInput
style={[
styles.input,
{
backgroundColor: colors.surface,
borderColor: colors.border,
color: colors.textPrimary,
},
]}
value={targetValue}
onChangeText={setTargetValue}
placeholder="e.g., 70"
placeholderTextColor={colors.textTertiary}
keyboardType="numeric"
/>
</View>
<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({
container: {
flex: 1,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 20,
paddingTop: Platform.OS === "ios" ? 60 : 20,
borderBottomWidth: 1,
},
closeButton: {
padding: 4,
},
content: {
flex: 1,
padding: 20,
},
field: {
marginBottom: 20,
},
input: {
borderWidth: 1,
borderRadius: 8,
padding: 12,
fontSize: 16,
marginTop: 8,
},
textArea: {
height: 80,
textAlignVertical: "top",
},
row: {
flexDirection: "row",
gap: 12,
},
flex1: {
flex: 1,
},
typeScroll: {
flexGrow: 0,
marginTop: 8,
},
typeButton: {
paddingHorizontal: 16,
paddingVertical: 8,
borderRadius: 8,
borderWidth: 1,
marginRight: 8,
},
dateButton: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
borderWidth: 1,
borderRadius: 8,
padding: 12,
marginTop: 8,
},
priorityContainer: {
flexDirection: "row",
gap: 12,
marginTop: 8,
},
priorityButton: {
flex: 1,
paddingVertical: 12,
borderRadius: 8,
borderWidth: 1,
alignItems: "center",
},
footer: {
padding: 20,
paddingBottom: Platform.OS === "ios" ? 40 : 20,
borderTopWidth: 1,
},
submitButton: {
width: "100%",
},
datePickerContainer: {
borderRadius: 8,
borderWidth: 1,
marginTop: 8,
overflow: "hidden",
},
});