diff --git a/tests/static-app.test.mjs b/tests/static-app.test.mjs index 8c6fa4c..ae557b7 100644 --- a/tests/static-app.test.mjs +++ b/tests/static-app.test.mjs @@ -17,6 +17,7 @@ test("vite entrypoint and app shell are wired", async () => { const homeOnboarding = await readFile("website/src/pages/HomeOnboardingSection.jsx", "utf8"); const aboutPage = await readFile("website/src/pages/AboutPage.jsx", "utf8"); const libraryPage = await readFile("website/src/pages/LibraryPage.jsx", "utf8"); + const moduleRegistry = await readFile("website/src/features/toolboxes/toolboxModuleRegistry.js", "utf8"); const hashRouter = await readFile("website/src/router/hashRouter.js", "utf8"); const routeContent = await readFile("website/src/router/RouteContent.jsx", "utf8"); const bodyScrollLock = await readFile("website/src/utils/bodyScrollLock.js", "utf8"); @@ -83,8 +84,11 @@ test("vite entrypoint and app shell are wired", async () => { assert.match(aboutPage, /about-tool-examples/); assert.match(libraryPage, /export function LibraryPage/); assert.match(libraryPage, /LIBRARY_TOOLBOX_ID/); - assert.match(libraryPage, /TaskPlannerModule/); - assert.match(libraryPage, /EquipmentPlannerModule/); + assert.match(libraryPage, /TOOLBOX_MODULE_LOADERS/); + assert.match(libraryPage, /Object\.entries\(TOOLBOX_MODULE_LOADERS\)/); + assert.match(moduleRegistry, /export const TOOLBOX_MODULE_LOADERS/); + assert.match(moduleRegistry, /taskPlanner: \(\) => import\("\.\/modules\/TaskPlannerModule\.jsx"\)/); + assert.match(moduleRegistry, /equipmentPlanner: \(\) => import\("\.\/modules\/EquipmentPlannerModule\.jsx"\)/); assert.match(libraryPage, /normalizeEquipmentPlannerData/); assert.match(libraryPage, /summarizeEquipmentPlannerData/); assert.match(bodyScrollLock, /modalLocks/); diff --git a/tests/static-toolboxes.test.mjs b/tests/static-toolboxes.test.mjs index 40b6463..0f65cb1 100644 --- a/tests/static-toolboxes.test.mjs +++ b/tests/static-toolboxes.test.mjs @@ -26,6 +26,7 @@ test("toolbox storage, cards and pages are wired", async () => { const checklistStorage = await readFile("website/src/features/toolboxes/storage/modules/checklist.js", "utf8"); const timerStorage = await readFile("website/src/features/toolboxes/storage/modules/timer.js", "utf8"); const calendarStorage = await readFile("website/src/features/toolboxes/storage/modules/calendar.js", "utf8"); + const resetUtils = await readFile("website/src/features/toolboxes/modules/resetUtils.js", "utf8"); const equipmentStorage = await readFile("website/src/features/toolboxes/storage/modules/equipmentPlanner.js", "utf8"); const indexedToolboxesHook = await readFile("website/src/features/toolboxes/storage/useIndexedToolboxes.js", "utf8"); const optimisticModuleData = await readFile("website/src/features/toolboxes/storage/optimisticModuleData.js", "utf8"); @@ -86,6 +87,8 @@ test("toolbox storage, cards and pages are wired", async () => { assert.match(storageCompactorRegistry, /goalTree: compactGoalTreeDataForStorage/); assert.match(timerStorage, /scrollResults/); assert.match(calendarStorage, /normalizeCalendarData/); + assert.match(resetUtils, /export function getLatestDailyReset/); + assert.match(resetUtils, /export function getLatestWeeklyReset/); assert.match(toolboxStorage, /nextId\("calendar"\)/); assert.match(toolboxCard, /DEFAULT_TOOLBOX_ICON/); assert.match(toolboxCard, /toolbox\.customCover/); diff --git a/website/src/features/toolboxes/modules/goalTreeUtils.js b/website/src/features/toolboxes/modules/goalTreeUtils.js index 64f5194..08b0530 100644 --- a/website/src/features/toolboxes/modules/goalTreeUtils.js +++ b/website/src/features/toolboxes/modules/goalTreeUtils.js @@ -1,4 +1,5 @@ // Rôle : regroupe les calculs purs de l'arbre d'objectifs. +import { getLatestDailyReset, getLatestWeeklyReset } from "./resetUtils.js"; export const GOAL_TREE_KINDS = ["step", "text", "checklist"]; export const GOAL_TREE_RESET_TYPES = ["none", "daily", "weekly"]; export const GOAL_TREE_ICON_NAMES = ["cube", "fire", "flask", "grass", "leaf", "meat", "powder", "tear", "ball"]; @@ -220,32 +221,6 @@ export function evaluateGoalTreeNodes(nodes) { })); } -function parseResetTime(value) { - const [hours, minutes] = String(value || "00:00").split(":").map((part) => Number.parseInt(part, 10)); - return { - hours: Number.isInteger(hours) ? Math.min(23, Math.max(0, hours)) : 0, - minutes: Number.isInteger(minutes) ? Math.min(59, Math.max(0, minutes)) : 0 - }; -} - -function getLatestDailyReset(now, resetTime) { - const { hours, minutes } = parseResetTime(resetTime); - const reset = new Date(now); - reset.setHours(hours, minutes, 0, 0); - if (reset.getTime() > now) reset.setDate(reset.getDate() - 1); - return reset.getTime(); -} - -function getLatestWeeklyReset(now, resetTime, weekDay) { - const { hours, minutes } = parseResetTime(resetTime); - const reset = new Date(now); - reset.setHours(hours, minutes, 0, 0); - const dayDiff = (reset.getDay() - weekDay + 7) % 7; - reset.setDate(reset.getDate() - dayDiff); - if (reset.getTime() > now) reset.setDate(reset.getDate() - 7); - return reset.getTime(); -} - export function applyGoalTreeDueResets(data, now = Date.now()) { let latestResetAt = data.lastResetAt || 0; let changed = false; diff --git a/website/src/features/toolboxes/modules/resetUtils.js b/website/src/features/toolboxes/modules/resetUtils.js new file mode 100644 index 0000000..1c336b6 --- /dev/null +++ b/website/src/features/toolboxes/modules/resetUtils.js @@ -0,0 +1,26 @@ +// Rôle : partage les calculs de réinitialisation temporelle des outils. +export function parseResetTime(value) { + const [hours, minutes] = String(value || "00:00").split(":").map((part) => Number.parseInt(part, 10)); + return { + hours: Number.isInteger(hours) ? Math.min(23, Math.max(0, hours)) : 0, + minutes: Number.isInteger(minutes) ? Math.min(59, Math.max(0, minutes)) : 0 + }; +} + +export function getLatestDailyReset(now, resetTime) { + const { hours, minutes } = parseResetTime(resetTime); + const reset = new Date(now); + reset.setHours(hours, minutes, 0, 0); + if (reset.getTime() > now) reset.setDate(reset.getDate() - 1); + return reset.getTime(); +} + +export function getLatestWeeklyReset(now, resetTime, weekDay) { + const { hours, minutes } = parseResetTime(resetTime); + const reset = new Date(now); + reset.setHours(hours, minutes, 0, 0); + const dayDiff = (reset.getDay() - weekDay + 7) % 7; + reset.setDate(reset.getDate() - dayDiff); + if (reset.getTime() > now) reset.setDate(reset.getDate() - 7); + return reset.getTime(); +} diff --git a/website/src/features/toolboxes/modules/taskPlannerUtils.js b/website/src/features/toolboxes/modules/taskPlannerUtils.js index 821a907..dcf4c5c 100644 --- a/website/src/features/toolboxes/modules/taskPlannerUtils.js +++ b/website/src/features/toolboxes/modules/taskPlannerUtils.js @@ -1,4 +1,6 @@ // Rôle : regroupe les constantes et helpers purs de l'outil task planner. +import { getLatestDailyReset, getLatestWeeklyReset, parseResetTime } from "./resetUtils.js"; + export const TASK_TYPES = ["daily", "weekly", "unique"]; const DAILY_RESET_SOON_MS = 6 * 60 * 60 * 1000; const WEEKLY_RESET_SOON_MS = 48 * 60 * 60 * 1000; @@ -19,32 +21,6 @@ export function getTaskTypeLabel(type, textContent) { return textContent.uniqueType || "Ponctuelle"; } -export function parseResetTime(value) { - const [hours, minutes] = String(value || "00:00").split(":").map((part) => Number.parseInt(part, 10)); - return { - hours: Number.isInteger(hours) ? Math.min(23, Math.max(0, hours)) : 0, - minutes: Number.isInteger(minutes) ? Math.min(59, Math.max(0, minutes)) : 0 - }; -} - -export function getLatestDailyReset(now, resetTime) { - const { hours, minutes } = parseResetTime(resetTime); - const reset = new Date(now); - reset.setHours(hours, minutes, 0, 0); - if (reset.getTime() > now) reset.setDate(reset.getDate() - 1); - return reset.getTime(); -} - -export function getLatestWeeklyReset(now, resetTime, weekDay) { - const { hours, minutes } = parseResetTime(resetTime); - const reset = new Date(now); - reset.setHours(hours, minutes, 0, 0); - const dayDiff = (reset.getDay() - weekDay + 7) % 7; - reset.setDate(reset.getDate() - dayDiff); - if (reset.getTime() > now) reset.setDate(reset.getDate() - 7); - return reset.getTime(); -} - export function getNextDailyReset(now, resetTime) { const { hours, minutes } = parseResetTime(resetTime); const reset = new Date(now); diff --git a/website/src/features/toolboxes/toolboxModuleRegistry.js b/website/src/features/toolboxes/toolboxModuleRegistry.js index 571a4a0..c0426f9 100644 --- a/website/src/features/toolboxes/toolboxModuleRegistry.js +++ b/website/src/features/toolboxes/toolboxModuleRegistry.js @@ -1,4 +1,4 @@ -// Rôle : centralise les métadonnées légères des outils toolbox sans importer leurs composants. +// Rôle : centralise les métadonnées et les chargeurs lazy des outils toolbox. export const TOOLBOX_MODULE_DEFINITIONS = { notepad: { label: "Bloc Note", @@ -162,6 +162,26 @@ export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(TOOLBOX_MODULE_ { label: definition.label, icon: definition.icon } ])); +export const TOOLBOX_MODULE_LOADERS = { + notepad: () => import("./modules/NotepadModule.jsx").then((module) => ({ default: module.NotepadModule })), + memos: () => import("./modules/MemosModule.jsx").then((module) => ({ default: module.MemosModule })), + checklist: () => import("./modules/ChecklistModule.jsx").then((module) => ({ default: module.ChecklistModule })), + images: () => import("./modules/ImagesModule.jsx").then((module) => ({ default: module.ImagesModule })), + links: () => import("./modules/LinksModule.jsx").then((module) => ({ default: module.LinksModule })), + counters: () => import("./modules/CountersModule.jsx").then((module) => ({ default: module.CountersModule })), + randomizer: () => import("./modules/RandomizerModule.jsx").then((module) => ({ default: module.RandomizerModule })), + rngSimulator: () => import("./modules/RngSimulatorModule.jsx").then((module) => ({ default: module.RngSimulatorModule })), + combos: () => import("./modules/CombosModule.jsx").then((module) => ({ default: module.CombosModule })), + calculator: () => import("./modules/CalculatorModule.jsx").then((module) => ({ default: module.CalculatorModule })), + calendar: () => import("./modules/CalendarModule.jsx").then((module) => ({ default: module.CalendarModule })), + table: () => import("./modules/TableModule.jsx").then((module) => ({ default: module.TableModule })), + goalTree: () => import("./modules/GoalTreeModule.jsx").then((module) => ({ default: module.GoalTreeModule })), + timer: () => import("./modules/TimerModule.jsx").then((module) => ({ default: module.TimerModule })), + taskPlanner: () => import("./modules/TaskPlannerModule.jsx").then((module) => ({ default: module.TaskPlannerModule })), + equipmentPlanner: () => import("./modules/EquipmentPlannerModule.jsx").then((module) => ({ default: module.EquipmentPlannerModule })), + imageAnnotation: () => import("./modules/ImageAnnotationModule.jsx").then((module) => ({ default: module.ImageAnnotationModule })) +}; + export const LIBRARY_CATEGORY_DEFINITIONS = [ { key: "notesTracking", types: ["memos", "notepad", "checklist", "counters"] }, { key: "references", types: ["images", "imageAnnotation", "links"] }, diff --git a/website/src/pages/LibraryPage.jsx b/website/src/pages/LibraryPage.jsx index 053db14..0d8646b 100644 --- a/website/src/pages/LibraryPage.jsx +++ b/website/src/pages/LibraryPage.jsx @@ -1,7 +1,7 @@ // Rôle : affiche la librairie des outils avec des exemples locaux non persistés. import { lazy, Suspense, useEffect, useMemo, useRef, useState } from "react"; import { Icon } from "../components/Icon.jsx"; -import { LIBRARY_CATEGORY_DEFINITIONS, getToolboxModuleDefinition } from "../features/toolboxes/toolboxModuleRegistry.js"; +import { LIBRARY_CATEGORY_DEFINITIONS, getToolboxModuleDefinition, TOOLBOX_MODULE_LOADERS } from "../features/toolboxes/toolboxModuleRegistry.js"; import { clampQty, hostnameFromUrl, @@ -29,25 +29,7 @@ import { const LIBRARY_TOOLBOX_ID = "library"; const LIBRARY_ANCHOR_RETRY_DELAYS = [0, 80, 240, 600]; -const MODULE_COMPONENTS = { - memos: lazy(() => import("../features/toolboxes/modules/MemosModule.jsx").then((module) => ({ default: module.MemosModule }))), - notepad: lazy(() => import("../features/toolboxes/modules/NotepadModule.jsx").then((module) => ({ default: module.NotepadModule }))), - checklist: lazy(() => import("../features/toolboxes/modules/ChecklistModule.jsx").then((module) => ({ default: module.ChecklistModule }))), - images: lazy(() => import("../features/toolboxes/modules/ImagesModule.jsx").then((module) => ({ default: module.ImagesModule }))), - links: lazy(() => import("../features/toolboxes/modules/LinksModule.jsx").then((module) => ({ default: module.LinksModule }))), - counters: lazy(() => import("../features/toolboxes/modules/CountersModule.jsx").then((module) => ({ default: module.CountersModule }))), - combos: lazy(() => import("../features/toolboxes/modules/CombosModule.jsx").then((module) => ({ default: module.CombosModule }))), - calculator: lazy(() => import("../features/toolboxes/modules/CalculatorModule.jsx").then((module) => ({ default: module.CalculatorModule }))), - calendar: lazy(() => import("../features/toolboxes/modules/CalendarModule.jsx").then((module) => ({ default: module.CalendarModule }))), - table: lazy(() => import("../features/toolboxes/modules/TableModule.jsx").then((module) => ({ default: module.TableModule }))), - goalTree: lazy(() => import("../features/toolboxes/modules/GoalTreeModule.jsx").then((module) => ({ default: module.GoalTreeModule }))), - timer: lazy(() => import("../features/toolboxes/modules/TimerModule.jsx").then((module) => ({ default: module.TimerModule }))), - taskPlanner: lazy(() => import("../features/toolboxes/modules/TaskPlannerModule.jsx").then((module) => ({ default: module.TaskPlannerModule }))), - equipmentPlanner: lazy(() => import("../features/toolboxes/modules/EquipmentPlannerModule.jsx").then((module) => ({ default: module.EquipmentPlannerModule }))), - imageAnnotation: lazy(() => import("../features/toolboxes/modules/ImageAnnotationModule.jsx").then((module) => ({ default: module.ImageAnnotationModule }))), - randomizer: lazy(() => import("../features/toolboxes/modules/RandomizerModule.jsx").then((module) => ({ default: module.RandomizerModule }))), - rngSimulator: lazy(() => import("../features/toolboxes/modules/RngSimulatorModule.jsx").then((module) => ({ default: module.RngSimulatorModule }))) -}; +const MODULE_COMPONENTS = Object.fromEntries(Object.entries(TOOLBOX_MODULE_LOADERS).map(([type, loader]) => [type, lazy(loader)])); function cloneData(value) { if (value === undefined) return {};