sokko-g/website/src/features/games/aniimo/AniimoCard.jsx
Shinuwa 1ee460b94f
All checks were successful
Deploy Sokko G / deploy (push) Successful in 17s
Display searchable Aniimo numbers on cards
2026-09-25 20:21:14 +02:00

114 lines
7.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Rôle : affiche une carte Aniimo retournable et les onglets des variantes correspondantes.
import { useId, useLayoutEffect, useRef, useState } from "react";
import { GuideChecklistCheckbox, GuideChecklistMark } from "../GuideChecklistControls.jsx";
import { Icon } from "../../../components/Icon.jsx";
import { Tooltip } from "../../../components/Tooltip.jsx";
import { ANIIMO_STATS, aniimoAsset } from "./utils.js";
function Attributes({ values, t }) {
return <span className="aniimo-attributes">{values.map((value) => <span className="nds-badge" data-variant="neutral" key={value} title={t(value)}><img src={aniimoAsset(value)} alt="" width="22" height="22" /><span>{t(value)}</span></span>)}</span>;
}
function shortForm(label) {
const words = label.split(/[\s’-]+/).filter((word) => !/^(forme|de|des|du|la|le|les|d)$/i.test(word));
// Trois lettres distinguent notamment les plateaux, prairies et prismana.
return (words.length === 1 ? words[0].slice(0, 3) : words.map((word) => word[0]).join("")).toLocaleUpperCase("fr");
}
function CardFaces({ variant, t, content, checklist, statsMax }) {
const completed = checklist.isCompleted(variant.key);
const [flipped, setFlipped] = useState(false);
const frontButton = useRef(null);
const backButton = useRef(null);
const focusAfterFlip = useRef(false);
useLayoutEffect(() => {
if (!focusAfterFlip.current) return;
(flipped ? backButton : frontButton).current?.focus({ preventScroll: true });
focusAfterFlip.current = false;
}, [flipped]);
function flip() {
focusAfterFlip.current = true;
setFlipped((value) => !value);
}
const flipButton = (back, ref) => <button ref={ref} type="button" className="aniimo-flip nds-icon-button" aria-pressed={back} aria-label={`${back ? content.showFront : content.showBack} — ${t(variant.name)}, ${t(variant.form)}`} title={back ? content.showFront : content.showBack} onClick={flip}><Icon name="flip" /></button>;
return (
<div className={`aniimo-card ${flipped ? "is-flipped" : ""}`}>
<div className="aniimo-card-inner">
<div className="aniimo-face aniimo-front nds-card" aria-hidden={flipped} inert={flipped}>
{flipButton(false, frontButton)}
<div className="aniimo-portrait">
<img src={variant.image} alt={t(variant.name)} width="512" height="840" loading="lazy" />
<span className="aniimo-number">N° {variant.aniimoId}</span>
<GuideChecklistCheckbox checklist={checklist} itemKey={variant.key} />
</div>
<div className="aniimo-card-body">
<h2 title={t(variant.name)}>{t(variant.name)}</h2>
<p className="aniimo-form" title={t(variant.form)}>{t(variant.form)}</p>
<span className="aniimo-element-icons">{variant.elements.map((element) => <Tooltip key={element} content={t(element)}><img src={aniimoAsset(element)} alt={t(element)} width="24" height="24" /></Tooltip>)}</span>
</div>
</div>
<div className="aniimo-face aniimo-back nds-card" aria-hidden={!flipped} inert={!flipped}>
{flipButton(true, backButton)}
<GuideChecklistMark completed={completed} label={checklist.content.completed} />
<div className="aniimo-card-body aniimo-back-heading">
<h2 title={t(variant.name)}>{t(variant.name)}</h2>
<p className="aniimo-form" title={t(variant.form)}>{t(variant.form)}</p>
</div>
<div className="aniimo-card-body aniimo-back-details nds-scrollbox" data-variant="ghost" role="region" aria-label={`${content.showBack} — ${t(variant.name)}`} tabIndex="0">
<div className="aniimo-back-summary">
<div><h3>{content.elements}</h3><Attributes values={variant.elements} t={t} /></div>
<div><h3>{content.role}</h3><Attributes values={[variant.role]} t={t} /></div>
</div>
<h3>{content.abilities}</h3>
<ul className="aniimo-abilities">{variant.homeland_abilities.map((ability) => <li key={ability.type}>
<img src={aniimoAsset(ability.type)} alt="" width="24" height="24" />
<span>{t(ability.type)}</span>
<span className="nds-badge" data-variant="gold">{content.level} {ability.level}</span>
</li>)}</ul>
<h3>{content.statsTitle}</h3>
<div className="aniimo-stats">
{ANIIMO_STATS.map((key) => <div className="aniimo-stat" key={key}>
<span title={content.stats[key]}>{content.stats[key]}</span>
<span className="nds-progress" data-size="sm" role="progressbar" aria-label={content.stats[key]} aria-valuemin={0} aria-valuemax={statsMax} aria-valuenow={variant.attributes.stats[key]}>
<span style={{ width: `${variant.attributes.stats[key] / statsMax * 100}%` }} />
</span>
<span className="aniimo-stat-value">{variant.attributes.stats[key]}</span>
</div>)}
<div className="aniimo-stat-total"><span>{content.statsTotal}</span><strong>{variant.attributes.total}</strong></div>
</div>
</div>
</div>
</div>
</div>
);
}
export function AniimoCard({ variants, t, content, checklist, statsMax }) {
const id = useId();
const [selectedKey, setSelectedKey] = useState(variants[0].key);
const variant = variants.find((item) => item.key === selectedKey) || variants[0];
// Mémoriser le repli pour ne pas restaurer un ancien onglet lorsque le filtre est retiré.
if (selectedKey !== variant.key) setSelectedKey(variant.key);
const multiple = variants.length > 1;
function selectTab(index, target) {
setSelectedKey(variants[index].key);
target.parentElement.children[index].focus();
}
return (
<article className="aniimo-result" data-checklist-complete={checklist.isCompleted(variant.key) || undefined} data-aniimo={variant.aniimoId} data-form={variant.form} aria-label={t(variant.name)}>
<div className="aniimo-tabs-slot">
{multiple && <div className="aniimo-tabs nds-radio-button-group" data-size="xs" role="tablist" aria-label={`${content.variants} — ${t(variant.name)}`}>
{variants.map((item, index) => <Tooltip as="button" content={t(item.form)} aria-label={t(item.form)} key={item.key} type="button" role="tab" data-form={item.form} id={`${id}-${item.form}`} aria-controls={`${id}-panel`} aria-selected={item.key === variant.key} tabIndex={item.key === variant.key ? 0 : -1} className={item.key === variant.key ? "active" : ""} onClick={() => setSelectedKey(item.key)} onKeyDown={(event) => {
const next = event.key === "ArrowRight" ? (index + 1) % variants.length : event.key === "ArrowLeft" ? (index - 1 + variants.length) % variants.length : event.key === "Home" ? 0 : event.key === "End" ? variants.length - 1 : null;
if (next === null) return;
event.preventDefault();
selectTab(next, event.currentTarget);
}}>{shortForm(t(item.form))}</Tooltip>)}
</div>}
</div>
<div id={`${id}-panel`} role={multiple ? "tabpanel" : undefined} aria-labelledby={multiple ? `${id}-${variant.form}` : undefined}>
<CardFaces variant={variant} t={t} content={content} checklist={checklist} statsMax={statsMax} />
</div>
</article>
);
}