Build & Deploy / build (push) Successful in 8s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
129 lines
5.4 KiB
Vue
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>
|