refonte: portfolio statique Astro (design moderne développeur)
Some checks failed
Deploy production / build-deploy (push) Failing after 56s

- Remplace l'ancien Astro hybrid + admin par un site one-page statique (sortie static)
- Thème clair/sombre sans flash, contenu typé (src/data/content.ts), zéro admin
- Icônes simple-icons (SVG monochrome), polices auto-hébergées (RGPD)
- SEO complet : canonical, Open Graph, Twitter Card, JSON-LD Person, robots.txt, sitemap.xml, image OG 1200x630
- CI Gitea de déploiement FTPS vers Plesk (.gitea/workflows/prod.yml)
This commit is contained in:
Johan LEROY
2026-06-19 16:03:09 +02:00
parent dc4e3820c4
commit ce17bf590b
189 changed files with 1836 additions and 9872 deletions

119
src/scripts/app.ts Normal file
View File

@@ -0,0 +1,119 @@
/* ============================================================
Comportements client (bundlés par Astro).
Le thème initial est posé AVANT le paint par le script inline
du <head> (cf. Base.astro) pour éviter tout flash.
Tout respecte prefers-reduced-motion.
============================================================ */
const reduced =
window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
/* ---------- Bascule de thème (persistée) ---------- */
function applyTheme(theme: "light" | "dark"): void {
if (theme === "light") document.documentElement.setAttribute("data-theme", "light");
else document.documentElement.removeAttribute("data-theme");
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", theme === "light" ? "#fafafa" : "#09090b");
}
function setupThemeToggle(): void {
const btn = document.getElementById("theme-toggle");
if (!btn) return;
btn.addEventListener("click", () => {
const isLight = document.documentElement.getAttribute("data-theme") === "light";
const next = isLight ? "dark" : "light";
applyTheme(next);
try { localStorage.setItem("jl-theme", next); } catch {}
});
}
/* ---------- Effet machine à écrire (mockup terminal) ---------- */
function setupTyping(): void {
const el = document.getElementById("typed");
if (!el) return;
const full = el.textContent ?? ""; // le contenu HTML sert d'état final (no-JS friendly)
if (reduced) return; // mouvement réduit : on garde l'état final
el.textContent = "";
let i = 0;
const tick = () => {
i += 1;
el.textContent = full.slice(0, i);
if (i < full.length) {
const ch = full.charAt(i - 1);
let d = 16;
if (ch === "\n") d = 80;
else if (ch === "{" || ch === "}") d = 40;
window.setTimeout(tick, d);
}
};
window.setTimeout(tick, 480);
}
/* ---------- Apparition des sections au scroll ---------- */
function setupReveal(): void {
const els = document.querySelectorAll<HTMLElement>(".reveal");
if (reduced || !("IntersectionObserver" in window)) {
els.forEach((el) => el.classList.add("is-visible"));
return;
}
const io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add("is-visible");
io.unobserve(e.target);
}
});
},
{ threshold: 0.1, rootMargin: "0px 0px -7% 0px" }
);
els.forEach((el) => io.observe(el));
}
/* ---------- Surlignage de la nav selon la section visible ---------- */
function setupActiveNav(): void {
if (!("IntersectionObserver" in window)) return;
const sections = document.querySelectorAll<HTMLElement>("section[id]");
const io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (!e.isIntersecting) return;
const id = e.target.id;
document.querySelectorAll<HTMLElement>("[data-nav]").forEach((link) => {
link.classList.toggle("is-active", link.getAttribute("data-nav") === id);
});
});
},
{ rootMargin: "-45% 0px -50% 0px" }
);
sections.forEach((s) => io.observe(s));
}
/* ---------- Menu mobile ---------- */
function setupMobileMenu(): void {
const burger = document.getElementById("burger");
const menu = document.getElementById("mobile-menu");
if (!burger || !menu) return;
const close = () => {
menu.classList.remove("is-open");
burger.setAttribute("aria-expanded", "false");
};
burger.addEventListener("click", () => {
const open = menu.classList.toggle("is-open");
burger.setAttribute("aria-expanded", open ? "true" : "false");
});
menu.querySelectorAll("a").forEach((a) => a.addEventListener("click", close));
window.addEventListener("resize", () => {
if (window.innerWidth >= 820) close();
});
}
function init(): void {
setupThemeToggle();
setupTyping();
setupReveal();
setupActiveNav();
setupMobileMenu();
}
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
else init();