📄 Add express resume page for recruiters (Story 1.7)

Complete resume page with hero section, skills badges, projects list,
contact CTA and adventure link. Uses minimal layout, loads data from
API with graceful fallbacks, SEO optimized for recruiters.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 01:52:33 +01:00
parent 9fd66def12
commit 676d362b24
6 changed files with 313 additions and 71 deletions

View File

@@ -1,7 +1,158 @@
<template>
<div class="min-h-screen p-8">
<h1 class="text-3xl font-narrative text-sky-text">{{ $t('pages.resume.title') }}</h1>
<p class="mt-4 text-sky-text/70">{{ $t('pages.resume.description') }}</p>
<div class="max-w-3xl mx-auto px-4 py-8 md:py-12">
<!-- Section Hero -->
<section class="text-center mb-12">
<img
src="/images/avatar.svg"
alt="Célian"
width="120"
height="120"
class="rounded-full mx-auto mb-4"
>
<h1 class="text-3xl md:text-4xl font-ui font-bold text-sky-text mb-2">
Célian
</h1>
<p class="text-xl text-sky-accent font-ui mb-3">
{{ $t('resume.title') }}
</p>
<p class="text-sky-text/80 font-narrative mb-6 max-w-lg mx-auto">
{{ $t('resume.tagline') }}
</p>
<div class="flex justify-center gap-4">
<a
v-if="config.public.githubUrl"
:href="config.public.githubUrl as string"
target="_blank"
rel="noopener noreferrer"
class="text-sky-text/60 hover:text-sky-accent transition-colors"
aria-label="GitHub"
>
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</a>
<a
v-if="config.public.linkedinUrl"
:href="config.public.linkedinUrl as string"
target="_blank"
rel="noopener noreferrer"
class="text-sky-text/60 hover:text-sky-accent transition-colors"
aria-label="LinkedIn"
>
<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
</a>
</div>
</section>
<!-- Section Compétences -->
<section class="mb-12">
<h2 class="text-xl font-ui font-semibold mb-6 text-sky-accent">
{{ $t('resume.skills_title') }}
</h2>
<div v-if="skillsPending" class="text-sky-text/50 text-sm">
{{ $t('common.loading') }}
</div>
<div v-else-if="skillsByCategory.length > 0" class="space-y-4">
<div v-for="category in skillsByCategory" :key="category.name" class="flex flex-wrap items-baseline gap-2">
<span class="text-sky-text/50 text-sm font-ui w-20 shrink-0">{{ category.name }}</span>
<div class="flex flex-wrap gap-2">
<span
v-for="skill in category.skills"
:key="skill.slug"
class="px-3 py-1 text-sm font-ui bg-sky-text/5 border border-sky-text/10 rounded-full text-sky-text/90"
>
{{ skill.name }}
</span>
</div>
</div>
</div>
<!-- Fallback si API non disponible -->
<div v-else class="space-y-4">
<div class="flex flex-wrap items-baseline gap-2">
<span class="text-sky-text/50 text-sm font-ui w-20 shrink-0">Frontend</span>
<div class="flex flex-wrap gap-2">
<span v-for="s in ['Vue.js', 'Nuxt', 'TypeScript', 'TailwindCSS']" :key="s" class="px-3 py-1 text-sm font-ui bg-sky-text/5 border border-sky-text/10 rounded-full text-sky-text/90">{{ s }}</span>
</div>
</div>
<div class="flex flex-wrap items-baseline gap-2">
<span class="text-sky-text/50 text-sm font-ui w-20 shrink-0">Backend</span>
<div class="flex flex-wrap gap-2">
<span v-for="s in ['Laravel', 'PHP', 'Node.js']" :key="s" class="px-3 py-1 text-sm font-ui bg-sky-text/5 border border-sky-text/10 rounded-full text-sky-text/90">{{ s }}</span>
</div>
</div>
<div class="flex flex-wrap items-baseline gap-2">
<span class="text-sky-text/50 text-sm font-ui w-20 shrink-0">Tools</span>
<div class="flex flex-wrap gap-2">
<span v-for="s in ['Git', 'Docker']" :key="s" class="px-3 py-1 text-sm font-ui bg-sky-text/5 border border-sky-text/10 rounded-full text-sky-text/90">{{ s }}</span>
</div>
</div>
</div>
</section>
<!-- Section Projets -->
<section class="mb-12">
<h2 class="text-xl font-ui font-semibold mb-6 text-sky-accent">
{{ $t('resume.projects_title') }}
</h2>
<div v-if="projectsPending" class="text-sky-text/50 text-sm">
{{ $t('common.loading') }}
</div>
<ul v-else-if="featuredProjects.length > 0" class="space-y-4">
<li v-for="project in featuredProjects" :key="project.slug" class="flex items-start gap-3">
<span class="text-sky-accent mt-1 shrink-0">&#x2022;</span>
<div>
<NuxtLink
:to="localePath(`/projets/${project.slug}`)"
class="font-ui font-semibold text-sky-text hover:text-sky-accent transition-colors"
>
{{ project.title }}
</NuxtLink>
<p v-if="project.short_description" class="text-sky-text/60 text-sm font-narrative mt-0.5">
{{ project.short_description }}
</p>
</div>
</li>
</ul>
<!-- Fallback si API non disponible -->
<p v-else class="text-sky-text/50 text-sm font-narrative">
{{ $t('resume.projects_loading_hint') }}
</p>
</section>
<!-- Section Contact -->
<section class="text-center mb-8">
<NuxtLink
:to="localePath('/contact')"
class="inline-block px-8 py-4 bg-sky-accent text-sky-dark font-ui font-bold text-lg rounded-lg hover:opacity-90 transition-opacity"
>
{{ $t('resume.cta_contact') }}
</NuxtLink>
<p class="mt-4 text-sky-text/60 font-ui">
<a href="mailto:contact@skycel.fr" class="hover:text-sky-accent transition-colors">
contact@skycel.fr
</a>
</p>
</section>
<!-- Lien vers l'aventure -->
<div class="text-center border-t border-sky-text/10 pt-6">
<NuxtLink
:to="localePath('/')"
class="text-sky-text/50 hover:text-sky-accent text-sm font-narrative transition-colors"
>
{{ $t('resume.adventure_link') }} &rarr;
</NuxtLink>
</div>
</div>
</template>
@@ -10,11 +161,54 @@ definePageMeta({
layout: 'minimal',
})
const { setPageMeta } = useSeo()
const { t } = useI18n()
const { locale } = useI18n()
const localePath = useLocalePath()
const config = useRuntimeConfig()
const { setPageMeta } = useSeo()
setPageMeta({
title: t('pages.resume.title'),
description: t('pages.resume.description'),
title: t('resume.meta_title'),
description: t('resume.meta_description'),
})
// Chargement des skills depuis l'API
const { data: skillsData, pending: skillsPending } = await useFetch<{ data: Record<string, Array<{ slug: string; name: string; icon: string; category: string }>> }>('/skills', {
baseURL: config.public.apiUrl as string,
headers: {
'X-API-Key': config.public.apiKey as string,
'Accept-Language': locale.value,
},
default: () => ({ data: {} }),
})
// Chargement des projets depuis l'API
const { data: projectsData, pending: projectsPending } = await useFetch<{ data: Array<{ slug: string; title: string; short_description: string; is_featured: boolean }> }>('/projects', {
baseURL: config.public.apiUrl as string,
headers: {
'X-API-Key': config.public.apiKey as string,
'Accept-Language': locale.value,
},
default: () => ({ data: [] }),
})
// Grouper les skills par catégorie (max 4 par catégorie)
const skillsByCategory = computed(() => {
const data = skillsData.value?.data
if (!data || typeof data !== 'object') return []
return Object.entries(data)
.map(([name, skills]) => ({
name,
skills: (skills as Array<{ slug: string; name: string }>).slice(0, 4),
}))
.filter(c => c.skills.length > 0)
})
// Projets featured (max 4)
const featuredProjects = computed(() => {
const data = projectsData.value?.data
if (!Array.isArray(data)) return []
return data.slice(0, 4)
})
</script>

View File

@@ -63,6 +63,17 @@
"continue": "Continue",
"restart": "Start over"
},
"resume": {
"title": "Full-Stack Developer",
"tagline": "Passionate about innovative and immersive web experiences",
"skills_title": "Tech Stack",
"projects_title": "Recent Projects",
"projects_loading_hint": "Projects coming soon...",
"cta_contact": "Contact Me",
"adventure_link": "Want to explore? Discover the full adventure",
"meta_title": "C\u00e9lian - Full-Stack Developer | Quick Resume",
"meta_description": "Full-Stack Developer specialized in Vue.js, Nuxt, Laravel. Discover my profile and projects in 30 seconds."
},
"pages": {
"projects": {
"title": "Projects",

View File

@@ -63,6 +63,17 @@
"continue": "Reprendre",
"restart": "Recommencer"
},
"resume": {
"title": "D\u00e9veloppeur Full-Stack",
"tagline": "Passionn\u00e9 par les exp\u00e9riences web innovantes et immersives",
"skills_title": "Stack technique",
"projects_title": "Projets r\u00e9cents",
"projects_loading_hint": "Projets disponibles bient\u00f4t...",
"cta_contact": "Me contacter",
"adventure_link": "Envie d'explorer ? D\u00e9couvrir l'aventure compl\u00e8te",
"meta_title": "C\u00e9lian - D\u00e9veloppeur Full-Stack | CV Express",
"meta_description": "D\u00e9veloppeur Full-Stack sp\u00e9cialis\u00e9 en Vue.js, Nuxt, Laravel. D\u00e9couvrez mon profil et mes projets en 30 secondes."
},
"pages": {
"projects": {
"title": "Projets",

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
<rect width="120" height="120" rx="60" fill="#0a0e1a"/>
<circle cx="60" cy="45" r="22" fill="#fa784f" opacity="0.9"/>
<path d="M60 72 C35 72 20 92 20 110 L100 110 C100 92 85 72 60 72Z" fill="#fa784f" opacity="0.7"/>
<text x="60" y="52" text-anchor="middle" fill="#0a0e1a" font-family="sans-serif" font-size="24" font-weight="bold">C</text>
</svg>

After

Width:  |  Height:  |  Size: 443 B