- Create useFetchProjects composable for API integration - Implement responsive grid layout (1/2/3/4 columns) - Add stagger fadeInUp animation with prefers-reduced-motion support - Include loading skeleton, error state with retry, and empty state - Configure SEO meta tags via useSeo composable - Update Project model ordered scope for proper sorting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
98 lines
2.4 KiB
Vue
98 lines
2.4 KiB
Vue
<template>
|
|
<div class="max-w-7xl mx-auto px-4 py-8 md:py-12">
|
|
<h1 class="text-3xl md:text-4xl font-ui font-bold text-sky-text mb-8">
|
|
{{ $t('projects.title') }}
|
|
</h1>
|
|
|
|
<!-- Loading state -->
|
|
<div v-if="pending" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
|
<div v-for="i in 6" :key="i" class="animate-pulse">
|
|
<div class="bg-sky-text/5 rounded-xl h-44 md:h-48" />
|
|
<div class="p-4">
|
|
<div class="bg-sky-text/5 h-6 rounded w-3/4 mb-2" />
|
|
<div class="bg-sky-text/5 h-4 rounded w-full" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error state -->
|
|
<div v-else-if="error" class="text-center py-16">
|
|
<p class="text-sky-text/60 font-narrative mb-6">
|
|
{{ $t('projects.load_error') }}
|
|
</p>
|
|
<button
|
|
class="px-6 py-3 bg-sky-accent text-sky-dark font-ui font-semibold rounded-lg hover:opacity-90 transition-opacity"
|
|
@click="refresh()"
|
|
>
|
|
{{ $t('common.retry') }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Empty state -->
|
|
<div v-else-if="!projects || projects.length === 0" class="text-center py-16">
|
|
<p class="text-sky-text/60 font-narrative">
|
|
{{ $t('projects.no_projects') }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Projects grid -->
|
|
<div
|
|
v-else
|
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
|
>
|
|
<FeatureProjectCard
|
|
v-for="(project, index) in projects"
|
|
:key="project.id"
|
|
:project="project"
|
|
class="project-card-animated"
|
|
:style="{ '--animation-delay': `${index * 80}ms` }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useProgressionStore } from '~/stores/progression'
|
|
|
|
const { t } = useI18n()
|
|
const { setPageMeta } = useSeo()
|
|
const store = useProgressionStore()
|
|
|
|
const { data: projects, pending, error, refresh } = await useFetchProjects()
|
|
|
|
setPageMeta({
|
|
title: t('projects.page_title'),
|
|
description: t('projects.page_description'),
|
|
})
|
|
|
|
onMounted(() => {
|
|
store.visitSection('projets')
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.project-card-animated {
|
|
animation: fadeInUp 0.5s ease-out forwards;
|
|
animation-delay: var(--animation-delay, 0ms);
|
|
opacity: 0;
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.project-card-animated {
|
|
animation: none;
|
|
opacity: 1;
|
|
}
|
|
}
|
|
</style>
|