mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-07 19:58:24 +00:00
57721efbcd
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
170 lines
4.6 KiB
Plaintext
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>
|