Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
<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 || 'Séance' }}</p>
|
||||
<p class="text-xs text-zinc-500">{{ new Date(s.started_at).toLocaleDateString() }} · {{ s.set_count }} 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>
|
||||
Reference in New Issue
Block a user