design refacto
All checks were successful
Deploy Sokko G / deploy (push) Successful in 6s

This commit is contained in:
Shinuwa 2026-07-22 16:07:10 +02:00
parent b8d1829bc7
commit d359fd0a35
25 changed files with 1340 additions and 1023 deletions

View file

@ -4,13 +4,14 @@ import { MhwildsPage } from "./mhwilds/MhwildsPage.jsx";
export function GameRoute(props) {
const { gameId, games } = props;
const game = games.find((item) => item.id === gameId);
const heroStyle = game?.images?.heroBg ? { "--game-hero-image": `url("${game.images.heroBg}")` } : undefined;
if (!game) return <GamesPage {...props} />;
if (game.id === "mhwilds") return <MhwildsPage {...props} />;
if (game.id === "mhwilds") return <MhwildsPage {...props} game={game} />;
return (
<>
<section className="game-hero" style={{ background: game.cover }}>
<section className="game-hero" style={heroStyle}>
<div>
<p className="eyebrow">{game.eyebrow || "Guide de jeu"}</p>
<h1>{game.title}</h1>

View file

@ -3,7 +3,7 @@ export function GamesPage({ siteContent, games, gamesError }) {
return (
<>
<section className="page-heading">
<section className="page-hero">
<div>
<p className="eyebrow">{content.eyebrow}</p>
<h1>{content.title}</h1>
@ -20,7 +20,7 @@ export function GamesPage({ siteContent, games, gamesError }) {
title={`Ouvrir ${game.title}`}
style={{ "--game-cover": game.cover || "var(--gradient-nebula)" }}
>
<img src={game.image || ""} alt={game.title} loading="lazy" />
<img src={game.images?.cardCover || game.image || ""} alt={game.title} loading="lazy" />
</a>
<div className="card-body">
<p className="eyebrow">{game.eyebrow || "Guide de jeu"}</p>

View file

@ -1,9 +1,11 @@
import { assetPath } from "./utils.js";
export function MhwildsOverview() {
export function MhwildsOverview({ game }) {
const heroStyle = game?.images?.heroBg ? { "--game-hero-image": `url("${game.images.heroBg}")` } : undefined;
return (
<>
<section className="game-hero mhwilds-hero">
<section className="game-hero" style={heroStyle}>
<div>
<p className="eyebrow">Guide de jeu</p>
<h1>Monster Hunter: Wilds</h1>

View file

@ -1,13 +1,14 @@
import { MhwildsListing } from "./MhwildsListing.jsx";
import { MhwildsOverview } from "./MhwildsOverview.jsx";
export function MhwildsPage({ category, mhwilds, filters, setFilters, t }) {
export function MhwildsPage({ category, game, mhwilds, filters, setFilters, t }) {
const activeCategory = category === "monsters" || category === "endemic" ? category : "";
const heroStyle = game?.images?.heroBg ? { "--game-hero-image": `url("${game.images.heroBg}")` } : undefined;
if (!mhwilds.loaded) {
return (
<>
<section className="game-hero mhwilds-hero">
<section className="game-hero" style={heroStyle}>
<div>
<p className="eyebrow">Monster Hunter Wilds</p>
<h1>Monster Hunter: Wilds</h1>
@ -31,7 +32,7 @@ export function MhwildsPage({ category, mhwilds, filters, setFilters, t }) {
);
}
if (!activeCategory) return <MhwildsOverview />;
if (!activeCategory) return <MhwildsOverview game={game} />;
return (
<MhwildsListing

View file

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { Icon } from "../../../components/Icon.jsx";
import { ChecklistModule } from "./ChecklistModule.jsx";
@ -15,11 +15,53 @@ const MODULE_COMPONENTS = {
counters: { label: "Compteurs", icon: "abacus", Component: CountersModule, editable: true }
};
const MODULE_COLUMN_GAP_PX = 16;
export const TOOLBOX_MODULES = Object.fromEntries(Object.entries(MODULE_COMPONENTS).map(([type, module]) => [
type,
{ label: module.label, icon: module.icon }
]));
function getSequentialModuleColumns(modules, splitIndex) {
return [
modules.slice(0, splitIndex),
modules.slice(splitIndex)
];
}
function getColumnHeight(heights) {
if (!heights.length) return 0;
return heights.reduce((total, height) => total + height, 0) + MODULE_COLUMN_GAP_PX * (heights.length - 1);
}
function chooseMeasuredSplitIndex(modules, moduleHeights, fallbackSplitIndex) {
if (modules.length <= 1) return modules.length;
if (modules.some((module) => !moduleHeights.get(module.id))) return fallbackSplitIndex;
let bestSplitIndex = fallbackSplitIndex;
let bestDiff = Number.POSITIVE_INFINITY;
let bestLeftDominantSplitIndex = 0;
let bestLeftDominantDiff = Number.POSITIVE_INFINITY;
for (let splitIndex = 1; splitIndex < modules.length; splitIndex += 1) {
const leftHeight = getColumnHeight(modules.slice(0, splitIndex).map((module) => moduleHeights.get(module.id)));
const rightHeight = getColumnHeight(modules.slice(splitIndex).map((module) => moduleHeights.get(module.id)));
const diff = Math.abs(leftHeight - rightHeight);
if (leftHeight >= rightHeight && diff < bestLeftDominantDiff) {
bestLeftDominantDiff = diff;
bestLeftDominantSplitIndex = splitIndex;
}
if (diff < bestDiff) {
bestDiff = diff;
bestSplitIndex = splitIndex;
}
}
return bestLeftDominantSplitIndex || bestSplitIndex;
}
export function AddToolControls({ onAdd }) {
const [open, setOpen] = useState(false);
const [quickOpen, setQuickOpen] = useState(false);
@ -134,6 +176,48 @@ export function AddToolControls({ onAdd }) {
export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDelete, onMove }) {
const [draggingModuleId, setDraggingModuleId] = useState("");
const [dropTarget, setDropTarget] = useState({ id: "", placement: "before" });
const [measuredSplitIndex, setMeasuredSplitIndex] = useState(() => Math.ceil(toolbox.modules.length / 2));
const moduleElementsRef = useRef(new Map());
const moduleIdSignature = useMemo(() => toolbox.modules.map((module) => module.id).join("|"), [toolbox.modules]);
useEffect(() => {
setMeasuredSplitIndex(Math.ceil(toolbox.modules.length / 2));
}, [toolbox.id, moduleIdSignature, toolbox.modules.length]);
useLayoutEffect(() => {
if (moduleColumns !== 2 || toolbox.modules.length <= 1) return undefined;
let frameId = 0;
function rebalanceColumns() {
cancelAnimationFrame(frameId);
frameId = requestAnimationFrame(() => {
const moduleHeights = new Map();
toolbox.modules.forEach((module) => {
const element = moduleElementsRef.current.get(module.id);
if (element) moduleHeights.set(module.id, element.getBoundingClientRect().height);
});
setMeasuredSplitIndex((currentSplitIndex) => {
const fallbackSplitIndex = Math.min(Math.max(currentSplitIndex, 1), toolbox.modules.length - 1);
const nextSplitIndex = chooseMeasuredSplitIndex(toolbox.modules, moduleHeights, fallbackSplitIndex);
return nextSplitIndex === currentSplitIndex ? currentSplitIndex : nextSplitIndex;
});
});
}
const observer = new ResizeObserver(rebalanceColumns);
toolbox.modules.forEach((module) => {
const element = moduleElementsRef.current.get(module.id);
if (element) observer.observe(element);
});
rebalanceColumns();
return () => {
cancelAnimationFrame(frameId);
observer.disconnect();
};
}, [moduleColumns, toolbox.id, moduleIdSignature, toolbox.modules.length]);
useEffect(() => {
if (!draggingModuleId) return undefined;
@ -180,6 +264,14 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDe
setDropTarget({ id: "", placement: "before" });
}
function registerModuleElement(moduleId, element) {
if (element) {
moduleElementsRef.current.set(moduleId, element);
} else {
moduleElementsRef.current.delete(moduleId);
}
}
if (moduleColumns === 1) {
return (
<section className="modules" data-toolbox-id={toolbox.id} data-layout-cols={moduleColumns}>
@ -195,14 +287,15 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDe
onRename={onRename}
onDelete={onDelete}
onMove={onMove}
registerModuleElement={registerModuleElement}
/>
))}
</section>
);
}
const columns = [[], []];
toolbox.modules.forEach((module, index) => columns[index % 2].push(module));
const splitIndex = Math.min(Math.max(measuredSplitIndex, 1), Math.max(1, toolbox.modules.length - 1));
const columns = getSequentialModuleColumns(toolbox.modules, splitIndex);
return (
<section className="modules" data-toolbox-id={toolbox.id} data-layout-cols={moduleColumns}>
@ -220,6 +313,7 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDe
onRename={onRename}
onDelete={onDelete}
onMove={onMove}
registerModuleElement={registerModuleElement}
/>
))}
</div>
@ -228,7 +322,7 @@ export function ToolboxModules({ toolbox, moduleColumns, context, onRename, onDe
);
}
function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, onDragStart, onRename, onDelete }) {
function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, onDragStart, onRename, onDelete, registerModuleElement }) {
const definition = MODULE_COMPONENTS[module.type] || MODULE_COMPONENTS.notepad;
const Component = definition.Component;
const label = definition.label || module.type;
@ -247,6 +341,7 @@ function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, o
className={className}
data-toolbox-id={toolbox.id}
data-module-id={module.id}
ref={(element) => registerModuleElement?.(module.id, element)}
>
<header>
<div>
@ -273,7 +368,7 @@ function ModuleShell({ toolbox, module, context, draggingModuleId, dropTarget, o
aria-pressed={editing}
title={editing ? "Masquer l'ajout" : "Ajouter"}
>
<Icon name="edit" />
<Icon name="add" />
</button>
)}
<button className="module-delete-button danger" onClick={() => onDelete(module.id)} aria-label={`Retirer ${module.title || label}`} title="Retirer">