192 lines
6.5 KiB
TypeScript
192 lines
6.5 KiB
TypeScript
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>
|
||
);
|
||
}
|