Files
neckfireandClaude Opus 4.8 bd739d13cf
Build & Deploy / build (push) Successful in 8s
i18n complet des pages + routine builder avancé (config par exo, réordonner, dupliquer)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 04:48:26 +02:00

129 lines
5.4 KiB
Vue

<script setup lang="ts">
definePageMeta({ middleware: 'auth' })
const { t } = useI18n()
const { api } = useApi()
interface Dashboard {
streak: { current: number, longest: number, freezes_available: number, last_active_date: string | null, flame_level: number, active_today: boolean }
week: { goal_sessions: number, done_sessions: number, days: { date: string, done: boolean, source: string | null }[] }
recent_sessions: { id: number, title: string | null, started_at: string, total_volume: number | null, set_count: number }[]
achievements: { key: string, earned_at: string, label: string }[]
suggestion: { type: 'routine' | 'rest', routine_id?: number, label: string } | null
}
const { data, pending, error, refresh } = await useAsyncData('dashboard', () => api<Dashboard>('/api/dashboard'), { lazy: true })
const flameScale = computed(() => 1 + Math.min(data.value?.streak.flame_level ?? 0, 5) * 0.12)
</script>
<template>
<div class="space-y-6">
<SfLogo v-if="pending && !data" :size="72" loader class="mx-auto mt-16" />
<UAlert
v-else-if="error"
color="error"
variant="soft"
:title="t('common.error')"
:actions="[{ label: t('common.retry'), onClick: () => refresh() }]"
/>
<template v-else-if="data">
<!-- Héros flamme -->
<section class="relative flex flex-col items-center pt-6 pb-2 text-center">
<div class="relative">
<UIcon
name="i-lucide-flame"
class="text-flame-500 drop-shadow-[0_0_25px_rgba(249,115,22,0.55)] transition-transform"
:style="{ fontSize: `${7 * flameScale}rem`, filter: data.streak.active_today ? 'none' : 'grayscale(0.4) opacity(0.75)' }"
/>
<div class="absolute inset-0 flex items-center justify-center">
<span class="text-4xl font-black text-white drop-shadow">{{ data.streak.current }}</span>
</div>
</div>
<p class="mt-2 text-lg font-bold">
{{ data.streak.current }} {{ t('dashboard.streakDays') }}
</p>
<p class="text-sm" :class="data.streak.active_today ? 'text-flame-600 dark:text-flame-400' : 'text-zinc-500'">
{{ data.streak.active_today ? t('dashboard.activeToday') : t('dashboard.keepFlame') }}
</p>
</section>
<!-- Stats -->
<section class="grid grid-cols-3 gap-3">
<UCard :ui="{ body: 'p-3 text-center' }">
<p class="text-2xl font-black sf-flame-text">{{ data.streak.longest }}</p>
<p class="text-xs text-zinc-500">{{ t('dashboard.longest') }}</p>
</UCard>
<UCard :ui="{ body: 'p-3 text-center' }">
<p class="text-2xl font-black text-sky-500">{{ data.streak.freezes_available }}</p>
<p class="text-xs text-zinc-500">{{ t('dashboard.freezes') }}</p>
</UCard>
<UCard :ui="{ body: 'p-3 text-center' }">
<p class="text-2xl font-black">{{ data.week.done_sessions }}/{{ data.week.goal_sessions }}</p>
<p class="text-xs text-zinc-500">{{ t('dashboard.weekGoal') }}</p>
</UCard>
</section>
<!-- Semaine (7 jours) -->
<section class="flex justify-between gap-1.5">
<div v-for="d in data.week.days" :key="d.date" class="flex-1 flex flex-col items-center gap-1">
<div
class="w-full aspect-square rounded-lg flex items-center justify-center text-xs"
:class="d.done ? 'sf-flame-bg text-white' : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-400'"
>
<UIcon v-if="d.done" name="i-lucide-flame" class="size-4" />
</div>
<span class="text-[10px] text-zinc-400">{{ new Date(d.date).toLocaleDateString(undefined, { weekday: 'narrow' }) }}</span>
</div>
</section>
<!-- CTA -->
<UButton
block
size="xl"
color="primary"
icon="i-lucide-play"
class="font-bold"
:label="data.suggestion?.type === 'rest' ? t('dashboard.restEarned') : t('dashboard.startWorkout')"
to="/session/new"
/>
<!-- Badges -->
<section v-if="data.achievements.length">
<h2 class="text-sm font-semibold text-zinc-500 mb-2">{{ t('dashboard.achievements') }}</h2>
<div class="flex gap-2 overflow-x-auto pb-1">
<UBadge
v-for="a in data.achievements"
:key="a.key"
color="primary"
variant="soft"
size="lg"
class="shrink-0"
>
🏅 {{ a.label }}
</UBadge>
</div>
</section>
<!-- Séances récentes -->
<section>
<h2 class="text-sm font-semibold text-zinc-500 mb-2">{{ t('dashboard.recentSessions') }}</h2>
<p v-if="!data.recent_sessions.length" class="text-sm text-zinc-400">{{ t('dashboard.noSessions') }}</p>
<div v-else class="space-y-2">
<UCard v-for="s in data.recent_sessions" :key="s.id" :ui="{ body: 'p-3' }">
<div class="flex items-center justify-between">
<div>
<p class="font-medium">{{ s.title || t('dashboard.session') }}</p>
<p class="text-xs text-zinc-500">{{ new Date(s.started_at).toLocaleDateString() }} · {{ s.set_count }} {{ t('dashboard.sets') }}</p>
</div>
<span v-if="s.total_volume" class="text-sm font-bold sf-flame-text">{{ Math.round(s.total_volume) }} kg</span>
</div>
</UCard>
</div>
</section>
</template>
</div>
</template>