// Rôle : point d'entrée React, assemble données, routes, shell et overlays. /* eslint-disable react-refresh/only-export-components -- point d'entrée Vite sans export de composant. */ import { lazy, StrictMode, Suspense, useCallback, useEffect, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import "./styles/main.scss"; import { AppOverlays } from "./components/AppOverlays.jsx"; import { AppErrorBoundary } from "./components/AppErrorBoundary.jsx"; import { Shell } from "./components/Shell.jsx"; import { Icon } from "./components/Icon.jsx"; import { useIndexedToolboxes } from "./features/toolboxes/storage/useIndexedToolboxes.js"; import { useToolboxActions } from "./features/toolboxes/useToolboxActions.js"; import { useAppData } from "./hooks/useAppData.js"; import { GuideChecklistContext } from "./features/games/useGuideChecklist.js"; import { useGuideChecklistStorage } from "./hooks/useGuideChecklistStorage.js"; import { useGameFilters } from "./hooks/useGameFilters.js"; import { useToolboxAlerts } from "./hooks/useToolboxAlerts.js"; import { RouteContent } from "./router/RouteContent.jsx"; import { useHashRoute } from "./router/hashRouter.js"; import { normalizeCalendarData, normalizeTimerData } from "./features/toolboxes/storage/toolboxStorage.js"; import { useAutoBackup } from "./features/toolboxes/storage/useAutoBackup.js"; import { GlobalMemosTicker } from "./features/toolboxes/GlobalMemosTicker.jsx"; import { useMediaQuery } from "./hooks/useMediaQuery.js"; const assetRecoveryUrl = new URL(window.location.href); if (assetRecoveryUrl.searchParams.has("_asset-recovery")) { assetRecoveryUrl.searchParams.delete("_asset-recovery"); window.history.replaceState( window.history.state, "", `${assetRecoveryUrl.pathname}${assetRecoveryUrl.search}${assetRecoveryUrl.hash}` ); } const ToolboxDrawer = lazy(() => import("./features/toolboxes/ToolboxPages.jsx").then((module) => ({ default: module.ToolboxDrawer }))); const DEFAULT_NOTIFICATION_DURATION_MS = 120000; const SHORT_NOTIFICATION_DURATION_MS = 5000; function getNotificationDuration(message, options = {}) { if (options.persistent) return 0; if (Number.isFinite(options.durationMs)) return Math.max(1000, Number(options.durationMs)); const normalized = String(message || "").toLowerCase(); return /\bcopi[ée]|\bcoll[ée]|presse-papiers/.test(normalized) ? SHORT_NOTIFICATION_DURATION_MS : DEFAULT_NOTIFICATION_DURATION_MS; } function App() { const route = useHashRoute(); const compactGameLayout = useMediaQuery("(max-width: 1439px)"); const reducedSidebar = useMediaQuery("(max-width: 1024px)"); const { siteContent, siteContentError, games, gamesError, mhwilds, diablo4, aniimo } = useAppData(route); const [filters, setFilters] = useGameFilters(); const [confirmModal, setConfirmModal] = useState(null); const [createModal, setCreateModal] = useState(null); const [autoBackupModalOpen, setAutoBackupModalOpen] = useState(false); const [linkModalGameId, setLinkModalGameId] = useState(""); const [drawerGameId, setDrawerGameId] = useState(""); const [drawerVisible, setDrawerVisible] = useState(false); const [drawerContext, setDrawerContext] = useState({ route, compactGameLayout }); const [image, setImage] = useState(null); const [pendingTutorialToolboxId, setPendingTutorialToolboxId] = useState(""); const [notifications, setNotifications] = useState([]); const [storageError, setStorageError] = useState(""); const backupReminderIdRef = useRef(""); const handleStorageError = useCallback((message) => setStorageError(message), []); const store = useIndexedToolboxes(handleStorageError); const autoBackup = useAutoBackup(store); const { backupNeeded, requestPermission: requestBackupPermission } = autoBackup; const guideChecklistStorage = useGuideChecklistStorage(store); const guideScrollRef = useRef(0); const setGameToolbox = useCallback((gameId) => { if (gameId) { if (compactGameLayout && !drawerVisible) guideScrollRef.current = window.scrollY; setDrawerGameId(gameId); setDrawerVisible(true); if (compactGameLayout) window.requestAnimationFrame(() => window.scrollTo({ top: 0, behavior: "auto" })); return; } setDrawerVisible(false); if (compactGameLayout) { window.requestAnimationFrame(() => window.scrollTo({ top: guideScrollRef.current, behavior: "auto" })); } else { setDrawerGameId(""); } }, [compactGameLayout, drawerVisible]); if (drawerContext.route !== route || drawerContext.compactGameLayout !== compactGameLayout) { setDrawerContext({ route, compactGameLayout }); const activeGameId = route.match(/^\/games\/([^/]+)/)?.[1] || ""; if (activeGameId !== drawerGameId) { setDrawerGameId(""); setDrawerVisible(false); } else if (!compactGameLayout && !drawerVisible && drawerGameId) { setDrawerGameId(""); } } const notify = useCallback((message, options = {}) => { const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`; setNotifications((current) => [ { id, message, durationMs: getNotificationDuration(message, options), actionLabel: options.actionLabel, onAction: options.onAction }, ...current ].slice(0, 5)); return id; }, []); const dismissNotification = useCallback((notificationId) => { setNotifications((current) => current.filter((notification) => notification.id !== notificationId)); }, []); useEffect(() => { function handleNotification(event) { if (event.detail?.message) notify(event.detail.message, event.detail); } window.addEventListener("sokkog:notify", handleNotification); return () => window.removeEventListener("sokkog:notify", handleNotification); }, [notify]); useEffect(() => { if (!backupNeeded) { if (backupReminderIdRef.current) dismissNotification(backupReminderIdRef.current); backupReminderIdRef.current = ""; return; } if (backupReminderIdRef.current || !siteContent?.toolboxes?.autoBackup) return; const content = siteContent.toolboxes.autoBackup; backupReminderIdRef.current = notify(content.reminderMessage, { persistent: true, actionLabel: content.reminderAction, onAction: () => void requestBackupPermission() }); }, [backupNeeded, dismissNotification, notify, requestBackupPermission, siteContent]); const t = (key, { capitalize = false } = {}) => { const value = mhwilds.translations[key] || String(key || "").replace(/_/g, " "); return capitalize ? value.charAt(0).toUpperCase() + value.slice(1) : value; }; const getGame = (gameId) => games.find((item) => item.id === gameId); const getToolboxGameId = (toolbox) => Object.entries(store.links).find(([, toolboxId]) => toolboxId === toolbox?.id)?.[0] || ""; const getToolboxGame = (toolbox) => getGame(getToolboxGameId(toolbox)); const actions = useToolboxActions({ store, notify, creationTemplate: siteContent?.toolboxes?.creationTemplate, setCreateModal, setConfirmModal, setLinkModalGameId, setDrawerGameId: setGameToolbox, setImage }); useToolboxAlerts({ toolboxes: store.toolboxes, moduleData: store.moduleData, links: store.links, route, drawerGameId: drawerVisible ? drawerGameId : "", normalizeTimerData, normalizeCalendarData, textContent: siteContent?.toolboxes?.modules, notify }); if (!siteContent) { return ( Contenu indisponible {siteContentError || "Chargement du contenu..."} ); } if (!store.ready) { const content = siteContent.toolboxes.storageStatus; return ( {store.loadError ? content.errorTitle : content.loadingTitle} {store.loadError ? content.errorMessage : content.loadingMessage} {store.loadError && <> {store.loadError} {content.retryLabel} >} ); } return ( setAutoBackupModalOpen(true)} getModuleData={store.getModuleData} updateToolbox={actions.updateToolbox} updateModuleData={store.updateModuleData} addImageFiles={actions.addImageFiles} pendingTutorialToolboxId={pendingTutorialToolboxId} setPendingTutorialToolboxId={setPendingTutorialToolboxId} /> {drawerGameId && ( )} ); } createRoot(document.querySelector("#app")).render();
{siteContentError || "Chargement du contenu..."}
{store.loadError ? content.errorMessage : content.loadingMessage}
{store.loadError}