feat: initial project scaffold with Watch companion app
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { View, Text, Pressable, ActivityIndicator } from "react-native";
|
||||
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 "../hooks/useSyncWorkout";
|
||||
import { useWatchConnection } from "../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 navigation = useNavigation<NativeStackNavigationProp<RootStackParamList>>();
|
||||
const route = useRoute<RouteProp<RootStackParamList, "PreSwim">>();
|
||||
const [workout, setWorkout] = useState<WorkoutWithSets | null>(null);
|
||||
const { isSyncing, sendWorkoutToWatch } = useSyncWorkout();
|
||||
const { status, isPaired, isInstalled } = useWatchConnection();
|
||||
|
||||
useEffect(() => {
|
||||
loadWorkout();
|
||||
}, []);
|
||||
|
||||
const loadWorkout = async () => {
|
||||
const data = await getWorkoutWithSets(route.params.workoutId);
|
||||
setWorkout(data as WorkoutWithSets | null);
|
||||
};
|
||||
|
||||
const handleStartSwim = async () => {
|
||||
if (!workout) return;
|
||||
|
||||
if (status !== "connected") {
|
||||
navigation.navigate("PostSwim", { workoutId: route.params.workoutId });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await sendWorkoutToWatch(
|
||||
workout.id,
|
||||
workout.name,
|
||||
workout.poolLength,
|
||||
workout.sets
|
||||
);
|
||||
|
||||
if ("error" in result) {
|
||||
navigation.navigate("PostSwim", { workoutId: route.params.workoutId });
|
||||
return;
|
||||
}
|
||||
|
||||
navigation.navigate("PostSwim", { workoutId: route.params.workoutId });
|
||||
};
|
||||
|
||||
if (!workout) {
|
||||
return (
|
||||
<View className="flex-1 bg-white items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#0ea5e9" />
|
||||
</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 || "Workout"}
|
||||
</Text>
|
||||
<Text className="text-slate-500">
|
||||
{formatDistance(workout.totalDistance ?? 0)} · {workout.poolLength}m pool
|
||||
</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">
|
||||
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">{set.stroke}</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">Rest</Text>
|
||||
<Text className="text-slate-900 font-medium">
|
||||
{workout.sets[0]?.restDuration ?? 0}s between sets
|
||||
</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 ${
|
||||
status === "connected" ? "bg-green-500" : "bg-slate-300"
|
||||
}`}
|
||||
/>
|
||||
<Text className="text-sm text-slate-600">
|
||||
{status === "connected"
|
||||
? "Apple Watch connected"
|
||||
: isPaired
|
||||
? "Watch paired but not connected"
|
||||
: "No Watch paired"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Pressable
|
||||
onPress={handleStartSwim}
|
||||
disabled={isSyncing}
|
||||
className={`rounded-2xl py-4 items-center ${
|
||||
isSyncing ? "bg-sky-300" : "bg-sky-500"
|
||||
}`}
|
||||
>
|
||||
{isSyncing ? (
|
||||
<ActivityIndicator color="#fff" />
|
||||
) : (
|
||||
<Text className="text-white font-bold text-lg">
|
||||
{status === "connected" ? "Send to Watch & Start" : "Start Without Watch"}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
|
||||
<Text className="text-slate-400 text-xs text-center mt-3">
|
||||
{status === "connected"
|
||||
? "Workout will be sent to your Apple Watch"
|
||||
: "Workout will be tracked on this device"}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user