From eef955dd3f749a92411642b95b2967a7971196fa Mon Sep 17 00:00:00 2001 From: BlackAngelTVdev Date: Wed, 27 May 2026 19:14:34 +0200 Subject: [PATCH] feat: main logic for one page --- README.md | 14 ++ index.html | 39 +++++ p/assets/main-nav/app.js | 166 ++++++++++++++++++ p/assets/main-styles/styles.css | 289 ++++++++++++++++++++++++++++++++ p/dl/index.html | 39 +++++ 5 files changed, 547 insertions(+) create mode 100644 index.html create mode 100644 p/assets/main-nav/app.js create mode 100644 p/assets/main-styles/styles.css create mode 100644 p/dl/index.html diff --git a/README.md b/README.md index e69de29..12f7e26 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,14 @@ +Site statique one-page. + +Le projet ne dépend pas de Node.js, ni d'un build, ni d'un bundler. + +Utilisation: + +1. Ouvrir `index.html` dans un navigateur, ou +2. Le servir avec n'importe quel serveur statique local, comme Five Server. + +Navigation: + +- `/?p/dl/` charge `p/dl/index.html` dans le `main` +- Chaque sous-dossier de `p/` peut devenir une route, par exemple `p/contact/index.html` pour `/?p/contact/` +- Le header et le footer restent communs à toutes les vues diff --git a/index.html b/index.html new file mode 100644 index 0000000..da5cbf5 --- /dev/null +++ b/index.html @@ -0,0 +1,39 @@ + + + + + + + Fws Web + + + +
+ + +
+ +
+ +
+

Un seul index, un header, un footer, et du contenu injecté dans le main selon l'URL.

+
+
+ + + + \ No newline at end of file diff --git a/p/assets/main-nav/app.js b/p/assets/main-nav/app.js new file mode 100644 index 0000000..05ed466 --- /dev/null +++ b/p/assets/main-nav/app.js @@ -0,0 +1,166 @@ +(() => { + const main = document.getElementById("app-main"); + + const links = Array.from(document.querySelectorAll("[data-route]")); + + function normalizeRoute(value) { + const raw = (value ?? "").toString().trim().toLowerCase(); + if (!raw || raw === "/" || raw === "home" || raw === "index" || raw === "accueil") { + return "home"; + } + return raw.replace(/^\/+|\/+$/g, "") || "home"; + } + + function routeFromLocation() { + const params = new URLSearchParams(window.location.search); + + if (params.has("p")) { + return normalizeRoute(params.get("p")); + } + + const rawSearch = window.location.search.replace(/^\?/, ""); + if (rawSearch.startsWith("p/")) { + return normalizeRoute(rawSearch.slice(2)); + } + + if (rawSearch.startsWith("p=")) { + return normalizeRoute(rawSearch.slice(2)); + } + + const path = window.location.pathname.replace(/^\/+|\/+$/g, ""); + if (path) { + const parts = path.split("/"); + if (parts[0] === "p" && parts[1]) { + return normalizeRoute(parts.slice(1).join("/")); + } + return normalizeRoute(parts[0]); + } + + return "home"; + } + + function setActiveRoute(route) { + links.forEach((link) => { + const isActive = link.dataset.route === route; + if (isActive) { + link.setAttribute("aria-current", "page"); + } else { + link.removeAttribute("aria-current"); + } + }); + } + + function setCanonicalUrl(route, replace = false) { + const url = route === "home" ? "/" : `/?p/${route}/`; + if (replace) { + history.replaceState({ route }, "", url); + return; + } + history.pushState({ route }, "", url); + } + + function routeToSource(route) { + return route === "home" ? null : `p/${route}/index.html`; + } + + function routeToTitle(route) { + if (route === "home") { + return "Fws Web"; + } + + const label = route + .split("/") + .filter(Boolean) + .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)) + .join(" / "); + + return `Fws Web - ${label}`; + } + + async function loadRoute(route) { + if (route === "home") { + main.innerHTML = ` +
+

Navigation one-page

+

Le site change de contenu sans changer de page.

+

+ Les liens du menu modifient l'URL, puis le script charge le HTML correspondant depuis + p/... et l'injecte dans le main. +

+ +
+ +
+
+

Header commun

+

Le bandeau du haut reste identique sur chaque vue.

+
+
+

Footer commun

+

Le pied de page reste fixe, quelle que soit la section chargée.

+
+
+

Injection réelle

+

Chaque route peut venir d'un vrai fichier index.html dans p/.

+
+
+ `; + return; + } + + const source = routeToSource(route); + if (!source) { + throw new Error(`Route inconnue: ${route}`); + } + + const response = await fetch(source, { cache: "no-store" }); + if (!response.ok) { + throw new Error(`Impossible de charger ${source} (${response.status})`); + } + + const html = await response.text(); + const parsed = new DOMParser().parseFromString(html, "text/html"); + const injected = parsed.querySelector("[data-page-content]") ?? parsed.body; + main.innerHTML = injected.innerHTML.trim(); + } + + function render(route, { replaceHistory = false } = {}) { + const safeRoute = route || "home"; + + loadRoute(safeRoute) + .then(() => { + setActiveRoute(safeRoute); + setCanonicalUrl(safeRoute, replaceHistory); + document.title = routeToTitle(safeRoute); + window.scrollTo({ top: 0, behavior: "smooth" }); + }) + .catch((error) => { + main.innerHTML = `

Erreur de chargement

${error.message}

`; + setActiveRoute("home"); + }); + } + + document.addEventListener("click", (event) => { + const link = event.target.closest("[data-route]"); + if (!link) { + return; + } + + const route = normalizeRoute(link.dataset.route); + if (!route) { + return; + } + + event.preventDefault(); + render(route); + }); + + window.addEventListener("popstate", () => { + render(routeFromLocation(), { replaceHistory: true }); + }); + + render(routeFromLocation(), { replaceHistory: true }); +})(); \ No newline at end of file diff --git a/p/assets/main-styles/styles.css b/p/assets/main-styles/styles.css new file mode 100644 index 0000000..3908f9c --- /dev/null +++ b/p/assets/main-styles/styles.css @@ -0,0 +1,289 @@ +:root { + color-scheme: dark; + --bg: #0b1020; + --bg-soft: rgba(11, 16, 32, 0.72); + --panel: rgba(17, 25, 49, 0.82); + --panel-border: rgba(148, 163, 184, 0.18); + --text: #e5eefc; + --muted: #9fb0cc; + --accent: #7dd3fc; + --accent-strong: #38bdf8; + --shadow: 0 24px 70px rgba(0, 0, 0, 0.35); +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-height: 100vh; + color: var(--text); + font-family: "Trebuchet MS", "Gill Sans", "Segoe UI", sans-serif; + background: + radial-gradient(circle at top left, rgba(56, 189, 248, 0.24), transparent 28%), + radial-gradient(circle at right center, rgba(14, 165, 233, 0.14), transparent 32%), + linear-gradient(160deg, #050816 0%, #0b1020 52%, #08101d 100%); +} + +body::before, +body::after { + content: ""; + position: fixed; + inset: auto; + border-radius: 999px; + pointer-events: none; + filter: blur(18px); + opacity: 0.6; +} + +body::before { + width: 22rem; + height: 22rem; + top: -6rem; + left: -8rem; + background: rgba(56, 189, 248, 0.16); +} + +body::after { + width: 18rem; + height: 18rem; + right: -4rem; + bottom: 2rem; + background: rgba(125, 211, 252, 0.12); +} + +.page-shell { + position: relative; + z-index: 1; + width: min(1120px, calc(100% - 2rem)); + margin: 0 auto; + min-height: 100vh; + display: grid; + grid-template-rows: auto 1fr auto; + gap: 1.25rem; + padding: 1rem 0 1.25rem; +} + +.site-header, +.site-footer, +.hero, +.card { + backdrop-filter: blur(18px); + background: var(--bg-soft); + border: 1px solid var(--panel-border); + box-shadow: var(--shadow); +} + +.site-header, +.site-footer { + border-radius: 1.5rem; +} + +.site-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + padding: 1rem 1.25rem; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 0.9rem; + color: inherit; + text-decoration: none; +} + +.brand-mark { + display: grid; + place-items: center; + width: 2.75rem; + height: 2.75rem; + border-radius: 0.95rem; + background: linear-gradient(145deg, rgba(125, 211, 252, 0.95), rgba(56, 189, 248, 0.45)); + color: #08101d; + font-weight: 800; +} + +.brand-text { + display: grid; + line-height: 1.05; +} + +.brand-text strong, +.hero h1, +.card h2 { + font-family: "Georgia", "Palatino Linotype", serif; +} + +.brand-text small { + color: var(--muted); +} + +.site-nav { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; +} + +.site-nav a, +.button { + border-radius: 999px; + transition: + transform 160ms ease, + background 160ms ease, + color 160ms ease, + border-color 160ms ease; +} + +.site-nav a { + padding: 0.7rem 1rem; + color: var(--muted); + text-decoration: none; + border: 1px solid transparent; +} + +.site-nav a:hover, +.site-nav a[aria-current="page"] { + color: var(--text); + border-color: rgba(125, 211, 252, 0.4); + background: rgba(125, 211, 252, 0.08); +} + +.site-main { + display: grid; + gap: 1.25rem; + align-content: start; +} + +.hero { + border-radius: 2rem; + padding: clamp(1.5rem, 3vw, 2.75rem); +} + +.eyebrow { + margin: 0 0 0.75rem; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.22em; + font-size: 0.78rem; + font-weight: 700; +} + +.hero h1 { + margin: 0; + font-size: clamp(2.2rem, 5vw, 4.5rem); + line-height: 0.98; + max-width: 12ch; +} + +.lead { + max-width: 62ch; + color: var(--muted); + font-size: 1.08rem; + line-height: 1.7; + margin: 1rem 0 0; +} + +.lead span, +.card span { + color: var(--text); +} + +.hero-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1.5rem; +} + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.85rem 1.2rem; + text-decoration: none; + border: 1px solid rgba(148, 163, 184, 0.22); + font-weight: 700; +} + +.button:hover, +.site-nav a:hover { + transform: translateY(-1px); +} + +.button-primary { + color: #08101d; + background: linear-gradient(145deg, var(--accent), var(--accent-strong)); +} + +.button-secondary { + color: var(--text); + background: rgba(255, 255, 255, 0.04); +} + +.grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +} + +.card { + border-radius: 1.4rem; + padding: 1.25rem; +} + +.card h2 { + margin-top: 0; + margin-bottom: 0.75rem; + font-size: 1.45rem; +} + +.card p { + margin: 0; + color: var(--muted); + line-height: 1.65; +} + +.site-footer { + padding: 1rem 1.25rem; + color: var(--muted); +} + +.site-footer p { + margin: 0; +} + +@media (max-width: 900px) { + .grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 700px) { + .page-shell { + width: min(100% - 1rem, 1120px); + gap: 0.9rem; + padding-top: 0.5rem; + } + + .site-header { + flex-direction: column; + align-items: stretch; + } + + .site-nav { + justify-content: space-between; + } + + .site-nav a, + .button { + width: 100%; + } +} \ No newline at end of file diff --git a/p/dl/index.html b/p/dl/index.html new file mode 100644 index 0000000..3417dca --- /dev/null +++ b/p/dl/index.html @@ -0,0 +1,39 @@ + + + + + + DL + + +
+
+

Route DL

+

Le contenu de la section DL charge ici.

+

+ Cette page vit dans p/dl/index.html et son contenu est injecté dans le + main quand l'URL pointe vers /?p/dl/. +

+ +
+ +
+
+

Vrai fichier

+

Le HTML est bien stocké dans le dossier p/, pas dans le fichier principal.

+
+
+

Injection

+

Le script lit ce fichier puis injecte uniquement le bloc portant data-page-content.

+
+
+

Extensible

+

Tu peux créer d'autres dossiers comme p/contact/ ou p/about/.

+
+
+
+ + \ No newline at end of file