Migrate to React/Vite & Sass
Some checks failed
Deploy Sokko G / deploy (push) Failing after 4s

This commit is contained in:
Shinuwa 2026-07-21 10:09:25 +02:00
parent db4b99aee3
commit 1dee8b528f
30 changed files with 3491 additions and 2444 deletions

View file

@ -0,0 +1,30 @@
import { GamesPage } from "./GamesPage.jsx";
import { MhwildsPage } from "./mhwilds/MhwildsPage.jsx";
export function GameRoute(props) {
const { gameId, games } = props;
const game = games.find((item) => item.id === gameId);
if (!game) return <GamesPage {...props} />;
if (game.id === "mhwilds") return <MhwildsPage {...props} />;
return (
<>
<section className="game-hero" style={{ background: game.cover }}>
<div>
<p className="eyebrow">{game.eyebrow || "Guide de jeu"}</p>
<h1>{game.title}</h1>
<p>{game.summary}</p>
</div>
</section>
<section className="info-grid">
{game.sections.map((section) => (
<article className="info-panel" key={section.title}>
<h2>{section.title}</h2>
<ul>{section.items.map((item) => <li key={item}>{item}</li>)}</ul>
</article>
))}
</section>
</>
);
}

View file

@ -0,0 +1,35 @@
export function GamesPage({ games, gamesError }) {
return (
<>
<section className="page-heading">
<div>
<p className="eyebrow">Pages informatives</p>
<h1>Jeux disponibles</h1>
<p>Choisissez un jeu pour consulter ses données maintenues et associer une toolbox locale.</p>
</div>
</section>
<section className="game-list">
{games.length ? games.map((game) => (
<article className="card game-card" key={game.id}>
<div className="card-cover game-card-cover" style={{ "--game-cover": game.cover || "var(--gradient-nebula)" }}>
<img src={game.image || ""} alt={game.title} loading="lazy" />
</div>
<div className="card-body">
<p className="eyebrow">{game.eyebrow || "Guide de jeu"}</p>
<h2>{game.title}</h2>
<p>{game.summary}</p>
<div className="card-actions">
<a className="button primary" href={`#/games/${game.id}`}>Ouvrir</a>
</div>
</div>
</article>
)) : (
<div className="empty">
<h2>Aucun jeu disponible</h2>
<p>{gamesError || "Ajoutez des entrées dans /data/games.json."}</p>
</div>
)}
</section>
</>
);
}

View file

@ -0,0 +1,54 @@
import { Icon } from "../../../components/Icon.jsx";
import { assetPath } from "./utils.js";
export function MhwildsFilters({ category, filterKey, options, filters, setFilters, t }) {
const activeFilters = filters[category];
const selected = new Set(activeFilters[filterKey]);
const logicLabel = activeFilters.logic === "or" ? t("or", { capitalize: true }) : t("and", { capitalize: true });
const updateCategory = (updater) => setFilters((state) => ({ ...state, [category]: updater(state[category]) }));
return (
<div className="filter-panel">
<div className="filter-panel-head">
<div>
<p className="eyebrow">Filtres</p>
<h2>{category === "monsters" ? t("weaknesses", { capitalize: true }) : t("locations", { capitalize: true })}</h2>
</div>
<button
className="filter-reset-button"
onClick={() => updateCategory(() => ({ name: "", [filterKey]: [], logic: "and" }))}
aria-label={t("reset", { capitalize: true })}
title={t("reset", { capitalize: true })}
>
<Icon name="rubber" />
</button>
</div>
<label className="field compact">
<span>{t("name", { capitalize: true })}</span>
<input value={activeFilters.name} placeholder="Rechercher..." onChange={(event) => updateCategory((state) => ({ ...state, name: event.target.value }))} />
</label>
<div className="filter-logic">
<span>Correspondance</span>
<button onClick={() => updateCategory((state) => ({ ...state, logic: state.logic === "and" ? "or" : "and" }))}>{logicLabel}</button>
</div>
<div className="filter-options legacy-scrollbar">
{options.map((option) => (
<label className={`filter-chip ${selected.has(option) ? "active" : ""}`} key={option}>
<input
type="checkbox"
checked={selected.has(option)}
onChange={(event) => updateCategory((state) => {
const values = new Set(state[filterKey]);
if (event.target.checked) values.add(option);
else values.delete(option);
return { ...state, [filterKey]: [...values] };
})}
/>
<img src={assetPath(option)} alt="" />
<span>{t(option, { capitalize: true })}</span>
</label>
))}
</div>
</div>
);
}

View file

@ -0,0 +1,52 @@
import { useMemo } from "react";
import { EndemicCard } from "./cards/EndemicCard.jsx";
import { MonsterCard } from "./cards/MonsterCard.jsx";
import { MhwildsFilters } from "./MhwildsFilters.jsx";
import { getFilteredMhwildsItems, getUniqueConditionValues } from "./utils.js";
export function MhwildsListing({ category, mhwilds, filters, setFilters, t }) {
const isMonsters = category === "monsters";
const filterKey = isMonsters ? "weaknesses" : "locations";
const items = isMonsters ? mhwilds.monsters : mhwilds.endemic;
const options = getUniqueConditionValues(items, filterKey, t);
const visible = useMemo(() => getFilteredMhwildsItems({ category, mhwilds, filters, t }), [category, mhwilds, filters, t]);
const label = isMonsters ? t("monsters", { capitalize: true }) : t("endemic life", { capitalize: true });
return (
<>
<section className="page-heading mhwilds-heading">
<div>
<p className="eyebrow">Monster Hunter Wilds</p>
<div className="mhwilds-title-row">
<h1>{label}</h1>
<span className="results-count">{visible.length} / {items.length}</span>
</div>
<p>{isMonsters ? "Filtrez les monstres par nom et faiblesses, puis consultez leurs dégâts détaillés." : "Filtrez la faune par nom et zones d'apparition."}</p>
</div>
<div className="actions">
<a className={`button ${isMonsters ? "primary" : ""}`} href="#/games/mhwilds/monsters">Monstres</a>
<a className={`button ${!isMonsters ? "primary" : ""}`} href="#/games/mhwilds/endemic">Faune</a>
</div>
</section>
<section className="mhwilds-layout" data-mhwilds-category={category}>
<aside className="mhwilds-filters">
<MhwildsFilters category={category} filterKey={filterKey} options={options} filters={filters} setFilters={setFilters} t={t} />
</aside>
<section className="mhwilds-results" aria-live="polite">
<div className={`mhwilds-grid ${isMonsters ? "monster-grid" : "endemic-grid"}`}>
{visible.length ? visible.map((item) => (
isMonsters
? <MonsterCard key={item.name} monster={item} t={t} />
: <EndemicCard key={item.name} item={item} t={t} />
)) : (
<div className="empty">
<h2>Aucun résultat</h2>
<p>Ajustez la recherche ou réinitialisez les filtres actifs.</p>
</div>
)}
</div>
</section>
</section>
</>
);
}

View file

@ -0,0 +1,27 @@
import { assetPath } from "./utils.js";
export function MhwildsOverview() {
return (
<>
<section className="game-hero mhwilds-hero">
<div>
<p className="eyebrow">Guide de jeu</p>
<h1>Monster Hunter: Wilds</h1>
<p>Consultez rapidement les monstres, faiblesses, afflictions, hitzones et emplacements de faune endémique.</p>
</div>
</section>
<section className="mhwilds-home-grid">
<a className="feature mhwilds-home-card" href="#/games/mhwilds/monsters">
<img src={assetPath("arkveld")} alt="" />
<strong>Monstres</strong>
<span>Recherche, filtres par faiblesse et tableau de dégâts par partie.</span>
</a>
<a className="feature mhwilds-home-card" href="#/games/mhwilds/endemic">
<img src={assetPath("vigorwasp")} alt="" />
<strong>Faune endémique</strong>
<span>Faune endémique et aquatique filtrable par localisation.</span>
</a>
</section>
</>
);
}

View file

@ -0,0 +1,45 @@
import { MhwildsListing } from "./MhwildsListing.jsx";
import { MhwildsOverview } from "./MhwildsOverview.jsx";
export function MhwildsPage({ category, mhwilds, filters, setFilters, t }) {
const activeCategory = category === "monsters" || category === "endemic" ? category : "";
if (!mhwilds.loaded) {
return (
<>
<section className="game-hero mhwilds-hero">
<div>
<p className="eyebrow">Monster Hunter Wilds</p>
<h1>Monster Hunter: Wilds</h1>
<p>Chargement des données de chasse, faune endémique et filtres associés.</p>
</div>
</section>
<section className="empty">
<h2>Chargement</h2>
<p>Préparation des données locales...</p>
</section>
</>
);
}
if (mhwilds.error) {
return (
<section className="empty">
<h1>Impossible de charger MH Wilds</h1>
<p>{mhwilds.error}</p>
</section>
);
}
if (!activeCategory) return <MhwildsOverview />;
return (
<MhwildsListing
category={activeCategory}
mhwilds={mhwilds}
filters={filters}
setFilters={setFilters}
t={t}
/>
);
}

View file

@ -0,0 +1,34 @@
import { assetPath } from "../utils.js";
export function DamageTable({ rows, t }) {
if (!rows.length) return null;
const columns = Object.keys(rows[0]);
return (
<div className="damage-table-wrap legacy-scrollbar">
<table className="damage-table">
<thead>
<tr>
{columns.map((column) => (
<th key={column}>
{column === "name" ? "" : <img src={assetPath(column)} alt={t(column, { capitalize: true })} title={t(column, { capitalize: true })} />}
</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, rowIndex) => (
<tr key={`${row.name}-${rowIndex}`}>
{columns.map((column) => column === "name" ? (
<td key={column} title={t(row[column], { capitalize: true })}>{t(row[column], { capitalize: true })}</td>
) : (
<td key={column}><img src={assetPath(`${row[column]}-stars`)} alt={`${row[column]} étoiles`} /></td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}

View file

@ -0,0 +1,21 @@
import { IconRow } from "./IconRow.jsx";
import { assetPath, getConditionValues, normalizeText } from "../utils.js";
export function EndemicCard({ item, t }) {
const locations = getConditionValues(item.locations);
const description = t(item.description);
const hasDescription = normalizeText(description) && normalizeText(description) !== normalizeText("à compléter");
return (
<article className="mhwilds-card endemic-card">
<div className="mhwilds-card-art">
<img src={assetPath(item.name)} alt={t(item.name, { capitalize: true })} loading="lazy" />
</div>
<div className="mhwilds-card-body">
<h2>{t(item.name, { capitalize: true })}</h2>
<IconRow label={t("locations", { capitalize: true })} values={locations} t={t} />
{hasDescription && <p>{description}</p>}
</div>
</article>
);
}

View file

@ -0,0 +1,14 @@
import { assetPath } from "../utils.js";
export function IconRow({ label, values, t }) {
return (
<div className="mhwilds-icon-row">
<span>{label}</span>
<div>
{values.map((value) => value === "none"
? <em key={value}>-</em>
: <img key={value} src={assetPath(value)} alt={t(value, { capitalize: true })} title={t(value, { capitalize: true })} loading="lazy" />)}
</div>
</div>
);
}

View file

@ -0,0 +1,47 @@
import { useState } from "react";
import { DamageTable } from "./DamageTable.jsx";
import { IconRow } from "./IconRow.jsx";
import { assetPath, getConditionValues } from "../utils.js";
export function MonsterCard({ monster, t }) {
const [flipped, setFlipped] = useState(false);
const weaknesses = getConditionValues(monster.weaknesses);
const ailments = getConditionValues(monster.ailments).filter((value) => value !== "none");
return (
<article
className={`mhwilds-card monster-card ${flipped ? "is-flipped" : ""}`}
onClick={() => setFlipped(!flipped)}
onKeyDown={(event) => {
if (event.key !== "Enter" && event.key !== " ") return;
event.preventDefault();
setFlipped((value) => !value);
}}
tabIndex="0"
role="button"
aria-pressed={flipped}
aria-label={`Afficher les dégâts de ${t(monster.name, { capitalize: true })}`}
>
<div className="mhwilds-card-inner">
<div className="mhwilds-card-face mhwilds-card-front">
<div className="mhwilds-card-art">
<img src={assetPath(monster.name)} alt={t(monster.name, { capitalize: true })} loading="lazy" />
</div>
<div className="mhwilds-card-body">
<p className="eyebrow">{t(monster.type, { capitalize: true })}</p>
<h2>{t(monster.name, { capitalize: true })}</h2>
<IconRow label={t("weaknesses", { capitalize: true })} values={weaknesses} t={t} />
<IconRow label={t("ailments", { capitalize: true })} values={ailments.length ? ailments : ["none"]} t={t} />
</div>
</div>
<div className="mhwilds-card-face mhwilds-card-back">
<div className="mhwilds-card-body">
<p className="eyebrow">Détails</p>
<h2>{t(monster.name, { capitalize: true })}</h2>
</div>
<DamageTable rows={monster.damage || []} t={t} />
</div>
</div>
</article>
);
}

View file

@ -0,0 +1,38 @@
const MHWILDS_IMG_PATH = "/static/img/mhwilds";
export function assetPath(name) {
return encodeURI(`${MHWILDS_IMG_PATH}/${name}.png`);
}
export function normalizeText(value) {
return String(value || "").trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\s+/g, " ");
}
export function getConditionValues(conditions = []) {
return [...new Set(conditions.flatMap((condition) => condition.values || []))];
}
export function getUniqueConditionValues(items, property, translate) {
return [...new Set(items.flatMap((item) => getConditionValues(item[property])))]
.filter((value) => value !== "none")
.sort((a, b) => translate(a).localeCompare(translate(b), "fr"));
}
export function getFilteredMhwildsItems({ category, mhwilds, filters, t }) {
const items = category === "monsters" ? mhwilds.monsters : mhwilds.endemic;
const activeFilters = filters[category];
const filterKey = category === "monsters" ? "weaknesses" : "locations";
const selected = activeFilters[filterKey];
const search = normalizeText(activeFilters.name);
return items.filter((item) => {
const nameMatches = !search || normalizeText(t(item.name)).includes(search) || normalizeText(item.name).includes(search);
if (!nameMatches) return false;
if (!selected.length) return true;
const values = getConditionValues(item[filterKey]);
return activeFilters.logic === "or" && selected.length >= 2
? selected.some((value) => values.includes(value))
: selected.every((value) => values.includes(value));
});
}