Files
healer-man/src/ui/DungeonBossCards.tsx
T
2026-09-25 18:46:09 -04:00

65 lines
3.1 KiB
TypeScript

import { lazy, Suspense, useEffect, useRef, useState, type CSSProperties } from "react";
import { BOSS_CARD_RARITIES, BOSS_CARD_RARITY_INFO } from "../game/bossCards";
import type { DungeonBossCard as BossCard } from "../game/dungeonBossCards";
import "./dungeonBossCards.css";
const BossCardPortrait = lazy(() => import("./BossCardPortrait"));
function DungeonBossModel({ card }: { readonly card: BossCard }) {
const container = useRef<HTMLDivElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const element = container.current;
if (!element || !card.model) return;
if (typeof IntersectionObserver === "undefined") {
setVisible(true);
return;
}
const observer = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting), {
rootMargin: "40px",
threshold: 0.01,
});
observer.observe(element);
return () => observer.disconnect();
}, [card.model]);
return <div ref={container} className="dungeon-boss-card__portrait">
{!card.model ? <span className="dungeon-boss-card__model-status">Model unavailable</span>
: visible ? <Suspense fallback={<span className="dungeon-boss-card__model-status">Loading model…</span>}>
<BossCardPortrait model={card.model} name={card.name} installed />
</Suspense> : <span className="dungeon-boss-card__model-status">Loading model…</span>}
</div>;
}
export function DungeonBossCards({ cards, compact = false }: {
readonly cards: readonly BossCard[];
readonly compact?: boolean;
}) {
if (!cards.length) return <p className="dungeon-boss-cards__empty">No boss cards are listed for this dungeon.</p>;
return <div className={`dungeon-boss-cards ${compact ? "dungeon-boss-cards--compact" : ""}`}>
{cards.map((card, index) => {
const highestRarity = card.ownedRarities.at(-1);
const accent = highestRarity ? BOSS_CARD_RARITY_INFO[highestRarity].color : "#9a947f";
return <article key={card.key} className={`dungeon-boss-card ${card.copies ? "is-owned" : ""}`}
style={{ "--boss-card-accent": accent } as CSSProperties}
aria-label={`${card.name}, ${card.copies ? `${card.copies} ${card.copies === 1 ? "card" : "cards"} owned` : "not collected"}`}>
<header><span>Boss {String(index + 1).padStart(2, "0")}</span><strong>{card.copies ? "Collected" : "Uncollected"}</strong></header>
<DungeonBossModel card={card} />
<div className="dungeon-boss-card__body">
<h3>{card.name}</h3>
<p>{card.copies ? `${card.copies} ${card.copies === 1 ? "copy" : "copies"} owned` : "Card not collected"}</p>
</div>
<footer aria-label="Card rarities">
{BOSS_CARD_RARITIES.map((rarity) => <span key={rarity}
className={card.ownedRarities.includes(rarity) ? "is-owned" : ""}
style={{ "--rarity-color": BOSS_CARD_RARITY_INFO[rarity].color } as CSSProperties}
title={`${BOSS_CARD_RARITY_INFO[rarity].label}: ${card.ownedRarities.includes(rarity) ? "owned" : "not owned"}`}>
{rarity.charAt(0).toUpperCase()}
</span>)}
</footer>
</article>;
})}
</div>;
}