portfolio_v2/app/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

405 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 Link from "next/link";
const badges = [
{ alt: "Linux", src: "https://img.shields.io/badge/Linux-FCC624?style=for-the-badge&logo=linux&logoColor=black" },
{ alt: "Docker", src: "https://img.shields.io/badge/Docker-2496ED?style=for-the-badge&logo=docker&logoColor=white" },
{ alt: "Python", src: "https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white" },
{ alt: "PHP", src: "https://img.shields.io/badge/PHP-777BB4?style=for-the-badge&logo=php&logoColor=white" },
{ alt: "Laravel", src: "https://img.shields.io/badge/Laravel-FF2D20?style=for-the-badge&logo=laravel&logoColor=white" },
{ alt: "JavaScript", src: "https://img.shields.io/badge/JavaScript-F7DF1E?style=for-the-badge&logo=javascript&logoColor=black" },
{ alt: "React", src: "https://img.shields.io/badge/React-20232A?style=for-the-badge&logo=react&logoColor=61DAFB" },
{ alt: "Tailwind CSS", src: "https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white" },
{ alt: "Git", src: "https://img.shields.io/badge/Git-F05032?style=for-the-badge&logo=git&logoColor=white" },
];
type Repo = {
name: string;
desc: string;
lang: string;
langColor: string;
topics: string[];
url?: string;
archived?: boolean;
};
const repos: Repo[] = [
{
name: "wifi-deauth-test",
desc: "Assistant Tkinter qui guide un test de déauthentification Wi-Fi sur un client, dans un cadre autorisé, en s'appuyant sur aircrack-ng.",
lang: "Python",
langColor: "#3572A5",
topics: ["python", "tkinter", "aircrack-ng", "wifi-security"],
url: "https://forgejo.francktwl.fr/Franck/wifi-deauth-test",
},
{
name: "exhaust-esport",
desc: "Site officiel de l'équipe esport Exhaust.",
lang: "PHP",
langColor: "#4F5D95",
topics: ["laravel", "php", "tailwindcss", "esport"],
url: "https://exhaust.re",
},
{
name: "progame",
desc: "Site et suivi de réparations pour Pro Game, atelier de réparation et micro-soudure à La Réunion.",
lang: "PHP",
langColor: "#4F5D95",
topics: ["laravel", "php", "suivi-réparation"],
url: "https://progame.re",
},
{
name: "rixguard",
desc: "Site vitrine et espace client d'un service VPN. Le service a fermé, le site n'existe plus aujourd'hui.",
lang: "PHP",
langColor: "#4F5D95",
topics: ["laravel", "php", "vpn"],
archived: true,
},
];
const parcours = [
{
year: "2023",
title: "Licence Informatique — Université de La Réunion",
body: "Actuellement en L3, après une L1 et une L2 validées.",
},
{
year: "juin 2022",
title: "Développeur web & administrateur système — Exhaust Esport",
body: "Serveurs web et de jeux (Minecraft, VPN), site de l'organisation en Laravel, services conteneurisés avec Docker.",
},
{
year: "2022–2023",
title: "Magicien — centre de loisirs, Saint-Paul",
body: "Animation de spectacles pour enfants pendant les vacances scolaires.",
},
{
year: "2022",
title: "Baccalauréat — Lycée Évariste de Parny",
body: "Plateau Caillou, La Réunion.",
},
{
year: "2020",
title: "Stage — datacenter, Le Tampon",
body: "Bases du DevOps et de l'administration système. Premier serveur dédié acquis à cette occasion.",
},
];
const homelab: { icon: string; name: string; note: string; url?: string }[] = [
{
icon: "🗃️",
name: "Forgejo",
note: "",
url: "https://forgejo.francktwl.fr/Franck",
},
{ icon: "☁️", name: "Nextcloud", note: "" },
{ icon: "🛡️", name: "AdGuard Home", note: "" },
{ icon: "🔒", name: "VPN", note: "" },
{ icon: "🌐", name: "Mes sites web", note: "" },
];
const personLd = {
"@context": "https://schema.org",
"@type": "Person",
name: "Franck Thiaw-Woaye-Lapierre",
alternateName: "francktwl",
url: "https://francktwl.fr",
image: "https://francktwl.fr/pp.jpg",
jobTitle: "Développeur web & DevOps",
description:
"Étudiant en L3 informatique à La Réunion, orienté DevOps, administration de serveurs Linux et auto-hébergement.",
address: {
"@type": "PostalAddress",
addressLocality: "Saint-Paul",
addressRegion: "La Réunion",
addressCountry: "RE",
},
knowsLanguage: ["fr", "en"],
alumniOf: {
"@type": "CollegeOrUniversity",
name: "Université de La Réunion",
},
knowsAbout: [
"DevOps",
"Linux",
"Docker",
"Laravel",
"Auto-hébergement",
"Administration système",
"Réseau",
],
sameAs: [
"https://forgejo.francktwl.fr/Franck",
"https://www.instagram.com/francktwl_photo/",
"https://discordapp.com/users/320512642785607681",
],
};
export default function Home() {
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(personLd) }}
/>
<header className="sticky top-0 z-20 border-b border-white/10 bg-[#0a0e17]/72 backdrop-blur-xl">
<div className="mx-auto flex max-w-[1280px] items-center gap-3 px-4 py-3 md:px-6">
<span className="glass grid size-8 place-items-center rounded-full font-bold">
F
</span>
<span className="font-semibold">Franck Thiaw-Woaye-Lapierre</span>
<nav className="ml-auto flex items-center gap-4">
<a
href="https://forgejo.francktwl.fr/Franck"
target="_blank"
rel="noreferrer noopener"
className="text-muted hover:text-accent"
>
git<span className="sr-only"> (nouvel onglet)</span>
</a>
<a
href="mailto:francktwl@francktwl.fr"
className="text-muted hover:text-accent"
>
email
</a>
<Link
href="/cv"
className="rounded-md border border-line bg-surface px-3 py-1.5 text-sm font-semibold hover:border-muted"
>
CV
</Link>
</nav>
</div>
</header>
<div className="mx-auto grid max-w-[1100px] gap-6 px-4 pt-6 pb-16 md:grid-cols-[296px_minmax(0,1fr)] md:px-6">
<aside data-reveal className="flex flex-col">
<div className="flex items-start gap-4 md:block">
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/pp.jpg"
alt="Franck Thiaw-Woaye-Lapierre"
className="size-16 shrink-0 rounded-full border border-line object-cover md:h-auto md:w-full md:aspect-square"
/>
<div className="md:mt-4">
<h1 className="text-xl font-semibold leading-tight md:text-2xl">
Franck Thiaw-Woaye-Lapierre
</h1>
<p className="text-base font-light text-muted md:text-xl">
@francktwl
</p>
</div>
</div>
<p className="mt-3">
<span className="glass inline-flex items-center gap-1.5 rounded-full px-3 py-1 text-xs">
<span aria-hidden="true">💻</span> Développeur &amp; DevOps · La Réunion
</span>
</p>
<p className="mt-4">
Étudiant en L3 informatique. J&apos;auto-héberge mon infra depuis 2020
: serveurs Linux, services en Docker, mon propre forge git. J&apos;apprends
surtout en cassant des choses pour comprendre comment elles tiennent.
</p>
<ul className="mt-4 space-y-2 text-muted">
<li>
<span aria-hidden="true">📍</span> Saint-Paul, La Réunion
</li>
<li>
<span aria-hidden="true">🎓</span> Licence Informatique · Université
de La Réunion
</li>
<li>
<span aria-hidden="true">🗣️</span> Français (natif) · Anglais
(TOEIC)
</li>
<li>
<span aria-hidden="true">🔗</span>{" "}
<a
href="https://forgejo.francktwl.fr/Franck"
target="_blank"
rel="noreferrer noopener"
className="text-accent hover:underline"
>
forgejo.francktwl.fr<span className="sr-only"> (nouvel onglet)</span>
</a>
</li>
<li>
<span aria-hidden="true">✉️</span>{" "}
<a
href="mailto:francktwl@francktwl.fr"
className="text-accent hover:underline"
>
francktwl@francktwl.fr
</a>
</li>
<li>
<span aria-hidden="true">📸</span>{" "}
<a
href="https://www.instagram.com/francktwl_photo/"
target="_blank"
rel="noreferrer noopener"
className="text-accent hover:underline"
>
@francktwl_photo<span className="sr-only"> (nouvel onglet)</span>
</a>
</li>
<li>
<span aria-hidden="true">💬</span>{" "}
<a
href="https://discordapp.com/users/320512642785607681"
target="_blank"
rel="noreferrer noopener"
className="text-accent hover:underline"
>
Discord<span className="sr-only"> (nouvel onglet)</span>
</a>
</li>
</ul>
</aside>
<main className="min-w-0">
<section data-reveal className="glass overflow-hidden rounded-2xl">
<div className="flex items-center gap-2 border-b border-white/10 px-4 py-3 text-muted">
<span aria-hidden="true">📖</span>
<span className="text-xs">francktwl / README.md</span>
</div>
<div className="space-y-4 p-4 md:p-6">
<h2 className="text-xl font-semibold">Salut, moi c&apos;est Franck 👋</h2>
<p className="max-w-[65ch]">
Passionné d&apos;informatique depuis le collège — les jeux vidéo
d&apos;abord, puis l&apos;assemblage de mon premier PC. Je suis
aujourd&apos;hui en 3ᵉ année de licence informatique à La Réunion.
</p>
<p className="max-w-[65ch]">
Ce qui me plaît vraiment, c&apos;est gérer des machines :
administrer des serveurs Linux, les configurer, faire tourner mes
services dans Docker. Mon objectif est de travailler dans le
DevOps ; l&apos;électronique est un domaine que j&apos;aimerais
explorer plus tard.
</p>
<div className="flex flex-wrap gap-1.5 pt-1">
{badges.map((b) => (
// eslint-disable-next-line @next/next/no-img-element
<img key={b.alt} src={b.src} alt={b.alt} className="h-7" />
))}
</div>
</div>
</section>
<section data-reveal className="mt-6">
<h2 className="mb-3 text-base font-semibold">Homelab</h2>
<div className="glass rounded-2xl p-4">
<p className="text-sm text-muted">
J&apos;héberge mes propres services derrière un reverse proxy, sur
un serveur dédié en ligne depuis 2020 — tout en conteneurs.
</p>
<ul className="mt-3 grid gap-x-4 gap-y-2 sm:grid-cols-2">
{homelab.map((h) => (
<li key={h.name} className="flex items-baseline gap-2 text-sm">
<span aria-hidden="true">{h.icon}</span>
<span>
{h.url ? (
<a
href={h.url}
target="_blank"
rel="noreferrer noopener"
className="font-semibold text-accent hover:underline"
>
{h.name}
<span className="sr-only"> (nouvel onglet)</span>
</a>
) : (
<span className="font-semibold">{h.name}</span>
)}
</span>
</li>
))}
</ul>
</div>
</section>
<section id="projets" data-reveal className="mt-6 scroll-mt-6">
<h2 className="mb-3 text-base font-semibold">Épinglé</h2>
<ul className="grid gap-4 sm:grid-cols-2">
{repos.map((r) => (
<li
key={r.name}
className="glass flex h-full flex-col rounded-2xl p-4"
>
<div className="flex items-center gap-2">
<span className="text-muted" aria-hidden="true">
📁
</span>
{r.url ? (
<a
href={r.url}
target="_blank"
rel="noreferrer noopener"
className="font-semibold text-accent hover:underline"
>
{r.name}
<span className="sr-only"> (nouvel onglet)</span>
</a>
) : (
<span className="font-semibold">{r.name}</span>
)}
<span
className={`ml-auto rounded-full border px-2 py-0.5 text-xs ${
r.archived
? "border-[#d29922]/40 text-[#d29922]"
: "border-line text-muted"
}`}
>
{r.archived ? "Archivé" : "Public"}
</span>
</div>
<p className="mt-2 text-xs text-muted">{r.desc}</p>
<ul className="mt-3 flex flex-wrap gap-1.5">
{r.topics.map((t) => (
<li
key={t}
className="rounded-full bg-accent/10 px-2 py-0.5 text-xs font-medium text-accent"
>
{t}
</li>
))}
</ul>
<p className="mt-auto flex items-center gap-1.5 pt-3 text-xs text-muted">
<span
className="inline-block size-3 rounded-full"
style={{ backgroundColor: r.langColor }}
aria-hidden="true"
/>
{r.lang}
</p>
</li>
))}
</ul>
</section>
<section data-reveal className="mt-6">
<h2 className="mb-3 text-base font-semibold">Parcours</h2>
<ol className="border-l border-line">
{parcours.map((p) => (
<li key={p.year} className="relative pb-6 pl-6 last:pb-0">
<span
className="absolute top-1.5 -left-[5px] size-2.5 rounded-full border-2 border-canvas bg-muted"
aria-hidden="true"
/>
<p className="text-xs text-muted">{p.year}</p>
<p className="font-semibold">{p.title}</p>
<p className="mt-1 max-w-[65ch] text-muted">{p.body}</p>
</li>
))}
</ol>
</section>
</main>
</div>
</>
);
}