Files
SwimBuddy/src/features/active-workout/screens/PreSwimScreen.tsx
T

192 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useState } from "react";
import { View, Text, Pressable, ActivityIndicator } from "react-native";
import { useTranslation } from "react-i18next";
import { useNavigation, useRoute } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import type { RootStackParamList } from "@navigation/types";
import type { RouteProp } from "@react-navigation/native";
import { getWorkoutWithSets } from "@db/operations";
import { useSyncWorkout } from "../../watch-connectivity/hooks/useSyncWorkout";
import { useWatchConnection } from "../../watch-connectivity/hooks/useWatchConnection";
import { formatDistance } from "@utils/formatDistance";
import { STROKE_COLORS } from "@constants/strokes";
import type { WorkoutSet } from "@/types/workout";
type WorkoutWithSets = {
id: string;
name: string | null;
poolLength: number;
totalDistance: number | null;
sets: WorkoutSet[];
};
export function PreSwimScreen() {
const { t } = useTranslation("active-workout");
const { t: tc } = useTranslation("common");
const navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
const route = useRoute<RouteProp<RootStackParamList, "PreSwim">>();
const [workout, setWorkout] = useState<WorkoutWithSets | null>(null);
const [syncResult, setSyncResult] = useState<"sent" | "error" | null>(null);
const { isSyncing, sendWorkoutToWatch } = useSyncWorkout();
const { status, isPaired, isInstalled, isReady } = useWatchConnection();
useEffect(() => {
loadWorkout();
}, []);
const loadWorkout = async () => {
const data = await getWorkoutWithSets(route.params.workoutId);
setWorkout(data as WorkoutWithSets | null);
};
const handleStartSwim = async () => {
if (!workout || !isReady) return;
const result = await sendWorkoutToWatch(
workout.id,
workout.name,
workout.poolLength,
workout.sets
);
if ("error" in result) {
setSyncResult("error");
return;
}
setSyncResult("sent");
};
if (!workout) {
return (
<View className="flex-1 bg-white items-center justify-center">
<ActivityIndicator size="large" color="#0ea5e9" />
</View>
);
}
if (syncResult === "sent") {
return (
<View className="flex-1 bg-white p-4 items-center justify-center">
<View className="bg-green-50 rounded-2xl p-6 items-center mb-6 w-full">
<Text className="text-4xl mb-3">⌚</Text>
<Text className="text-xl font-bold text-green-800 mb-2">{t("workoutSentToWatch")}</Text>
<Text className="text-green-600 text-center">
{t("openWatchApp")}
</Text>
</View>
<Pressable
onPress={() => navigation.popToTop()}
className="bg-slate-900 rounded-2xl py-4 items-center w-full"
>
<Text className="text-white font-bold text-lg">{tc("done")}</Text>
</Pressable>
</View>
);
}
if (syncResult === "error") {
return (
<View className="flex-1 bg-white p-4 items-center justify-center">
<View className="bg-red-50 rounded-2xl p-6 items-center mb-6 w-full">
<Text className="text-4xl mb-3">⚠️</Text>
<Text className="text-xl font-bold text-red-800 mb-2">{t("failedToSend")}</Text>
<Text className="text-red-600 text-center">
{t("couldNotSend")}
</Text>
</View>
<Pressable
onPress={() => setSyncResult(null)}
className="bg-sky-500 rounded-2xl py-4 items-center w-full"
>
<Text className="text-white font-bold text-lg">{tc("retry")}</Text>
</Pressable>
</View>
);
}
return (
<View className="flex-1 bg-white p-4">
<View className="items-center mb-6">
<Text className="text-2xl font-bold text-slate-900 mb-1">
{workout.name || tc("workout")}
</Text>
<Text className="text-slate-500">
{formatDistance(workout.totalDistance ?? 0)} · {tc("pool", { length: workout.poolLength })}
</Text>
</View>
<View className="bg-sky-50 rounded-2xl p-4 mb-4 border border-sky-100">
<Text className="text-xs font-semibold text-sky-700 mb-3 uppercase tracking-wide">
{tc("sets")}
</Text>
{workout.sets.map((set, i) => (
<View
key={i}
className="flex-row items-center justify-between py-2 border-b border-sky-100 last:border-b-0"
>
<View className="flex-row items-center gap-2">
<View
className="w-3 h-3 rounded-full"
style={{ backgroundColor: STROKE_COLORS[set.stroke] ?? "#94a3b8" }}
/>
<Text className="text-slate-700 font-medium">{tc(`stroke.${set.stroke.toLowerCase()}`)}</Text>
</View>
<Text className="text-slate-600">
{set.repCount}×{set.distancePerRep}m
</Text>
</View>
))}
</View>
<View className="bg-slate-50 rounded-2xl p-4 mb-6">
<View className="flex-row justify-between mb-2">
<Text className="text-slate-500">{t("rest")}</Text>
<Text className="text-slate-900 font-medium">
{t("betweenSets", { duration: workout.sets[0]?.restDuration ?? 0 })}
</Text>
</View>
</View>
<View className="bg-slate-50 rounded-2xl p-3 mb-6 flex-row items-center gap-3">
<View
className={`w-3 h-3 rounded-full ${
isReady ? "bg-green-500" : isPaired ? "bg-yellow-500" : "bg-slate-300"
}`}
/>
<Text className="text-sm text-slate-600">
{isReady
? t("watchStatus.ready")
: isPaired
? isInstalled
? t("watchStatus.pairedNotInRange")
: t("watchStatus.pairedNotInstalled")
: t("watchStatus.noWatch")}
</Text>
</View>
<Pressable
onPress={handleStartSwim}
disabled={isSyncing || !isReady}
className={`rounded-2xl py-4 items-center ${
isSyncing || !isReady ? "bg-slate-300" : "bg-sky-500"
}`}
>
{isSyncing ? (
<ActivityIndicator color="#fff" />
) : (
<Text className={`font-bold text-lg ${!isReady ? "text-slate-500" : "text-white"}`}>
{t("sendToWatchAndStart")}
</Text>
)}
</Pressable>
{!isReady && (
<Text className="text-red-500 text-xs text-center mt-3">
{t("watchRequired")}
</Text>
)}
</View>
);
}