Files
com.gregkilmer.www/services/site/src/components/library/GameCard.astro
T
Claude 57721efbcd Link roundup posts from Shelf cards and keep long link lists short
Shelf game cards show the post about the game, as Library rows already
do. A make's chip prefers its own build log and says Blog for other
posts. Post chips and Connected pages count the links past the first
few, so a roundup linking a hundred items stays readable.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
2026-10-18 17:09:43 +00:00

170 lines
4.6 KiB
Plaintext

---
/**
* One board game on The Shelf: box art (or star-chart art), how it plays,
* title, blurb, its states and a link to BoardGameGeek. The card is the
* game's anchor (`#slug`); the data-* attributes feed the filter and sort
* script.
*/
import Card from "@gk/ui/components/Card.astro";
import Star from "@gk/ui/components/Star.astro";
import StarChartArt from "@gk/ui/components/StarChartArt.astro";
import StatusPill from "@gk/ui/components/StatusPill.astro";
import StatusChip from "../StatusChip.astro";
import { formatDate, mediaSrc, type SiteIndex } from "../../lib/site.ts";
import { gamePlayers, gamePlayTime, minutesOf, playersOf, type Game } from "../../lib/library.ts";
interface Props {
game: Game;
site: SiteIndex;
hidden?: boolean;
}
const { game, site, hidden } = Astro.props;
const d = game.data;
const plays = d.howItPlays ?? [];
const status = site.statusFor(game);
// Players and play time sync from BoardGameGeek; nothing shows until they have.
const facts = [gamePlayers(game), gamePlayTime(game)].filter(Boolean);
const seats = playersOf(game);
const post = site.backlinks(game, { type: "post" })[0];
const postHref = post && site.href(post);
const hasPost = !!post;
---
<li
id={game.slug}
class="game"
data-p={plays.map((p) => p.toLowerCase()).join("|") || undefined}
data-o={d.owned ? "1" : undefined}
data-w={d.wantToPlay ? "1" : undefined}
data-f={d.favorite ? "1" : undefined}
data-lp={d.lastPlayed}
data-c={d.created}
data-pl={seats?.join("-")}
data-t={minutesOf(game)}
data-b={hasPost ? "1" : undefined}
hidden={hidden}
>
<Card as="div" class="game-card">
<StarChartArt class="box" ratio="1 / 1" seal="tria-prima" src={mediaSrc(d.cover)} alt="" />
<div class="body">
{plays.length > 0 && <span class="plays">{plays.join(" · ")}</span>}
<h3 class="t">
{game.title}
{d.favorite && <><Star size={12} /><span class="gk-visually-hidden">Favorite</span></>}
</h3>
<p class="blurb">{d.blurb}</p>
{facts.length > 0 && <p class="facts">{facts.join(" · ")}</p>}
{
(status || d.wantToPlay || !d.owned || d.lastPlayed) && (
<div class="states">
{status && <StatusChip status={status} site={site} />}
{d.wantToPlay && <StatusPill kind="state">Want to play</StatusPill>}
{!d.owned && <StatusPill kind="neutral">Not owned</StatusPill>}
{d.lastPlayed && <span class="played">Last played {formatDate(d.lastPlayed)}</span>}
</div>
)
}
<span class="links">
{
post && postHref && (
<a href={postHref} class="blog">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5h16v14H4zM8 9h8M8 13h5" /></svg>
Blog: {post.title}
</a>
)
}
<a class="gk-text gk-quiet bgg" href={`https://boardgamegeek.com/boardgame/${d.bggId}`} rel="external noopener">
BoardGameGeek<span class="gk-visually-hidden">: {game.title}</span>
</a>
</span>
</div>
</Card>
</li>
<style>
.game {
display: flex;
scroll-margin-top: 24px;
}
.game[hidden] {
display: none;
}
.game :global(.game-card) {
flex: 1;
padding: 0;
gap: 0;
overflow: hidden;
}
.game:target :global(.game-card) {
border-color: var(--gk-gold);
}
.game :global(.box) {
border-radius: 0;
border-bottom: 1px solid var(--gk-lines);
}
.body {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
padding: 14px;
}
.plays {
font-size: 11px;
letter-spacing: 0.16em;
font-weight: 600;
text-transform: uppercase;
color: var(--gk-muted);
}
.t {
margin: 0;
display: flex;
align-items: center;
gap: 6px;
font-size: 15px;
line-height: 1.3;
letter-spacing: 0.02em;
}
.blurb {
margin: 0;
font-size: 13px;
line-height: 1.45;
color: var(--gk-muted);
}
.facts {
margin: 0;
font-size: 12px;
color: var(--gk-soft);
}
.states {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
margin-top: 2px;
}
.played {
font-size: 12px;
color: var(--gk-muted);
}
.links {
margin-top: auto;
padding-top: 4px;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 6px;
}
.blog {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: var(--gk-teal-bright);
}
.bgg {
align-self: flex-start;
font-size: 12px;
}
</style>