world-game/src/router/index.js
isUnknown 947275544d
All checks were successful
Deploy / Deploy to Production (push) Successful in 5m26s
perf : deferred slide rendering + sequential loading by proximity. related to #55
Only render the active slide initially. After its critical media (videos)
fires canplaythrough, progressively render remaining slides by distance.
JSON loading is now sequential by proximity instead of all-parallel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 11:31:40 +02:00

182 lines
5 KiB
JavaScript

import { slides } from "@state/slides.svelte";
import { site } from "@state/site.svelte";
import { locale } from "@state/locale.svelte";
let siteInitialized = false;
function normalizePath(path) {
const stripped = path.replace(/^\/en(\/|$)/, "$1") || "/";
return stripped === "/" ? "/home" : stripped;
}
function apiPrefix() {
return locale.current === "en" ? "/en" : "";
}
/**
* Trouve l'index de la slide correspondant au path.
* Si le path exact n'existe pas, essaie le chemin parent
* (ex: /blog/article-slug → /blog).
*/
function findSlideIndex(path) {
let idx = slides.getIndexByPath(path);
if (idx !== -1) return idx;
const parentPath = path.replace(/\/[^/]+$/, "");
if (parentPath) return slides.getIndexByPath(parentPath);
return -1;
}
async function loadSlide(path) {
let slidePath = path;
let idx = slides.getIndexByPath(slidePath);
// Sub-page: resolve to parent slide (ex: /blog/slug → /blog)
if (idx === -1) {
const parentPath = path.replace(/\/[^/]+$/, "");
if (parentPath && parentPath !== path) {
const parentIdx = slides.getIndexByPath(parentPath);
if (parentIdx !== -1) {
idx = parentIdx;
slidePath = parentPath;
} else if (!siteInitialized) {
// Slides not yet initialized — assume sub-page, fetch parent to bootstrap
slidePath = parentPath;
}
}
}
if (idx !== -1) {
const slide = slides.all[idx];
if (slide.loaded || slide.loading) return;
slides.setLoading(slidePath, true);
}
try {
const response = await fetch(`${apiPrefix()}${slidePath}.json`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
console.log("slide data", data);
if (!siteInitialized && data.site) {
site.set(data.site);
locale.initialize(data.site.language, data.site.languages);
slides.init(data.site.navigation);
siteInitialized = true;
}
const finalIdx = slides.getIndexByPath(slidePath);
if (finalIdx !== -1) {
slides.setData(slidePath, data);
} else {
slides.setStandalone(data);
}
} catch (error) {
console.error(`[router] Failed to load slide ${slidePath}:`, error);
slides.setLoading(slidePath, false);
}
}
async function loadAllSlidesInBackground(exceptPath) {
const activeIdx = slides.getIndexByPath(exceptPath)
const remaining = slides.all
.map((s, i) => ({ path: s.path, distance: Math.abs(i - activeIdx) }))
.filter(s => s.path !== exceptPath)
.sort((a, b) => a.distance - b.distance)
for (const { path } of remaining) {
await loadSlide(path)
}
}
export function slideTo(path, { skipHistory = false } = {}) {
path = normalizePath(path);
if (!skipHistory) {
const historyPath =
locale.current === "en"
? path === "/home"
? "/en"
: `/en${path}`
: path === "/home"
? "/"
: path;
history.pushState({}, "", historyPath);
}
const idx = findSlideIndex(path);
const slidePath = idx !== -1 ? slides.all[idx].path : path;
if (idx !== -1) {
slides.clearStandalone();
if (slides.all[idx].title) {
document.title = `World Game - ${slides.all[idx].title}`;
}
if (idx === slides.activeIndex && !skipHistory) {
window.dispatchEvent(new PopStateEvent('popstate'));
return;
}
slides.slideTo(slidePath);
if (!slides.all[idx].loaded) {
loadSlide(slidePath);
}
} else {
// Page standalone (hors navigation)
loadSlide(path);
}
}
export async function initRouter() {
// Language detection: URL prefix > localStorage > navigator
const hasEnPrefix = window.location.pathname.startsWith("/en");
if (hasEnPrefix) {
locale.setLanguage("en");
localStorage.setItem("wg_lang", "en");
} else if (!localStorage.getItem("wg_lang")) {
const navLang = navigator.language || navigator.languages?.[0] || "fr";
if (navLang.startsWith("en")) {
window.location.replace("/en" + window.location.pathname);
return;
}
} else if (localStorage.getItem("wg_lang") === "en") {
window.location.replace("/en" + window.location.pathname);
return;
}
const initialPath = normalizePath(window.location.pathname);
await loadSlide(initialPath);
const idx = findSlideIndex(initialPath);
if (idx !== -1) {
slides.setActiveIndex(idx);
}
loadAllSlidesInBackground(idx !== -1 ? slides.all[idx].path : initialPath);
window.addEventListener("popstate", () => {
const path = normalizePath(window.location.pathname);
slideTo(path, { skipHistory: true });
});
document.addEventListener("click", (e) => {
const link = e.target.closest("a");
if (!link) return;
const url = new URL(link.href, window.location.origin);
if (
url.origin === window.location.origin &&
!link.target &&
!link.hasAttribute("download")
) {
e.preventDefault();
slideTo(url.pathname);
}
});
}
// Keep navigateTo as alias so existing views don't break
export const navigateTo = slideTo;