527 lines
15 KiB
TypeScript
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",
|
|
},
|
|
});
|