change json format & add diablo4
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 7s
This commit is contained in:
parent
e65c74ea0a
commit
34523e78b1
33 changed files with 5849 additions and 3431 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import { GamesPage } from "./GamesPage.jsx";
|
||||
import { Diablo4Page } from "./diablo4/Diablo4Page.jsx";
|
||||
import { MhwildsPage } from "./mhwilds/MhwildsPage.jsx";
|
||||
|
||||
export function GameRoute(props) {
|
||||
|
|
@ -7,6 +8,7 @@ export function GameRoute(props) {
|
|||
const heroStyle = game?.images?.heroBg ? { "--game-hero-image": `url("${game.images.heroBg}")` } : undefined;
|
||||
|
||||
if (!game) return <GamesPage {...props} />;
|
||||
if (game.id === "diablo4") return <Diablo4Page {...props} game={game} />;
|
||||
if (game.id === "mhwilds") return <MhwildsPage {...props} game={game} />;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -20,7 +20,11 @@ export function GamesPage({ siteContent, games, gamesError }) {
|
|||
title={`Ouvrir ${game.title}`}
|
||||
style={{ "--game-cover": game.cover || "var(--gradient-nebula)" }}
|
||||
>
|
||||
<img src={game.images?.cardCover || game.image || ""} alt={game.title} loading="lazy" />
|
||||
{game.images?.cardCover || game.image ? (
|
||||
<img src={game.images?.cardCover || game.image} alt={game.title} loading="lazy" />
|
||||
) : (
|
||||
<span className="game-card-placeholder" aria-hidden="true">{game.title}</span>
|
||||
)}
|
||||
</a>
|
||||
<div className="card-body">
|
||||
<p className="eyebrow">{game.eyebrow || "Guide de jeu"}</p>
|
||||
|
|
|
|||
22
website/src/features/games/diablo4/Diablo4AffixCard.jsx
Normal file
22
website/src/features/games/diablo4/Diablo4AffixCard.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { getCategoryIconStyle, getCategoryLabel } from "./utils.js";
|
||||
|
||||
export function Diablo4AffixCard({ affix, categoryMap }) {
|
||||
return (
|
||||
<article className="mhwilds-card diablo4-affix-card">
|
||||
<div className="diablo4-affix-card-body">
|
||||
<h2>{affix.label}</h2>
|
||||
<div className="diablo4-category-list" aria-label="Catégories">
|
||||
{(affix.categories || []).map((categoryId) => {
|
||||
const category = categoryMap[categoryId] || categoryId;
|
||||
return (
|
||||
<span className={`tone-${category.tone || "default"}`} key={categoryId}>
|
||||
<span className="diablo4-category-icon" aria-hidden="true" style={getCategoryIconStyle(category)} />
|
||||
{getCategoryLabel(category)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
58
website/src/features/games/diablo4/Diablo4Filters.jsx
Normal file
58
website/src/features/games/diablo4/Diablo4Filters.jsx
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import { Icon } from "../../../components/Icon.jsx";
|
||||
import { getCategoryIconStyle, getCategoryId, getCategoryLabel } from "./utils.js";
|
||||
|
||||
export function Diablo4Filters({ options, filters, setFilters }) {
|
||||
const selected = new Set(filters.categories || []);
|
||||
const logicLabel = filters.logic === "or" ? "Ou" : "Et";
|
||||
|
||||
return (
|
||||
<div className="filter-panel">
|
||||
<div className="filter-panel-head">
|
||||
<div>
|
||||
<p className="eyebrow">Filtres</p>
|
||||
<h2>Catégories</h2>
|
||||
</div>
|
||||
<button
|
||||
className="filter-reset-button"
|
||||
onClick={() => setFilters((state) => ({ ...state, diablo4: { name: "", categories: [], logic: "and" } }))}
|
||||
aria-label="Réinitialiser"
|
||||
title="Réinitialiser"
|
||||
>
|
||||
<Icon name="rubber" />
|
||||
</button>
|
||||
</div>
|
||||
<label className="field compact">
|
||||
<span>Nom</span>
|
||||
<input
|
||||
value={filters.name}
|
||||
placeholder="Rechercher..."
|
||||
onChange={(event) => setFilters((state) => ({ ...state, diablo4: { ...state.diablo4, name: event.target.value } }))}
|
||||
/>
|
||||
</label>
|
||||
<div className="filter-logic">
|
||||
<span>Correspondance</span>
|
||||
<button onClick={() => setFilters((state) => ({ ...state, diablo4: { ...state.diablo4, logic: state.diablo4.logic === "and" ? "or" : "and" } }))}>
|
||||
{logicLabel}
|
||||
</button>
|
||||
</div>
|
||||
<div className="filter-options legacy-scrollbar">
|
||||
{options.map((option) => (
|
||||
<label className={`filter-chip diablo4-filter-chip tone-${option.tone || "default"} ${selected.has(getCategoryId(option)) ? "active" : ""}`} key={getCategoryId(option)}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.has(getCategoryId(option))}
|
||||
onChange={(event) => setFilters((state) => {
|
||||
const values = new Set(state.diablo4.categories || []);
|
||||
if (event.target.checked) values.add(getCategoryId(option));
|
||||
else values.delete(getCategoryId(option));
|
||||
return { ...state, diablo4: { ...state.diablo4, categories: [...values] } };
|
||||
})}
|
||||
/>
|
||||
<span className="diablo4-category-icon" aria-hidden="true" style={getCategoryIconStyle(option)} />
|
||||
<span>{getCategoryLabel(option)}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
45
website/src/features/games/diablo4/Diablo4Listing.jsx
Normal file
45
website/src/features/games/diablo4/Diablo4Listing.jsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { useMemo } from "react";
|
||||
import { Diablo4AffixCard } from "./Diablo4AffixCard.jsx";
|
||||
import { Diablo4Filters } from "./Diablo4Filters.jsx";
|
||||
import { getCategoryLabel, getFilteredDiablo4Affixes } from "./utils.js";
|
||||
|
||||
export function Diablo4Listing({ diablo4, filters, setFilters }) {
|
||||
const activeFilters = filters.diablo4;
|
||||
const options = [...(diablo4.filterOptions.affixes || [])].sort((a, b) => getCategoryLabel(a).localeCompare(getCategoryLabel(b), "fr"));
|
||||
const visible = useMemo(
|
||||
() => getFilteredDiablo4Affixes({ affixes: diablo4.affixes, filters: activeFilters }),
|
||||
[diablo4.affixes, activeFilters]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="page-heading mhwilds-heading">
|
||||
<div>
|
||||
<p className="eyebrow">Diablo IV</p>
|
||||
<div className="mhwilds-title-row">
|
||||
<h1>Affixes</h1>
|
||||
<span className="results-count">{visible.length} / {diablo4.affixes.length}</span>
|
||||
</div>
|
||||
<p>Filtrez les affixes par nom et catégories pour retrouver rapidement les statistiques utiles à votre build.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="mhwilds-layout diablo4-layout" data-game-category="diablo4-affixes">
|
||||
<aside className="mhwilds-filters">
|
||||
<Diablo4Filters options={options} filters={activeFilters} setFilters={setFilters} />
|
||||
</aside>
|
||||
<section className="mhwilds-results" aria-live="polite">
|
||||
<div className="mhwilds-grid diablo4-affix-grid">
|
||||
{visible.length ? visible.map((affix) => (
|
||||
<Diablo4AffixCard key={affix.id} affix={affix} categoryMap={diablo4.categoryMap} />
|
||||
)) : (
|
||||
<div className="empty">
|
||||
<h2>Aucun résultat</h2>
|
||||
<p>Ajustez la recherche ou réinitialisez les filtres actifs.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
22
website/src/features/games/diablo4/Diablo4Overview.jsx
Normal file
22
website/src/features/games/diablo4/Diablo4Overview.jsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export function Diablo4Overview({ game }) {
|
||||
const heroStyle = game?.images?.heroBg ? { "--game-hero-image": `url("${game.images.heroBg}")` } : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="game-hero" style={heroStyle}>
|
||||
<div>
|
||||
<p className="eyebrow">Guide de jeu</p>
|
||||
<h1>Diablo IV</h1>
|
||||
<p>Consultez rapidement les affixes et leurs catégories pour préparer vos builds sans quitter votre session.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="mhwilds-home-grid diablo4-home-grid">
|
||||
<a className="feature mhwilds-home-card diablo4-home-card" href="#/games/diablo4/affixes">
|
||||
<span className="diablo4-home-mark" aria-hidden="true" />
|
||||
<strong>Affixes</strong>
|
||||
<span>Recherche et filtres par catégorie pour retrouver les statistiques utiles.</span>
|
||||
</a>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
38
website/src/features/games/diablo4/Diablo4Page.jsx
Normal file
38
website/src/features/games/diablo4/Diablo4Page.jsx
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { Diablo4Listing } from "./Diablo4Listing.jsx";
|
||||
import { Diablo4Overview } from "./Diablo4Overview.jsx";
|
||||
|
||||
export function Diablo4Page({ category, game, diablo4, filters, setFilters }) {
|
||||
const activeCategory = category === "affixes" ? category : "";
|
||||
const heroStyle = game?.images?.heroBg ? { "--game-hero-image": `url("${game.images.heroBg}")` } : undefined;
|
||||
|
||||
if (!diablo4.loaded) {
|
||||
return (
|
||||
<>
|
||||
<section className="game-hero" style={heroStyle}>
|
||||
<div>
|
||||
<p className="eyebrow">Diablo IV</p>
|
||||
<h1>Diablo IV</h1>
|
||||
<p>Chargement des affixes et filtres associés.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section className="empty">
|
||||
<h2>Chargement</h2>
|
||||
<p>Préparation des données locales...</p>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (diablo4.error) {
|
||||
return (
|
||||
<section className="empty">
|
||||
<h1>Impossible de charger Diablo IV</h1>
|
||||
<p>{diablo4.error}</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (!activeCategory) return <Diablo4Overview game={game} />;
|
||||
|
||||
return <Diablo4Listing diablo4={diablo4} filters={filters} setFilters={setFilters} />;
|
||||
}
|
||||
36
website/src/features/games/diablo4/utils.js
Normal file
36
website/src/features/games/diablo4/utils.js
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
export function normalizeText(value) {
|
||||
return String(value || "").trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
export function formatCategory(value) {
|
||||
return String(value || "").replace(/_/g, " ");
|
||||
}
|
||||
|
||||
export function getCategoryId(category) {
|
||||
return typeof category === "string" ? category : category?.id || "";
|
||||
}
|
||||
|
||||
export function getCategoryLabel(category) {
|
||||
return typeof category === "string" ? formatCategory(category) : category?.label || formatCategory(category?.id);
|
||||
}
|
||||
|
||||
export function getCategoryIconStyle(category) {
|
||||
const icon = typeof category === "string" ? "" : category?.icon;
|
||||
return icon ? { "--diablo4-category-icon": `url("/static/img/diablo4/${icon}.svg")` } : undefined;
|
||||
}
|
||||
|
||||
export function getFilteredDiablo4Affixes({ affixes, filters }) {
|
||||
const selected = filters.categories || [];
|
||||
const search = normalizeText(filters.name);
|
||||
|
||||
return affixes.filter((affix) => {
|
||||
const nameMatches = !search || normalizeText(affix.label).includes(search) || normalizeText(affix.id).includes(search);
|
||||
if (!nameMatches) return false;
|
||||
if (!selected.length) return true;
|
||||
|
||||
const categories = affix.categories || [];
|
||||
return filters.logic === "or" && selected.length >= 2
|
||||
? selected.some((category) => categories.includes(category))
|
||||
: selected.every((category) => categories.includes(category));
|
||||
});
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ 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 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]) }));
|
||||
|
||||
|
|
|
|||
|
|
@ -2,15 +2,17 @@ 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";
|
||||
import { getFilteredMhwildsItems, getMhwildsFilterKey, getUniqueConditionValues } from "./utils.js";
|
||||
|
||||
export function MhwildsListing({ category, mhwilds, filters, setFilters, t }) {
|
||||
const isMonsters = category === "monsters";
|
||||
const filterKey = isMonsters ? "weaknesses" : "locations";
|
||||
const filterKey = getMhwildsFilterKey(category, mhwilds);
|
||||
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 });
|
||||
const options = (mhwilds.filterOptions?.[category]?.length ? mhwilds.filterOptions[category] : getUniqueConditionValues(items, filterKey, t))
|
||||
.filter((value) => value !== "none")
|
||||
.sort((a, b) => t(a).localeCompare(t(b), "fr"));
|
||||
const visible = useMemo(() => getFilteredMhwildsItems({ category, filterKey, mhwilds, filters, t }), [category, filterKey, mhwilds, filters, t]);
|
||||
const label = isMonsters ? t("monsters", { capitalize: true }) : t("endemic_life", { capitalize: true });
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,23 @@
|
|||
import { assetPath } from "../utils.js";
|
||||
|
||||
const PHYSICAL_COLUMNS = ["cut", "blunt", "ammo"];
|
||||
const ELEMENTAL_COLUMNS = ["fire", "water", "thunder", "ice", "dragon"];
|
||||
const COLUMNS = ["name", ...PHYSICAL_COLUMNS, ...ELEMENTAL_COLUMNS];
|
||||
|
||||
function hitzoneValue(row, column) {
|
||||
if (column === "name") return row.name;
|
||||
return row.physical?.[column] ?? row.elemental?.[column];
|
||||
}
|
||||
|
||||
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) => (
|
||||
{COLUMNS.map((column) => (
|
||||
<th key={column}>
|
||||
{column === "name" ? "" : <img src={assetPath(column)} alt={t(column, { capitalize: true })} title={t(column, { capitalize: true })} />}
|
||||
</th>
|
||||
|
|
@ -20,10 +27,10 @@ export function DamageTable({ rows, t }) {
|
|||
<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>
|
||||
{COLUMNS.map((column) => column === "name" ? (
|
||||
<td key={column} title={t(row.name, { capitalize: true })}>{t(row.name, { capitalize: true })}</td>
|
||||
) : (
|
||||
<td key={column}><img src={assetPath(`${row[column]}-stars`)} alt={`${row[column]} étoiles`} /></td>
|
||||
<td key={column}><img src={assetPath(`${hitzoneValue(row, column)}-stars`)} alt={`${hitzoneValue(row, column)} étoiles`} /></td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,10 @@ 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");
|
||||
const normalizedDescription = normalizeText(description);
|
||||
const hasDescription = normalizedDescription
|
||||
&& normalizedDescription !== normalizeText("à compléter")
|
||||
&& normalizedDescription !== normalizeText("to be completed");
|
||||
|
||||
return (
|
||||
<article className="mhwilds-card endemic-card">
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export function MonsterCard({ monster, t }) {
|
|||
<p className="eyebrow">Détails</p>
|
||||
<h2>{t(monster.name, { capitalize: true })}</h2>
|
||||
</div>
|
||||
<DamageTable rows={monster.damage || []} t={t} />
|
||||
<DamageTable rows={monster.hitzones || []} t={t} />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
const MHWILDS_IMG_PATH = "/static/img/mhwilds";
|
||||
|
||||
export function assetPath(name) {
|
||||
return encodeURI(`${MHWILDS_IMG_PATH}/${name}.png`);
|
||||
return encodeURI(`${MHWILDS_IMG_PATH}/${String(name || "").replace(/_/g, " ")}.png`);
|
||||
}
|
||||
|
||||
export function normalizeText(value) {
|
||||
|
|
@ -18,11 +18,16 @@ export function getUniqueConditionValues(items, property, translate) {
|
|||
.sort((a, b) => translate(a).localeCompare(translate(b), "fr"));
|
||||
}
|
||||
|
||||
export function getFilteredMhwildsItems({ category, mhwilds, filters, t }) {
|
||||
export function getMhwildsFilterKey(category, mhwilds) {
|
||||
const optionKey = mhwilds.filterOptionKeys?.[category];
|
||||
if (optionKey === "elements") return "weaknesses";
|
||||
return optionKey || (category === "monsters" ? "weaknesses" : "locations");
|
||||
}
|
||||
|
||||
export function getFilteredMhwildsItems({ category, filterKey, 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 selected = activeFilters[filterKey] || [];
|
||||
const search = normalizeText(activeFilters.name);
|
||||
|
||||
return items.filter((item) => {
|
||||
|
|
|
|||
|
|
@ -594,10 +594,29 @@ 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: [] });
|
||||
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 [filters, setFilters] = useState({
|
||||
monsters: { name: "", weaknesses: [], logic: "and" },
|
||||
endemic: { name: "", locations: [], logic: "and" }
|
||||
endemic: { name: "", locations: [], logic: "and" },
|
||||
diablo4: { name: "", categories: [], logic: "and" }
|
||||
});
|
||||
const [confirmModal, setConfirmModal] = useState(null);
|
||||
const [createModal, setCreateModal] = useState(null);
|
||||
|
|
@ -636,19 +655,68 @@ function App() {
|
|||
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 || [])]
|
||||
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: [] }));
|
||||
}).catch((error) => setMhwilds({
|
||||
loaded: true,
|
||||
loading: false,
|
||||
error: error.message,
|
||||
translations: {},
|
||||
monsters: [],
|
||||
endemic: [],
|
||||
filterOptions: { monsters: [], endemic: [] },
|
||||
filterOptionKeys: { monsters: "", endemic: "" }
|
||||
}));
|
||||
}, [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: "" }
|
||||
}));
|
||||
}, [route, diablo4.loaded, diablo4.loading]);
|
||||
|
||||
const t = (key, { capitalize = false } = {}) => {
|
||||
const value = mhwilds.translations[key] || key;
|
||||
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);
|
||||
|
|
@ -829,6 +897,7 @@ function App() {
|
|||
games={games}
|
||||
gamesError={gamesError}
|
||||
mhwilds={mhwilds}
|
||||
diablo4={diablo4}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
t={t}
|
||||
|
|
|
|||
188
website/src/styles/_diablo4.scss
Normal file
188
website/src/styles/_diablo4.scss
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
.game-card-placeholder {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: inherit;
|
||||
place-items: center;
|
||||
background:
|
||||
radial-gradient(circle at 78% 22%, rgba(246, 196, 83, 0.18), transparent 28%),
|
||||
radial-gradient(circle at 22% 70%, rgba(139, 92, 246, 0.28), transparent 38%),
|
||||
linear-gradient(135deg, rgba(16, 20, 38, 0.92), rgba(53, 18, 45, 0.76));
|
||||
color: var(--color-text-primary);
|
||||
font-size: clamp(1.8rem, 5vw, 3.3rem);
|
||||
font-weight: 900;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.diablo4-home-mark {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
margin: 0;
|
||||
place-items: center;
|
||||
border-bottom: 1px solid rgba(246, 196, 83, 0.42);
|
||||
background: rgba(5, 7, 17, 0.28);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.mhwilds-home-card .diablo4-home-mark {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.diablo4-home-mark::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
background: currentColor;
|
||||
filter: drop-shadow(0 16px 24px rgba(0, 0, 0, 0.44));
|
||||
mask: url("/static/img/diablo4/cube.svg") center / contain no-repeat;
|
||||
}
|
||||
|
||||
.diablo4-affix-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
|
||||
}
|
||||
|
||||
.diablo4-affix-card {
|
||||
min-height: 158px;
|
||||
}
|
||||
|
||||
.diablo4-affix-card-body {
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.diablo4-affix-card h2 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.diablo4-category-list {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 4px;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.diablo4-category-list > span {
|
||||
display: inline-flex;
|
||||
min-height: 23px;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
border: 1px solid rgba(246, 196, 83, 0.22);
|
||||
border-radius: var(--radius-pill);
|
||||
background: rgba(246, 196, 83, 0.08);
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
padding: 2px 6px;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.diablo4-filter-chip {
|
||||
grid-template-columns: 18px 22px 1fr;
|
||||
}
|
||||
|
||||
.diablo4-filter-chip span {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.diablo4-category-icon {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex: 0 0 18px;
|
||||
background: currentColor;
|
||||
mask: var(--diablo4-category-icon) center / contain no-repeat;
|
||||
}
|
||||
|
||||
.diablo4-filter-chip .diablo4-category-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-basis: 22px;
|
||||
}
|
||||
|
||||
.diablo4-category-list .diablo4-category-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-basis: 12px;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.tone-purple .diablo4-category-icon {
|
||||
color: #c4b5fd;
|
||||
}
|
||||
|
||||
.diablo4-category-list .tone-purple {
|
||||
border-color: rgba(168, 85, 247, 0.3);
|
||||
background: rgba(168, 85, 247, 0.1);
|
||||
}
|
||||
|
||||
.tone-gold .diablo4-category-icon {
|
||||
color: #f6c453;
|
||||
}
|
||||
|
||||
.diablo4-category-list .tone-gold {
|
||||
border-color: rgba(246, 196, 83, 0.34);
|
||||
background: rgba(246, 196, 83, 0.1);
|
||||
}
|
||||
|
||||
.tone-blue .diablo4-category-icon {
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.diablo4-category-list .tone-blue {
|
||||
border-color: rgba(96, 165, 250, 0.34);
|
||||
background: rgba(96, 165, 250, 0.1);
|
||||
}
|
||||
|
||||
.tone-yellow .diablo4-category-icon {
|
||||
color: #fde047;
|
||||
}
|
||||
|
||||
.diablo4-category-list .tone-yellow {
|
||||
border-color: rgba(250, 204, 21, 0.34);
|
||||
background: rgba(250, 204, 21, 0.1);
|
||||
}
|
||||
|
||||
.tone-red .diablo4-category-icon {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.diablo4-category-list .tone-red {
|
||||
border-color: rgba(248, 113, 113, 0.34);
|
||||
background: rgba(248, 113, 113, 0.1);
|
||||
}
|
||||
|
||||
.tone-violet .diablo4-category-icon {
|
||||
color: #a78bfa;
|
||||
}
|
||||
|
||||
.diablo4-category-list .tone-violet {
|
||||
border-color: rgba(139, 92, 246, 0.34);
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
|
||||
.tone-green .diablo4-category-icon {
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.diablo4-category-list .tone-green {
|
||||
border-color: rgba(74, 222, 128, 0.34);
|
||||
background: rgba(74, 222, 128, 0.1);
|
||||
}
|
||||
|
||||
.diablo4-category-list .tone-purple .diablo4-category-icon,
|
||||
.diablo4-category-list .tone-gold .diablo4-category-icon,
|
||||
.diablo4-category-list .tone-blue .diablo4-category-icon,
|
||||
.diablo4-category-list .tone-yellow .diablo4-category-icon,
|
||||
.diablo4-category-list .tone-red .diablo4-category-icon,
|
||||
.diablo4-category-list .tone-violet .diablo4-category-icon,
|
||||
.diablo4-category-list .tone-green .diablo4-category-icon {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
.game-card .card-cover {
|
||||
display: block;
|
||||
height: 170px;
|
||||
min-height: 170px;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
.mhwilds-home-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(240px, 380px));
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 380px));
|
||||
justify-content: center;
|
||||
gap: var(--space-4);
|
||||
margin-top: var(--space-6);
|
||||
|
|
|
|||
|
|
@ -9,4 +9,5 @@
|
|||
@use "toolboxes";
|
||||
@use "overlays";
|
||||
@use "mhwilds";
|
||||
@use "diablo4";
|
||||
@use "responsive";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue