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
This commit is contained in:
franck 2026-09-08 19:33:47 +04:00
parent 3146609f96
commit ec005b1622
27 changed files with 1408 additions and 101 deletions

3
.gitignore vendored
View file

@ -39,3 +39,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
# IDE
/.idea/

93
DEPLOY.md Normal file
View file

@ -0,0 +1,93 @@
# Déploiement — francktwl.fr
Le site est un **export statique** Next.js : que du HTML/CSS/JS/images.
**Aucun Node, aucun process à faire tourner sur le serveur.**
## 1. Construire
```bash
npm ci
npm run build
```
→ génère le dossier **`out/`** (~1,4 Mo). C'est **tout ce qu'il faut transférer**.
## 2. Transférer sur le serveur
```bash
rsync -avz --delete out/ user@serveur:/srv/francktwl.fr/
```
(ou `scp -r out/* user@serveur:/srv/francktwl.fr/`)
Contenu de `out/` :
```
index.html → page d'accueil
cv/index.html → /cv
404.html → page d'erreur
robots.txt
sitemap.xml
opengraph-image.png → carte de partage réseaux sociaux
twitter-image.png
icon.png → favicon
pp.jpg → photo
_next/ → JS/CSS (fichiers hashés, cache 1 an)
```
## 3. Servir le dossier
### Caddy
```caddy
francktwl.fr {
root * /srv/francktwl.fr
encode zstd gzip
try_files {path} {path}/index.html {path}.html
file_server
handle_errors {
rewrite * /404.html
file_server
}
@assets path /_next/static/*
header @assets Cache-Control "public, max-age=31536000, immutable"
}
```
### Nginx
```nginx
server {
listen 80;
server_name francktwl.fr www.francktwl.fr;
root /srv/francktwl.fr;
location / {
try_files $uri $uri.html $uri/index.html $uri/ =404;
}
error_page 404 /404.html;
location /_next/static/ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
}
```
TLS : certbot, ou via ton reverse proxy existant (celui qui sert déjà Forgejo /
Nextcloud) — ajoute simplement `francktwl.fr` comme vhost pointant sur
`/srv/francktwl.fr`.
## 4. À chaque mise à jour
```bash
npm run build && rsync -avz --delete out/ user@serveur:/srv/francktwl.fr/
```
## Après le premier déploiement
- Google Search Console : ajouter `francktwl.fr`, prouver la propriété
(coller le token dans `metadata.verification.google` de `app/layout.tsx`,
rebuild, redéployer), soumettre `https://francktwl.fr/sitemap.xml`.
- L'image de partage (`opengraph-image.png` / `twitter-image.png`) est
pré-générée. Pour la changer : éditer le PNG ou regénérer.

25
app/cv/cv-toolbar.tsx Normal file
View file

@ -0,0 +1,25 @@
"use client";
import Link from "next/link";
export default function CvToolbar() {
return (
<div className="no-print sticky top-0 z-10 border-b border-line bg-canvas/85 backdrop-blur">
<div className="mx-auto flex max-w-[860px] items-center justify-between gap-3 px-6 py-2.5">
<Link
href="/"
className="text-sm font-medium text-muted hover:text-accent"
>
← Portfolio
</Link>
<button
type="button"
onClick={() => window.print()}
className="rounded-md border border-line bg-surface px-3.5 py-1.5 text-sm font-semibold hover:border-muted"
>
Télécharger en PDF
</button>
</div>
</div>
);
}

287
app/cv/page.tsx Normal file
View file

@ -0,0 +1,287 @@
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>
);
}

51
app/cv/reveal-phone.tsx Normal file
View file

@ -0,0 +1,51 @@
"use client";
import { useState, useSyncExternalStore } from "react";
// Numéro inversé : jamais présent tel quel dans le HTML rendu côté serveur,
// et pas de correspondance grep directe. Il n'est reconstruit et injecté dans
// le DOM qu'après hydratation (client), pour l'impression du CV et le clic
// « Afficher ». Les harvesters qui n'exécutent pas de JS ne récupèrent rien.
const REV = "78 45 06 396 262+";
const subscribe = () => () => {};
export default function RevealPhone() {
const isClient = useSyncExternalStore(
subscribe,
() => true,
() => false,
);
const [shown, setShown] = useState(false);
if (!isClient) {
return <span className="text-muted">Afficher le numéro</span>;
}
const display = REV.split("").reverse().join("");
const tel = display.replace(/\s/g, "");
const link = (
<a href={`tel:${tel}`} className="text-accent hover:underline">
{display}
</a>
);
return (
<>
{/* Visible uniquement à l'impression / export PDF */}
<span className="hidden print:inline">{link}</span>
<span className="print:hidden">
{shown ? (
link
) : (
<button
type="button"
onClick={() => setShown(true)}
className="text-accent hover:underline"
>
Afficher le numéro
</button>
)}
</span>
</>
);
}

100
app/error.tsx Normal file
View file

@ -0,0 +1,100 @@
"use client";
import { useEffect, useRef } from "react";
import Link from "next/link";
import gsap from "gsap";
export default function RouteError({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
const root = useRef<HTMLElement>(null);
useEffect(() => {
console.error(error);
}, [error]);
useEffect(() => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
const ctx = gsap.context(() => {
gsap.set("[data-fade]", { autoAlpha: 0, y: 14 });
gsap
.timeline({ defaults: { ease: "power3.out" } })
.from("[data-icon]", { autoAlpha: 0, scale: 0.6, duration: 0.4, ease: "back.out(2)" })
.to("[data-icon]", {
keyframes: { rotate: [0, -9, 8, -6, 4, 0] },
duration: 0.55,
ease: "power1.inOut",
})
.to("[data-fade]", { autoAlpha: 1, y: 0, duration: 0.5, stagger: 0.08 }, "-=0.3")
.to("[data-icon]", {
scale: 1.05,
duration: 1.6,
ease: "sine.inOut",
repeat: -1,
yoyo: true,
});
}, root);
return () => ctx.revert();
}, []);
return (
<main
ref={root}
className="flex min-h-screen flex-col items-center justify-center gap-6 px-6 text-center"
>
<svg
data-icon
viewBox="0 0 24 24"
className="size-20 text-danger"
fill="none"
stroke="currentColor"
strokeWidth="1.75"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
<div data-fade className="space-y-2">
<h1 className="text-2xl font-semibold">Quelque chose a planté</h1>
<p className="max-w-[46ch] text-muted">
Une erreur inattendue s&apos;est produite pendant le rendu de cette
page.
</p>
</div>
<div data-fade className="flex flex-wrap justify-center gap-3">
<button
type="button"
onClick={() => retry()}
className="rounded-md border border-line bg-surface px-4 py-2 text-sm font-semibold hover:border-muted"
>
Réessayer
</button>
<Link
href="/"
className="rounded-md border border-line px-4 py-2 text-sm font-semibold text-accent hover:border-muted"
>
Retour à l&apos;accueil
</Link>
</div>
{error.digest && (
<p data-fade className="font-mono text-xs text-muted">
ref: {error.digest}
</p>
)}
</main>
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

64
app/global-error.tsx Normal file
View file

@ -0,0 +1,64 @@
"use client";
import { useEffect, useRef } from "react";
import gsap from "gsap";
import "./globals.css";
export default function GlobalError({
error,
retry,
}: {
error: Error & { digest?: string };
retry: () => void;
}) {
const root = useRef<HTMLBodyElement>(null);
useEffect(() => {
console.error(error);
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
const ctx = gsap.context(() => {
gsap.from("[data-fade]", {
autoAlpha: 0,
y: 16,
duration: 0.5,
ease: "power3.out",
stagger: 0.09,
});
}, root);
return () => ctx.revert();
}, [error]);
return (
<html lang="fr">
<title>Erreur — Franck Thiaw-Woaye-Lapierre</title>
<body ref={root} className="min-h-screen antialiased">
<main className="flex min-h-screen flex-col items-center justify-center gap-6 px-6 text-center">
<div
data-fade
className="leading-none font-bold text-danger [font-size:clamp(4rem,18vw,9rem)]"
>
500
</div>
<div data-fade className="space-y-2">
<h1 className="text-2xl font-semibold">Erreur critique</h1>
<p className="max-w-[46ch] text-muted">
L&apos;application n&apos;a pas pu se charger. Réessaie dans un
instant.
</p>
</div>
<button
data-fade
type="button"
onClick={() => retry()}
className="rounded-md border border-line bg-surface px-4 py-2 text-sm font-semibold hover:border-muted"
>
Recharger
</button>
</main>
</body>
</html>
);
}

View file

@ -1,26 +1,83 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme {
--color-canvas: #0d1117;
--color-surface: #161b22;
--color-line: #30363d;
--color-line-soft: #21262d;
--color-fg: #e6edf3;
--color-muted: #8b949e;
--color-accent: #58a6ff;
--color-success: #3fb950;
--color-danger: #f85149;
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans",
Helvetica, Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
"Liberation Mono", monospace;
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
background:
radial-gradient(
620px 460px at -8% -12%,
rgba(56, 132, 255, 0.22),
transparent 58%
),
radial-gradient(
580px 460px at 108% -8%,
rgba(150, 100, 255, 0.18),
transparent 58%
),
radial-gradient(
900px 560px at 50% 118%,
rgba(22, 200, 165, 0.12),
transparent 62%
),
#0a0e17;
background-attachment: fixed;
color: var(--color-fg);
font-family: var(--font-sans);
font-size: 14px;
line-height: 1.5;
}
/* Panneaux « liquid glass » — verre dépoli translucide */
@utility glass {
background: rgba(20, 25, 38, 0.62);
backdrop-filter: blur(20px) saturate(1.4);
-webkit-backdrop-filter: blur(20px) saturate(1.4);
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.12),
0 16px 44px -18px rgba(0, 0, 0, 0.7);
}
:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
border-radius: 6px;
}
@media print {
.no-print {
display: none !important;
}
:root {
--color-canvas: #ffffff;
--color-surface: #ffffff;
--color-line: #d0d7de;
--color-line-soft: #e4e8ec;
--color-fg: #14161a;
--color-muted: #48505b;
--color-accent: #0b5cad;
--color-success: #1a7f37;
}
body {
background: #fff;
color: #14161a;
}
a {
text-decoration: none;
}
}

BIN
app/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

67
app/intro.tsx Normal file
View file

@ -0,0 +1,67 @@
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
import gsap from "gsap";
export default function Intro() {
const pathname = usePathname();
useEffect(() => {
const overlay = "[data-intro-overlay]";
const reveal = gsap.utils.toArray<HTMLElement>("[data-reveal]");
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// No reveal targets (e.g. error / not-found pages) or reduced motion: skip the intro.
if (reduce || reveal.length === 0) {
gsap.set(overlay, { display: "none" });
return;
}
gsap.set(reveal, { autoAlpha: 0, y: 12 });
const tl = gsap.timeline({
defaults: { ease: "power3.out" },
onComplete: () => gsap.set(reveal, { clearProps: "all" }),
});
tl.to("[data-intro-mark]", { autoAlpha: 1, scale: 1, duration: 0.45, ease: "power2.out" }, 0)
.fromTo(
"[data-intro-bar]",
{ scaleX: 0 },
{ scaleX: 1, duration: 0.85, ease: "power2.inOut" },
0.1,
)
.to(overlay, { yPercent: -100, duration: 0.5, ease: "power3.inOut" }, ">-0.1")
.to(reveal, { autoAlpha: 1, y: 0, duration: 0.5, stagger: 0.07 }, "<0.2")
.set(overlay, { display: "none" });
return () => {
tl.kill();
gsap.set(reveal, { clearProps: "all" });
};
}, []);
if (pathname !== "/") return null;
return (
<div
data-intro-overlay
className="fixed inset-0 z-50 flex items-center justify-center bg-canvas"
aria-hidden="true"
>
<div
data-intro-bar
className="absolute inset-x-0 top-0 h-[3px] origin-left bg-accent"
style={{ transform: "scaleX(0)" }}
/>
<div
data-intro-mark
className="grid size-12 place-items-center rounded-xl bg-surface text-lg font-bold"
style={{ opacity: 0, transform: "scale(0.94)" }}
>
F
</div>
</div>
);
}

View file

@ -1,29 +1,86 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import type { Metadata, Viewport } from "next";
import "./globals.css";
import Intro from "./intro";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
const SITE = "https://francktwl.fr";
const NAME = "Franck Thiaw-Woaye-Lapierre";
const TAGLINE = "Développeur web & DevOps à La Réunion";
const DESCRIPTION =
"Franck Thiaw-Woaye-Lapierre — étudiant en L3 informatique à La Réunion. " +
"Développement web (Laravel, React) et DevOps : serveurs Linux, Docker, auto-hébergement.";
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
metadataBase: new URL(SITE),
title: {
default: `${NAME} — ${TAGLINE}`,
template: `%s · ${NAME}`,
},
description: DESCRIPTION,
applicationName: `${NAME} · Portfolio`,
authors: [{ name: NAME, url: SITE }],
creator: NAME,
publisher: NAME,
keywords: [
NAME,
"Franck Thiaw Woaye Lapierre",
"francktwl",
"développeur La Réunion",
"développeur web Réunion",
"DevOps La Réunion",
"DevOps Réunion 974",
"administrateur système Réunion",
"développeur Laravel Réunion",
"auto-hébergement",
"Linux",
"Docker",
"portfolio développeur Réunion",
],
category: "technology",
alternates: { canonical: "/" },
openGraph: {
type: "profile",
firstName: "Franck",
lastName: "Thiaw-Woaye-Lapierre",
username: "francktwl",
url: SITE,
siteName: `${NAME} — Portfolio`,
title: `${NAME} — ${TAGLINE}`,
description: DESCRIPTION,
locale: "fr_FR",
},
twitter: {
card: "summary_large_image",
title: `${NAME} — ${TAGLINE}`,
description: DESCRIPTION,
},
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
"max-snippet": -1,
"max-video-preview": -1,
},
},
};
export const viewport: Viewport = {
themeColor: "#0d1117",
};
export default function RootLayout({ children }: LayoutProps<"/">) {
return (
<html
lang="en"
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
>
<body className="min-h-full flex flex-col">{children}</body>
<html lang="fr" className="h-full">
<body className="min-h-full antialiased">
{children}
<Intro />
<noscript>
<style>{`[data-intro-overlay]{display:none!important}`}</style>
</noscript>
</body>
</html>
);
}

81
app/not-found.tsx Normal file
View file

@ -0,0 +1,81 @@
"use client";
import { useEffect, useRef } from "react";
import { usePathname } from "next/navigation";
import Link from "next/link";
import gsap from "gsap";
export default function NotFound() {
const root = useRef<HTMLElement>(null);
const pathname = usePathname();
useEffect(() => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) return;
const ctx = gsap.context(() => {
const digits = gsap.utils.toArray<HTMLElement>("[data-digit]");
gsap.set("[data-fade]", { autoAlpha: 0, y: 14 });
gsap
.timeline({ defaults: { ease: "power3.out" } })
.from(digits, {
yPercent: -130,
rotate: -14,
autoAlpha: 0,
duration: 0.7,
ease: "back.out(1.6)",
stagger: 0.12,
})
.to("[data-fade]", { autoAlpha: 1, y: 0, duration: 0.5, stagger: 0.09 }, "-=0.15")
.add(() => {
gsap.to(digits, {
y: (i: number) => (i % 2 ? 7 : -7),
duration: 2.4,
ease: "sine.inOut",
repeat: -1,
yoyo: true,
stagger: { each: 0.15, from: "center" },
});
});
}, root);
return () => ctx.revert();
}, []);
return (
<main
ref={root}
className="flex min-h-screen flex-col items-center justify-center gap-8 px-6 text-center"
>
<div className="flex leading-none font-bold text-fg [font-size:clamp(6rem,26vw,13rem)]">
<span data-digit>4</span>
<span data-digit className="text-accent">
0
</span>
<span data-digit>4</span>
</div>
<p data-fade className="max-w-full font-mono text-sm break-words text-muted">
fatal: le chemin «&nbsp;
<span className="text-fg">{pathname}</span>
&nbsp;» n&apos;existe pas dans ce dépôt
</p>
<div data-fade className="flex flex-wrap justify-center gap-3">
<Link
href="/"
className="rounded-md border border-line bg-surface px-4 py-2 text-sm font-semibold hover:border-muted"
>
← Retour à l&apos;accueil
</Link>
<Link
href="/#projets"
className="rounded-md border border-line px-4 py-2 text-sm font-semibold text-accent hover:border-muted"
>
Voir les projets
</Link>
</div>
</main>
);
}

BIN
app/opengraph-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -1,69 +1,404 @@
import Image from "next/image";
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 (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert h-5 w-[100px]"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(personLd) }}
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the{" "}
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
page.tsx
</code>{" "}
file.
<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="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Learning
</a>{" "}
center.
<p className="text-base font-light text-muted md:text-xl">
@francktwl
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert h-[14px] w-4"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={14}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</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>
</>
);
}

61
app/robots.ts Normal file
View file

@ -0,0 +1,61 @@
import type { MetadataRoute } from "next";
export const dynamic = "force-static";
// Moteurs de recherche + robots d'aperçu de lien (pour que le référencement
// et les cartes Open Graph continuent de fonctionner quand tu partages le site).
const ALLOWED = [
"Googlebot",
"Google-InspectionTool",
"Bingbot",
"DuckDuckBot",
"facebookexternalhit",
"Twitterbot",
"LinkedInBot",
"Slackbot-LinkExpanding",
"Discordbot",
"TelegramBot",
"WhatsApp",
];
// Robots d'entraînement IA et scrapers connus : bloqués explicitement.
const BLOCKED = [
"GPTBot",
"ChatGPT-User",
"OAI-SearchBot",
"ClaudeBot",
"Claude-Web",
"anthropic-ai",
"Google-Extended",
"PerplexityBot",
"CCBot",
"Bytespider",
"Amazonbot",
"Applebot-Extended",
"Meta-ExternalAgent",
"FacebookBot",
"cohere-ai",
"Diffbot",
"Omgilibot",
"ImagesiftBot",
"YouBot",
"AhrefsBot",
"SemrushBot",
"MJ12bot",
"DotBot",
"DataForSeoBot",
"Scrapy",
];
export default function robots(): MetadataRoute.Robots {
return {
rules: [
...ALLOWED.map((userAgent) => ({ userAgent, allow: "/" })),
...BLOCKED.map((userAgent) => ({ userAgent, disallow: "/" })),
// Tout le reste : interdit.
{ userAgent: "*", disallow: "/" },
],
sitemap: "https://francktwl.fr/sitemap.xml",
host: "https://francktwl.fr",
};
}

20
app/sitemap.ts Normal file
View file

@ -0,0 +1,20 @@
import type { MetadataRoute } from "next";
export const dynamic = "force-static";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: "https://francktwl.fr",
lastModified: new Date(),
changeFrequency: "monthly",
priority: 1,
},
{
url: "https://francktwl.fr/cv",
lastModified: new Date(),
changeFrequency: "monthly",
priority: 0.8,
},
];
}

BIN
app/twitter-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

View file

@ -1,7 +1,10 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
output: "export",
trailingSlash: true,
images: { unoptimized: true },
devIndicators: false,
};
export default nextConfig;

7
package-lock.json generated
View file

@ -8,6 +8,7 @@
"name": "francktwl",
"version": "0.1.0",
"dependencies": {
"gsap": "^3.15.0",
"next": "16.3.4",
"react": "19.2.8",
"react-dom": "19.2.8"
@ -4064,6 +4065,12 @@
"dev": true,
"license": "ISC"
},
"node_modules/gsap": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/gsap/-/gsap-3.15.0.tgz",
"integrity": "sha512-dMW4CWBTUK1AEEDeZc1g4xpPGIrSf9fJF960qbTZmN/QwZIWY5wgliS6JWl9/25fpTGJrMRtSjGtOmPnfjZB+A==",
"license": "Standard 'no charge' license: https://gsap.com/standard-license."
},
"node_modules/has-bigints": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",

View file

@ -9,6 +9,7 @@
"lint": "eslint"
},
"dependencies": {
"gsap": "^3.15.0",
"next": "16.3.4",
"react": "19.2.8",
"react-dom": "19.2.8"

View file

@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View file

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/pp.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View file

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B