210 lines
8.7 KiB
TypeScript
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>
|
|
);
|
|
}
|