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

This commit is contained in:
Shinuwa 2026-07-22 22:49:36 +02:00
parent 34523e78b1
commit 5c86fd2ceb
19 changed files with 724 additions and 519 deletions

View file

@ -4,7 +4,9 @@ import "./styles/main.scss";
import { Icon } from "./components/Icon.jsx";
import { GameRoute } from "./features/games/GameRoute.jsx";
import { GamesPage } from "./features/games/GamesPage.jsx";
import { INITIAL_DIABLO4_STATE, INITIAL_MHWILDS_STATE, loadDiablo4Data, loadMhwildsData } from "./features/games/loaders.js";
import { AddToolControls, ToolboxModules, TOOLBOX_MODULES } from "./features/toolboxes/modules/index.jsx";
import { usePointerReorder } from "./hooks/usePointerReorder.js";
const STORAGE_KEYS = {
registry: "sokkog:toolboxes",
@ -594,25 +596,8 @@ function App() {
const [siteContent, setSiteContent] = useState(DEFAULT_SITE_CONTENT);
const [games, setGames] = useState([]);
const [gamesError, setGamesError] = useState("");
const [mhwilds, setMhwilds] = useState({
loaded: false,
loading: false,
error: "",
translations: {},
monsters: [],
endemic: [],
filterOptions: { monsters: [], endemic: [] },
filterOptionKeys: { monsters: "", endemic: "" }
});
const [diablo4, setDiablo4] = useState({
loaded: false,
loading: false,
error: "",
affixes: [],
filterOptions: { affixes: [] },
categoryMap: {},
filterOptionKeys: { affixes: "" }
});
const [mhwilds, setMhwilds] = useState(INITIAL_MHWILDS_STATE);
const [diablo4, setDiablo4] = useState(INITIAL_DIABLO4_STATE);
const [filters, setFilters] = useState({
monsters: { name: "", weaknesses: [], logic: "and" },
endemic: { name: "", locations: [], logic: "and" },
@ -642,77 +627,17 @@ function App() {
useEffect(() => {
if (!route.startsWith("/games/mhwilds") || mhwilds.loaded || mhwilds.loading) return;
setMhwilds((state) => ({ ...state, loading: true, error: "" }));
Promise.all([
fetch("/data/mhwilds/monsters.json"),
fetch("/data/mhwilds/endemic_life.json"),
fetch("/data/mhwilds/i18n/fr.json")
]).then(async ([monstersResponse, endemicResponse, translationsResponse]) => {
if (!monstersResponse.ok || !endemicResponse.ok || !translationsResponse.ok) {
throw new Error("Impossible de charger les données Monster Hunter Wilds.");
}
const [monstersJson, endemicJson, translations] = await Promise.all([
monstersResponse.json(),
endemicResponse.json(),
translationsResponse.json()
]);
const monsterFilterKey = Object.keys(monstersJson).find((key) => key !== "monsters") || "";
const endemicFilterKey = Object.keys(endemicJson).find((key) => key !== "endemicLife" && key !== "aquaticLife") || "";
setMhwilds({
loaded: true,
loading: false,
error: "",
translations,
monsters: monstersJson.monsters || [],
endemic: [...(endemicJson.endemicLife || []), ...(endemicJson.aquaticLife || [])],
filterOptions: {
monsters: monstersJson[monsterFilterKey] || [],
endemic: endemicJson[endemicFilterKey] || []
},
filterOptionKeys: {
monsters: monsterFilterKey,
endemic: endemicFilterKey
}
});
}).catch((error) => setMhwilds({
loaded: true,
loading: false,
error: error.message,
translations: {},
monsters: [],
endemic: [],
filterOptions: { monsters: [], endemic: [] },
filterOptionKeys: { monsters: "", endemic: "" }
}));
loadMhwildsData()
.then(setMhwilds)
.catch((error) => setMhwilds({ ...INITIAL_MHWILDS_STATE, loaded: true, error: error.message }));
}, [route, mhwilds.loaded, mhwilds.loading]);
useEffect(() => {
if (!route.startsWith("/games/diablo4") || diablo4.loaded || diablo4.loading) return;
setDiablo4((state) => ({ ...state, loading: true }));
fetch("/data/diablo4/affixes_types.json")
.then(async (response) => {
if (!response.ok) throw new Error("Impossible de charger les données Diablo IV.");
const payload = await response.json();
const filterKey = Object.keys(payload).find((key) => key !== "affixes") || "";
const categories = payload[filterKey] || [];
setDiablo4({
loaded: true,
loading: false,
error: "",
affixes: payload.affixes || [],
filterOptions: { affixes: categories },
categoryMap: Object.fromEntries(categories.map((category) => [category.id, category])),
filterOptionKeys: { affixes: filterKey }
});
})
.catch((error) => setDiablo4({
loaded: true,
loading: false,
error: error.message,
affixes: [],
filterOptions: { affixes: [] },
categoryMap: {},
filterOptionKeys: { affixes: "" }
}));
setDiablo4((state) => ({ ...state, loading: true, error: "" }));
loadDiablo4Data()
.then(setDiablo4)
.catch((error) => setDiablo4({ ...INITIAL_DIABLO4_STATE, loaded: true, error: error.message }));
}, [route, diablo4.loaded, diablo4.loading]);
const t = (key, { capitalize = false } = {}) => {
@ -852,6 +777,10 @@ function App() {
setScreenshot,
addScreenshotFiles,
updateModuleData: store.updateModuleData,
updateToolboxOrder: (orderedIds) => {
const order = new Map(orderedIds.map((id, index) => [id, index]));
store.setToolboxes([...store.toolboxes].sort((a, b) => (order.get(a.id) ?? 9999) - (order.get(b.id) ?? 9999)));
},
importToolbox: async (file, gameId = "") => {
try {
return await importToolbox(file, gameId);
@ -1070,6 +999,26 @@ function RichText({ text }) {
function ToolboxesPage({ siteContent, toolboxes, getToolboxGame, actions }) {
const content = siteContent.toolboxes;
const updateToolboxOrder = actions.updateToolboxOrder;
const {
draggingId: draggingToolboxId,
dropTarget,
startDrag: startToolboxDrag
} = usePointerReorder({
targetSelector: ".toolbox-card",
getTargetId: (target) => target.dataset.toolboxId,
getPlacement: (event, target) => {
const rect = target.getBoundingClientRect();
return event.clientY > rect.top + rect.height / 2 || event.clientX > rect.left + rect.width / 2 ? "after" : "before";
},
onMove: (draggingId, targetId, placement) => {
const nextIds = toolboxes.map((toolbox) => toolbox.id).filter((id) => id !== draggingId);
const targetIndex = nextIds.indexOf(targetId);
nextIds.splice(placement === "after" ? targetIndex + 1 : targetIndex, 0, draggingId);
updateToolboxOrder(nextIds);
}
});
return (
<div className="toolbox-page">
<section className="page-hero">
@ -1091,7 +1040,17 @@ function ToolboxesPage({ siteContent, toolboxes, getToolboxGame, actions }) {
</section>
<section className="cards">
<StorageHelpCard help={content.storageHelp} />
{toolboxes.length ? toolboxes.map((toolbox) => <ToolboxCard key={toolbox.id} toolbox={toolbox} game={getToolboxGame(toolbox)} actions={actions} />) : (
{toolboxes.length ? toolboxes.map((toolbox) => (
<ToolboxCard
key={toolbox.id}
toolbox={toolbox}
game={getToolboxGame(toolbox)}
actions={actions}
draggingToolboxId={draggingToolboxId}
dropTarget={dropTarget}
onDragStart={startToolboxDrag}
/>
)) : (
<div className="empty"><h2>{content.emptyTitle}</h2><p>{content.emptyText}</p></div>
)}
</section>
@ -1117,11 +1076,30 @@ function StorageHelpCard({ help }) {
);
}
function ToolboxCard({ toolbox, game, actions }) {
function ToolboxCard({ toolbox, game, actions, draggingToolboxId, dropTarget, onDragStart }) {
const gameCoverImage = getGameCardCover(game);
const coverImage = gameCoverImage || toolbox.icon || DEFAULT_TOOLBOX_ICON;
const isDragging = draggingToolboxId === toolbox.id;
const isDropTarget = dropTarget.id === toolbox.id;
const className = [
"card",
"toolbox-card",
isDragging ? "is-dragging" : "",
isDropTarget ? "is-drop-target" : "",
isDropTarget && dropTarget.placement === "after" ? "drop-after" : ""
].filter(Boolean).join(" ");
return (
<article className="card toolbox-card">
<article className={className} data-toolbox-id={toolbox.id}>
<button
className="toolbox-card-drag-handle"
type="button"
onPointerDown={(event) => onDragStart(event, toolbox.id)}
aria-label={`Déplacer ${toolbox.name}`}
title="Déplacer"
>
<Icon name="drag" />
</button>
<a
className={`card-cover toolbox-card-cover-link ${gameCoverImage ? "toolbox-card-cover" : "toolbox-icon-cover"}`}
href={`#/toolbox/${toolbox.id}`}