StreakFit — PWA Nuxt (thème flamme, logo SF animé)
Build & Deploy / build (push) Failing after 5s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-19 03:00:18 +02:00
co-authored by Claude Opus 4.8
commit 8ee9596529
41 changed files with 24630 additions and 0 deletions
+89
View File
@@ -0,0 +1,89 @@
<script setup lang="ts">
const { t } = useI18n()
const { api } = useApi()
interface ExerciseRow { id: number, name: string, slug: string, body_part: string | null, equipment: string | null, target: string | null, image_url: string | null }
interface Facets { body_parts: string[], equipments: string[], targets: string[], categories: string[] }
interface Paginated { data: ExerciseRow[], meta: { current_page: number, last_page: number, total: number } }
const search = ref('')
const bodyPart = ref<string | undefined>()
const page = ref(1)
const { data: facets } = await useAsyncData('ex-facets', () => api<Facets>('/api/exercises/facets'), { lazy: true })
const query = computed(() => ({ search: search.value || undefined, body_part: bodyPart.value || undefined, page: page.value }))
const { data, pending } = await useAsyncData<Paginated>(
'exercises',
() => api<Paginated>('/api/exercises', { query: query.value }),
{ watch: [query], lazy: true }
)
watch([search, bodyPart], () => { page.value = 1 })
const bodyPartOptions = computed(() => [
{ label: t('exercises.all'), value: undefined },
...(facets.value?.body_parts ?? []).map(b => ({ label: b, value: b }))
])
</script>
<template>
<div class="space-y-4">
<h1 class="text-2xl font-black tracking-tight">{{ t('exercises.title') }}</h1>
<div class="flex gap-2">
<UInput
v-model="search"
icon="i-lucide-search"
:placeholder="t('exercises.search')"
size="lg"
class="flex-1"
/>
<USelectMenu
v-model="bodyPart"
:items="bodyPartOptions"
value-key="value"
:placeholder="t('exercises.bodyPart')"
size="lg"
class="w-36"
/>
</div>
<div v-if="pending && !data" class="flex justify-center py-16">
<SfLogo :size="56" loader />
</div>
<p v-else-if="!data?.data.length" class="text-center text-zinc-400 py-16">{{ t('exercises.noResults') }}</p>
<div v-else class="grid grid-cols-2 sm:grid-cols-3 gap-3">
<NuxtLink
v-for="ex in data.data"
:key="ex.id"
:to="`/exercises/${ex.id}`"
class="group rounded-xl overflow-hidden border border-zinc-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 hover:border-flame-400 transition-colors"
>
<div class="aspect-square bg-zinc-100 dark:bg-zinc-800 overflow-hidden">
<img
v-if="ex.image_url"
:src="ex.image_url"
:alt="ex.name"
loading="lazy"
class="w-full h-full object-cover group-hover:scale-105 transition-transform"
>
</div>
<div class="p-2">
<p class="text-sm font-semibold capitalize truncate">{{ ex.name }}</p>
<p class="text-xs text-zinc-500 capitalize truncate">{{ ex.target }}</p>
</div>
</NuxtLink>
</div>
<div v-if="data && data.meta.last_page > 1" class="flex justify-center pt-2">
<UPagination
v-model:page="page"
:total="data.meta.total"
:items-per-page="Math.ceil(data.meta.total / data.meta.last_page)"
/>
</div>
</div>
</template>