✨ Add projects gallery page with responsive grid (Story 2.2)
- 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>
This commit is contained in:
21
frontend/app/composables/useFetchProjects.ts
Normal file
21
frontend/app/composables/useFetchProjects.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Project } from '~/types/project'
|
||||
|
||||
interface ProjectsResponse {
|
||||
data: Project[]
|
||||
meta: { lang: string }
|
||||
}
|
||||
|
||||
export function useFetchProjects() {
|
||||
const config = useRuntimeConfig()
|
||||
const { locale } = useI18n()
|
||||
|
||||
return useFetch<ProjectsResponse>('/projects', {
|
||||
baseURL: config.public.apiUrl as string,
|
||||
headers: {
|
||||
'X-API-Key': config.public.apiKey as string,
|
||||
'Accept-Language': locale.value,
|
||||
},
|
||||
transform: (response) => response.data,
|
||||
default: () => [],
|
||||
})
|
||||
}
|
||||
@@ -1,16 +1,97 @@
|
||||
<template>
|
||||
<div class="min-h-screen p-8">
|
||||
<h1 class="text-3xl font-narrative text-sky-text">{{ $t('pages.projects.title') }}</h1>
|
||||
<p class="mt-4 text-sky-text/70">{{ $t('pages.projects.description') }}</p>
|
||||
<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">
|
||||
const { setPageMeta } = useSeo()
|
||||
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('pages.projects.title'),
|
||||
description: t('pages.projects.description'),
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user