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>
215 lines
8.6 KiB
Vue
215 lines
8.6 KiB
Vue
<template>
|
|
<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>
|
|
|
|
<script setup lang="ts">
|
|
definePageMeta({
|
|
layout: 'minimal',
|
|
})
|
|
|
|
const { t } = useI18n()
|
|
const { locale } = useI18n()
|
|
const localePath = useLocalePath()
|
|
const config = useRuntimeConfig()
|
|
const { setPageMeta } = useSeo()
|
|
|
|
setPageMeta({
|
|
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>
|