All checks were successful
Deploy / Deploy to Production (push) Successful in 5m26s
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>
182 lines
5 KiB
JavaScript
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;
|