portfolio_v2/app/cv/page.tsx
franck ec005b1622 Refonte du portfolio : profil GitHub, CV, SEO, export statique
- Accueil façon profil GitHub (bio, homelab, projets épinglés, parcours)
  avec fond dégradé et panneaux liquid glass
- /cv : consultable en ligne (thème sombre), imprimable en PDF propre
  (thème clair, 1 page) ; numéro de téléphone non scrapable
- Pages d'erreur 404 / 500 / global-error animées (GSAP)
- Métadonnées : Open Graph, Twitter, JSON-LD Person, sitemap, robots
  (allowlist Google + bots d'aperçu, reste bloqué)
- Images OG et favicon en PNG statiques
- Contenu du CV : Saint-Paul, langues, expériences détaillées
- Export statique (output: export) + DEPLOY.md
- Retrait de l'indicateur de dev Next.js et des fichiers du starter

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3hzMzvei419MwkKFZkvMX
2026-09-08 19:33:47 +04:00

288 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { Metadata } from "next";
import CvToolbar from "./cv-toolbar";
import RevealPhone from "./reveal-phone";
export const metadata: Metadata = {
title: "CV",
description:
"CV de Franck Thiaw-Woaye-Lapierre — développeur web & DevOps à La Réunion. Consultable en ligne ou en PDF.",
alternates: { canonical: "/cv" },
};
const experiences = [
{
role: "Développeur web & administrateur système",
org: "Exhaust Esport — La Réunion",
period: "Juin 2022 → aujourd'hui",
points: [
"Administration des serveurs web et de jeux (Minecraft, VPN), déployés en conteneurs Docker.",
"Développement et maintenance du site de l'organisation (Laravel, Tailwind CSS).",
],
},
{
role: "Stagiaire — datacenter",
org: "Le Tampon — La Réunion",
period: "Août – septembre 2020",
points: [
"Bases du DevOps et de l'administration système ; installation et gestion de serveurs.",
"Acquisition et configuration de mon premier serveur dédié.",
],
},
];
const formation = [
{
title: "Licence Informatique",
org: "Université de La Réunion — Saint-Denis",
period: "2023 → aujourd'hui",
note: "Actuellement en L3, après une L1 et une L2 validées.",
},
{
title: "Baccalauréat",
org: "Lycée Évariste de Parny — Saint-Paul",
period: "2023",
note: "",
},
];
const projets = [
{
name: "Exhaust Esport",
desc: "Site officiel de l'équipe esport.",
stack: "Laravel · Tailwind CSS",
url: "https://exhaust.re",
},
{
name: "Pro Game",
desc: "Site et suivi de réparations pour un atelier de réparation et micro-soudure.",
stack: "Laravel",
url: "https://progame.re",
},
{
name: "wifi-deauth-test",
desc: "Assistant Tkinter guidant un test de déauthentification Wi-Fi, dans un cadre autorisé (aircrack-ng).",
stack: "Python",
url: "https://forgejo.francktwl.fr/Franck/wifi-deauth-test",
},
{
name: "Rixguard",
desc: "Site d'un service VPN (projet archivé).",
stack: "Laravel",
url: "",
},
];
const competences = [
{ label: "Langages", value: "PHP, Python, JavaScript, HTML, CSS, Ruby" },
{
label: "Frameworks",
value: "Laravel, React, Next.js, Node.js, Symfony, Tailwind CSS, Sass",
},
{
label: "Infrastructure",
value: "Linux, Docker, reverse proxy, auto-hébergement, Git",
},
{
label: "Systèmes",
value: "Serveurs web et de jeux, VPN, DNS, administration système",
},
];
const contact = [
{ label: "Email", value: "francktwl@francktwl.fr", href: "mailto:francktwl@francktwl.fr" },
{ label: "Téléphone", value: "", href: "" },
{ label: "Localisation", value: "Saint-Paul, La Réunion" },
{ label: "Site", value: "francktwl.fr", href: "https://francktwl.fr" },
{
label: "Code",
value: "forgejo.francktwl.fr/Franck",
href: "https://forgejo.francktwl.fr/Franck",
},
];
function SectionTitle({ children }: { children: string }) {
return (
<h2 className="mb-1.5 border-b border-line pb-1 text-[14px] font-semibold">
{children}
</h2>
);
}
export default function CvPage() {
return (
<div className="min-h-dvh bg-canvas text-fg print:min-h-0">
<CvToolbar />
<article className="print:m-[12mm]">
<header className="border-b border-line">
<div className="mx-auto max-w-[860px] px-6 py-6 sm:px-8 print:px-0">
<h1 className="text-[26px] font-semibold tracking-tight sm:text-[32px] sm:leading-[1.12]">
Franck Thiaw-Woaye-Lapierre
</h1>
<p className="mt-1 text-base font-medium text-accent">
Développeur &amp; DevOps
</p>
</div>
</header>
<div className="mx-auto grid max-w-[860px] gap-x-10 gap-y-5 px-6 pt-5 pb-8 text-[13px] leading-[1.5] sm:grid-cols-[200px_minmax(0,1fr)] sm:px-8 print:px-0">
<aside className="space-y-4 text-[12px]">
<section className="break-inside-avoid">
<SectionTitle>Contact</SectionTitle>
<ul className="space-y-1.5">
{contact.map((c) => (
<li key={c.label}>
<span className="block text-[11px] font-semibold tracking-wide text-muted uppercase">
{c.label}
</span>
{c.label === "Téléphone" ? (
<RevealPhone />
) : c.href ? (
<a
href={c.href}
className="break-words text-accent hover:underline"
target={c.href.startsWith("http") ? "_blank" : undefined}
rel={
c.href.startsWith("http")
? "noreferrer noopener"
: undefined
}
>
{c.value}
</a>
) : (
<span>{c.value}</span>
)}
</li>
))}
</ul>
</section>
<section className="break-inside-avoid">
<SectionTitle>Compétences</SectionTitle>
<ul className="space-y-1.5">
{competences.map((c) => (
<li key={c.label}>
<span className="block text-[11px] font-semibold tracking-wide text-muted uppercase">
{c.label}
</span>
<span className="text-fg/85">{c.value}</span>
</li>
))}
</ul>
</section>
<section className="break-inside-avoid">
<SectionTitle>Langues</SectionTitle>
<ul className="space-y-1.5">
<li>
<span className="font-semibold">Français</span>
<span className="text-muted"> — natif</span>
</li>
<li>
<span className="font-semibold">Anglais</span>
<span className="text-muted"> — TOEIC</span>
</li>
</ul>
</section>
</aside>
<main className="space-y-4">
<section>
<SectionTitle>Profil</SectionTitle>
<p className="max-w-[64ch] text-fg/90">
Étudiant en L3 informatique à l&apos;Université de La Réunion,
passionné d&apos;informatique depuis le collège. Compétences en
développement web, administration de serveurs et auto-hébergement.
Objectif : travailler dans le DevOps et l&apos;infrastructure ;
l&apos;électronique est un domaine que j&apos;aimerais explorer
par la suite.
</p>
</section>
<section>
<SectionTitle>Expérience</SectionTitle>
<div className="space-y-2.5">
{experiences.map((e) => (
<div key={e.role} className="break-inside-avoid">
<div className="flex flex-wrap items-baseline justify-between gap-x-4">
<h3 className="text-[14px] font-semibold">{e.role}</h3>
<span className="text-[11px] whitespace-nowrap text-muted tabular-nums">
{e.period}
</span>
</div>
<p className="text-[12.5px] font-medium text-accent">
{e.org}
</p>
<ul className="mt-1 space-y-0.5 text-fg/85">
{e.points.map((p) => (
<li key={p} className="flex gap-2">
<span
aria-hidden="true"
className="mt-[7px] size-1 shrink-0 rounded-full bg-accent"
/>
<span>{p}</span>
</li>
))}
</ul>
</div>
))}
</div>
</section>
<section>
<SectionTitle>Formation</SectionTitle>
<div className="space-y-2.5">
{formation.map((f) => (
<div key={f.title} className="break-inside-avoid">
<div className="flex flex-wrap items-baseline justify-between gap-x-4">
<h3 className="text-[14px] font-semibold">{f.title}</h3>
<span className="text-[11px] whitespace-nowrap text-muted tabular-nums">
{f.period}
</span>
</div>
<p className="text-[12.5px] font-medium text-accent">
{f.org}
{f.note ? ` — ${f.note}` : ""}
</p>
</div>
))}
</div>
</section>
<section>
<SectionTitle>Projets</SectionTitle>
<ul className="space-y-1.5">
{projets.map((p) => (
<li key={p.name} className="break-inside-avoid">
{p.url ? (
<a
href={p.url}
target="_blank"
rel="noreferrer noopener"
className="text-[13px] font-semibold text-accent hover:underline"
>
{p.name}
</a>
) : (
<span className="text-[13px] font-semibold">{p.name}</span>
)}
<span className="text-fg/85"> — {p.desc}</span>
<span className="text-[11px] text-muted"> ({p.stack})</span>
</li>
))}
</ul>
</section>
</main>
</div>
</article>
<style>{`
@media print {
@page { size: A4; margin: 0; }
}
`}</style>
</div>
);
}