Files
SwimBuddy/src/features/workout-builder/screens/BuilderScreen.tsx
T

210 lines
8.7 KiB
TypeScript

import { useState } from "react";
import { View, Text, TextInput, Pressable, ScrollView, Alert, KeyboardAvoidingView, Platform } from "react-native";
import { useTranslation } from "react-i18next";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import type { RootStackParamList } from "@navigation/types";
import { useWorkoutBuilder } from "../hooks/useWorkoutBuilder";
import { useSettings } from "../../../providers/WatchProvider";
import type { StrokeType } from "@/types/workout";
const STROKES: StrokeType[] = ["Freestyle", "Backstroke", "Breaststroke", "Butterfly", "Mixed"];
export function BuilderScreen() {
const { t } = useTranslation("workout");
const { t: tc } = useTranslation("common");
const { t: tb } = useTranslation("navigation");
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const { poolLength: defaultPoolLength } = useSettings();
const [poolLength, setPoolLength] = useState(defaultPoolLength);
const [fieldValues, setFieldValues] = useState<Record<string, string>>({});
const {
sets,
workoutName,
setWorkoutName,
totalDistance,
isSaving,
addSet,
updateSet,
removeSet,
save,
} = useWorkoutBuilder();
const handleSave = async () => {
try {
const workoutId = await save(poolLength);
navigation.navigate("PreSwim", { workoutId });
} catch (e) {
console.error("Failed to save workout:", e);
Alert.alert(tc("validation.error"), tc("validation.failedToSave", { error: String(e) }));
}
};
return (
<KeyboardAvoidingView
className="flex-1"
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 90 : 0}
>
<ScrollView className="flex-1 bg-white" contentContainerStyle={{ paddingBottom: 40 }}>
<View className="p-4">
<Text className="text-2xl font-bold text-slate-900 mb-1">{tb("buildWorkout")}</Text>
<Text className="text-slate-500 mb-4">
{totalDistance}m total · {tc("pool", { length: poolLength })}
</Text>
<View className="mb-4">
<Text className="text-sm font-medium text-slate-500 mb-1">{t("workoutName")}</Text>
<TextInput
value={workoutName}
onChangeText={setWorkoutName}
placeholder={t("placeholder.workoutName")}
placeholderTextColor="#94a3b8"
className="bg-slate-50 border border-slate-200 rounded-xl px-4 py-3 text-slate-900"
/>
</View>
<View className="mb-4">
<Text className="text-sm font-medium text-slate-500 mb-1">{t("poolLength")}</Text>
<View className="flex-row gap-2">
{[25, 50].map((length) => (
<Pressable
key={length}
onPress={() => setPoolLength(length)}
className={`flex-1 py-2.5 rounded-xl items-center ${
poolLength === length ? "bg-sky-500" : "bg-slate-100"
}`}
>
<Text
className={`font-semibold ${
poolLength === length ? "text-white" : "text-slate-600"
}`}
>
{length}m
</Text>
</Pressable>
))}
</View>
</View>
{sets.map((set, index) => (
<View key={index} className="bg-slate-50 rounded-2xl p-4 mb-3 border border-slate-200">
<View className="flex-row justify-between items-center mb-3">
<Text className="font-semibold text-slate-700">{t("setNumber", { number: index + 1 })}</Text>
{sets.length > 1 && (
<Pressable onPress={() => removeSet(index)}>
<Text className="text-red-500 text-sm font-medium">{t("remove")}</Text>
</Pressable>
)}
</View>
<Text className="text-xs font-medium text-slate-500 mb-1.5 uppercase tracking-wide">
{t("stroke")}
</Text>
<ScrollView horizontal showsHorizontalScrollIndicator={false} className="mb-3">
{STROKES.map((stroke) => (
<Pressable
key={stroke}
onPress={() => updateSet(index, { stroke })}
className={`mr-2 px-3 py-1.5 rounded-full ${
set.stroke === stroke ? "bg-sky-500" : "bg-slate-200"
}`}
>
<Text
className={`text-xs font-medium ${
set.stroke === stroke ? "text-white" : "text-slate-600"
}`}
>
{tc(`stroke.${stroke.toLowerCase()}`)}
</Text>
</Pressable>
))}
</ScrollView>
<View className="flex-row gap-2">
<View className="flex-1">
<Text className="text-xs font-medium text-slate-500 mb-1">{t("distanceM")}</Text>
<TextInput
keyboardType="numeric"
value={fieldValues[`${index}-distance`] ?? String(set.distancePerRep)}
onChangeText={(val) => setFieldValues((prev) => ({ ...prev, [`${index}-distance`]: val }))}
onBlur={() => {
const num = Number(fieldValues[`${index}-distance`] ?? set.distancePerRep);
updateSet(index, { distancePerRep: isNaN(num) || num < poolLength ? poolLength : num });
setFieldValues((prev) => {
const next = { ...prev };
delete next[`${index}-distance`];
return next;
});
}}
placeholder={String(poolLength)}
placeholderTextColor="#94a3b8"
className="bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900"
/>
</View>
<View className="flex-1">
<Text className="text-xs font-medium text-slate-500 mb-1">{t("reps")}</Text>
<TextInput
keyboardType="numeric"
value={fieldValues[`${index}-rep`] ?? String(set.repCount)}
onChangeText={(val) => setFieldValues((prev) => ({ ...prev, [`${index}-rep`]: val }))}
onBlur={() => {
const num = Number(fieldValues[`${index}-rep`] ?? set.repCount);
updateSet(index, { repCount: isNaN(num) || num < 1 ? 1 : num });
setFieldValues((prev) => {
const next = { ...prev };
delete next[`${index}-rep`];
return next;
});
}}
placeholder="1"
placeholderTextColor="#94a3b8"
className="bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900"
/>
</View>
<View className="flex-1">
<Text className="text-xs font-medium text-slate-500 mb-1">{t("restS")}</Text>
<TextInput
keyboardType="numeric"
value={fieldValues[`${index}-rest`] ?? String(set.restDuration)}
onChangeText={(val) => setFieldValues((prev) => ({ ...prev, [`${index}-rest`]: val }))}
onBlur={() => {
const num = Number(fieldValues[`${index}-rest`] ?? set.restDuration);
updateSet(index, { restDuration: isNaN(num) || num < 0 ? 0 : num });
setFieldValues((prev) => {
const next = { ...prev };
delete next[`${index}-rest`];
return next;
});
}}
placeholder="0"
placeholderTextColor="#94a3b8"
className="bg-white border border-slate-300 rounded-lg px-3 py-2 text-slate-900"
/>
</View>
</View>
</View>
))}
<Pressable
onPress={addSet}
className="bg-slate-100 rounded-2xl py-3 items-center mb-6 border border-slate-200"
>
<Text className="text-sky-600 font-semibold">{t("addSet")}</Text>
</Pressable>
<Pressable
onPress={handleSave}
disabled={isSaving}
className={`rounded-2xl py-4 items-center mb-8 ${isSaving ? "bg-sky-300" : "bg-sky-500"}`}
>
<Text className="text-white font-bold text-lg">
{isSaving ? tc("saving") : t("startSwim")}
</Text>
</Pressable>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}