feat: initial project scaffold with Watch companion app

This commit is contained in:
2026-08-04 17:28:06 +02:00
parent adbb4f01c5
commit 43b6467efb
65 changed files with 6361 additions and 166 deletions
@@ -0,0 +1,60 @@
import { useEffect, useState, useCallback } from "react";
import { getTemplates, deleteTemplate, toggleTemplateFavorite } from "@db/operations";
import type { WorkoutSet } from "@types/workout";
type Template = {
id: string;
name: string;
description: string | null;
poolLength: number;
jsonData: string;
isFavorite: boolean;
createdAt: string;
};
export function useTemplates() {
const [templates, setTemplates] = useState<Template[]>([]);
const [isLoading, setIsLoading] = useState(true);
const load = useCallback(async () => {
setIsLoading(true);
try {
const data = await getTemplates();
setTemplates(data as Template[]);
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
load();
}, [load]);
const remove = useCallback(
async (id: string) => {
await deleteTemplate(id);
setTemplates((prev) => prev.filter((t) => t.id !== id));
},
[]
);
const toggleFavorite = useCallback(
async (id: string, current: boolean) => {
await toggleTemplateFavorite(id, !current);
setTemplates((prev) =>
prev.map((t) => (t.id === id ? { ...t, isFavorite: !current } : t))
);
},
[]
);
const parseSets = (template: Template): WorkoutSet[] => {
try {
return JSON.parse(template.jsonData);
} catch {
return [];
}
};
return { templates, isLoading, remove, toggleFavorite, parseSets, reload: load };
}
@@ -0,0 +1,64 @@
import { View, Text, Pressable, FlatList, Alert } from "react-native";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import type { WorkoutStackParamList } from "@navigation/types";
import { useTemplates } from "../hooks/useTemplates";
export function TemplatesScreen() {
const navigation = useNavigation<NativeStackNavigationProp<WorkoutStackParamList>>();
const { templates, remove, toggleFavorite } = useTemplates();
const handleDelete = (id: string, name: string) => {
Alert.alert("Delete Template", `Delete "${name}"?`, [
{ text: "Cancel", style: "cancel" },
{ text: "Delete", style: "destructive", onPress: () => remove(id) },
]);
};
return (
<View className="flex-1 bg-white">
<FlatList
data={templates}
keyExtractor={(item) => item.id}
contentContainerClassName="p-4"
ListEmptyComponent={
<View className="py-12 items-center">
<Text className="text-slate-400 text-lg">No templates yet</Text>
<Text className="text-slate-400 text-sm mt-1">
Save a workout as a template to reuse it
</Text>
</View>
}
renderItem={({ item }) => (
<Pressable
onPress={() => navigation.navigate("Builder", { templateId: item.id })}
className="bg-slate-50 rounded-2xl p-4 mb-3 border border-slate-200"
>
<View className="flex-row justify-between items-start">
<View className="flex-1">
<Text className="font-semibold text-slate-900 text-base">{item.name}</Text>
<Text className="text-slate-500 text-sm mt-1">
{item.poolLength}m pool
</Text>
</View>
<View className="flex-row gap-2">
<Pressable
onPress={() => toggleFavorite(item.id, item.isFavorite)}
className="p-1"
>
<Text className="text-lg">{item.isFavorite ? "★" : "☆"}</Text>
</Pressable>
<Pressable
onPress={() => handleDelete(item.id, item.name)}
className="p-1"
>
<Text className="text-red-400 text-lg">×</Text>
</Pressable>
</View>
</View>
</Pressable>
)}
/>
</View>
);
}