📄 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:
@@ -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">•</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') }} →
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user