mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
TCGs
This commit is contained in:
@@ -234,6 +234,39 @@ It's idempotent — run it again any time you add games to the JSON.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## TCGs (Trading Card Games page)
|
||||||
|
|
||||||
|
The TCGs page (`src/pages/interests/tcgs.astro`) is hand-built, like the other
|
||||||
|
interest pages. Two parts are data-driven:
|
||||||
|
|
||||||
|
- **The games I play** and **Proxy making** are inline frontmatter arrays of
|
||||||
|
`{ icon, title, body }` — edit them directly on the page.
|
||||||
|
- **Magic: The Gathering decks** are pulled **live from Archidekt** on the
|
||||||
|
server (the site is SSR, so no CORS or API key is needed).
|
||||||
|
|
||||||
|
### Archidekt integration
|
||||||
|
|
||||||
|
`src/lib/archidekt.ts` fetches Greg's public decks from:
|
||||||
|
|
||||||
|
```
|
||||||
|
GET https://archidekt.com/api/decks/v3/?ownerUsername=<username>
|
||||||
|
```
|
||||||
|
|
||||||
|
- The username is the `ARCHIDEKT_USERNAME` export (currently `Patiun`). Change
|
||||||
|
it there to point at a different account.
|
||||||
|
- Results are cached to `node_modules/.cache/archidekt-<username>.json` for
|
||||||
|
`CACHE_TTL_MS` (default **1 hour**) so repeat visits are instant and we're
|
||||||
|
gentle on Archidekt. Tune the TTL via that export.
|
||||||
|
- Only public, non-unlisted decks are shown. If Archidekt is unreachable, the
|
||||||
|
section falls back to a "Manage on Archidekt" link — it never breaks the page.
|
||||||
|
|
||||||
|
Each deck renders as a `DeckCard` (featured card art, mana-color pips, card
|
||||||
|
count, format, last-updated) linking to its Archidekt page. To add or remove
|
||||||
|
decks, just change them on Archidekt — the page picks them up automatically
|
||||||
|
(within the cache window).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Interest pages
|
## Interest pages
|
||||||
|
|
||||||
Each interest is a hand-built page in `src/pages/interests/`. They follow the
|
Each interest is a hand-built page in `src/pages/interests/`. They follow the
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 615 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 784 KiB |
@@ -0,0 +1,115 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* DeckCard.astro
|
||||||
|
* ---------------------------------------------------------------------------
|
||||||
|
* Card for a single Magic: The Gathering deck pulled live from Archidekt. Shows
|
||||||
|
* the deck's featured card art (or a placeholder slab), the deck name as a link
|
||||||
|
* to its Archidekt page, a row of mana-color pips sized by color presence, and
|
||||||
|
* a meta line (card count, format, last-updated).
|
||||||
|
*
|
||||||
|
* Data shape matches ArchidektDeck in src/lib/archidekt.ts.
|
||||||
|
*/
|
||||||
|
import Icon from '@components/Icon.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
href: string;
|
||||||
|
featured?: string;
|
||||||
|
colors?: { W: number; U: number; B: number; R: number; G: number };
|
||||||
|
size?: number;
|
||||||
|
formatLabel?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, href, featured, colors, size, formatLabel, updatedAt } = Astro.props;
|
||||||
|
|
||||||
|
// MTG mana colors. Used for the presence pips; a color shows only if count > 0.
|
||||||
|
const MANA: Array<[key: 'W' | 'U' | 'B' | 'R' | 'G', hex: string]> = [
|
||||||
|
['W', '#f8f4e8'],
|
||||||
|
['U', '#3b7bd4'],
|
||||||
|
['B', '#2b2b2b'],
|
||||||
|
['R', '#d44b3b'],
|
||||||
|
['G', '#3f9d52'],
|
||||||
|
];
|
||||||
|
const present = colors ? MANA.filter(([k]) => (colors[k] ?? 0) > 0) : [];
|
||||||
|
|
||||||
|
// Friendly relative-ish date (e.g. "Jul 2026"). Falls back gracefully.
|
||||||
|
const updated = updatedAt ? new Date(updatedAt).toLocaleDateString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
}) : '';
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="card card-interactive group flex h-full flex-col gap-0 overflow-hidden p-0"
|
||||||
|
>
|
||||||
|
<div class="relative aspect-[4/3] w-full overflow-hidden">
|
||||||
|
{
|
||||||
|
featured ? (
|
||||||
|
<img
|
||||||
|
src={featured}
|
||||||
|
alt={`${name} featured card`}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
class="flex h-full w-full items-center justify-center"
|
||||||
|
style="background: color-mix(in srgb, var(--color-secondary) 16%, transparent);"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="cards-playing"
|
||||||
|
class="h-12 w-12 opacity-50"
|
||||||
|
style="color: var(--color-text-muted);"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{/* Legibility gradient at the bottom for the meta row. */}
|
||||||
|
<div
|
||||||
|
aria-hidden="true"
|
||||||
|
class="absolute inset-x-0 bottom-0 h-1/3"
|
||||||
|
style="background: linear-gradient(to top, rgba(0,0,0,0.55), transparent);"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{
|
||||||
|
/* Mana color pips, top-left over the art. */
|
||||||
|
present.length > 0 && (
|
||||||
|
<div class="absolute left-3 top-3 flex gap-1.5">
|
||||||
|
{present.map(([key, hex]) => (
|
||||||
|
<span
|
||||||
|
class="h-3 w-3 rounded-full border border-black/30"
|
||||||
|
style={`background:${hex};`}
|
||||||
|
title={`${key} mana`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2 p-4">
|
||||||
|
<h3
|
||||||
|
class="font-sans text-base leading-tight underline-offset-4 group-hover:underline"
|
||||||
|
style="color: var(--color-text);"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
class="flex flex-wrap items-center gap-2 font-mono text-[0.65rem] uppercase tracking-[0.16em]"
|
||||||
|
style="color: var(--color-text-muted);"
|
||||||
|
>
|
||||||
|
{formatLabel && <span>{formatLabel}</span>}
|
||||||
|
{formatLabel && (size || updated) && <span aria-hidden="true">·</span>}
|
||||||
|
{size ? <span>{size} cards</span> : null}
|
||||||
|
{updated && (size ? <span aria-hidden="true">·</span> : null)}
|
||||||
|
{updated && <span>{updated}</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* archidekt.ts
|
||||||
|
* ---------------------------------------------------------------------------
|
||||||
|
* Server-side helper for pulling Greg's public MTG decks from Archidekt into
|
||||||
|
* the TCGs page. The site is SSR (output: 'server'), so this runs on the
|
||||||
|
* server — no CORS or API key required (Archidekt's decks list endpoint is
|
||||||
|
* public JSON).
|
||||||
|
*
|
||||||
|
* Endpoint: GET https://archidekt.com/api/decks/v3/?ownerUsername=<username>
|
||||||
|
* -> { count, results: [{ id, name, featured, colors, size, deckFormat,
|
||||||
|
* updatedAt, private, unlisted, theorycrafted, ... }] }
|
||||||
|
*
|
||||||
|
* Decks are cached to a filesystem file for `CACHE_TTL_MS` (default 1 hour) so
|
||||||
|
* repeat page loads are instant and we're gentle on Archidekt. The cache is
|
||||||
|
* stale-while-revalidate: if a refresh fails, we serve the stale data rather
|
||||||
|
* than erroring. Every failure path returns an empty list — the page renders a
|
||||||
|
* fallback link and never breaks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Greg's Archidekt username. Change here to point at a different account. */
|
||||||
|
export const ARCHIDEKT_USERNAME = 'Patiun';
|
||||||
|
|
||||||
|
/** How long the cached deck list is considered fresh. Default: 1 hour. */
|
||||||
|
export const CACHE_TTL_MS = 60 * 60 * 1000;
|
||||||
|
|
||||||
|
const API_BASE = 'https://archidekt.com/api/decks/v3';
|
||||||
|
const USER_AGENT = 'GregKilmerProfileSite/1.0 (+https://gregkilmer.com)';
|
||||||
|
|
||||||
|
/** Known Archidekt deckFormat ids → labels. Add as needed. */
|
||||||
|
const FORMAT_LABELS: Record<number, string> = {
|
||||||
|
1: 'Casual',
|
||||||
|
2: 'Modern',
|
||||||
|
3: 'Commander',
|
||||||
|
5: 'Standard',
|
||||||
|
7: 'Legacy',
|
||||||
|
10: 'Pauper',
|
||||||
|
14: 'Historic',
|
||||||
|
16: 'Pioneer',
|
||||||
|
37: 'Oathbreaker',
|
||||||
|
45: 'Brawl',
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ArchidektDeck {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
/** Featured card art URL (may be empty). */
|
||||||
|
featured: string;
|
||||||
|
/** Mana color counts present in the deck. */
|
||||||
|
colors: { W: number; U: number; B: number; R: number; G: number };
|
||||||
|
/** Total card count. */
|
||||||
|
size: number;
|
||||||
|
/** Archidekt format id. */
|
||||||
|
formatId: number;
|
||||||
|
/** Human-readable format label (e.g. "Commander"). */
|
||||||
|
formatLabel: string;
|
||||||
|
/** ISO timestamp of last update. */
|
||||||
|
updatedAt: string;
|
||||||
|
/** Direct link to the deck on Archidekt. */
|
||||||
|
href: string;
|
||||||
|
theorycrafted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatLabel = (id: number): string => FORMAT_LABELS[id] ?? 'Custom';
|
||||||
|
|
||||||
|
interface RawDeck {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
featured?: string;
|
||||||
|
customFeatured?: string;
|
||||||
|
colors?: { W?: number; U?: number; B?: number; R?: number; G?: number };
|
||||||
|
size?: number;
|
||||||
|
deckFormat?: number;
|
||||||
|
updatedAt?: string;
|
||||||
|
private?: boolean;
|
||||||
|
unlisted?: boolean;
|
||||||
|
theorycrafted?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileURL = (username: string): string =>
|
||||||
|
// node_modules/.cache is gitignored and survives across dev reloads.
|
||||||
|
new URL(
|
||||||
|
`../node_modules/.cache/archidekt-${username}.json`,
|
||||||
|
import.meta.url,
|
||||||
|
).href;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the cached decks for `username` if still fresh, else null.
|
||||||
|
* Best-effort: any read/parse error returns null (treated as a miss).
|
||||||
|
*/
|
||||||
|
async function readCache(username: string): Promise<ArchidektDeck[] | null> {
|
||||||
|
try {
|
||||||
|
const { readFile } = await import('node:fs/promises');
|
||||||
|
const raw = await readFile(fileURL(username), 'utf8');
|
||||||
|
const data = JSON.parse(raw) as { fetchedAt: number; decks: ArchidektDeck[] };
|
||||||
|
if (!data?.fetchedAt || !Array.isArray(data.decks)) return null;
|
||||||
|
if (Date.now() - data.fetchedAt > CACHE_TTL_MS) return null;
|
||||||
|
return data.decks;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Writes the cache. Failures are non-fatal (best-effort). */
|
||||||
|
async function writeCache(username: string, decks: ArchidektDeck[]): Promise<void> {
|
||||||
|
try {
|
||||||
|
const { mkdir, writeFile } = await import('node:fs/promises');
|
||||||
|
const { dirname } = await import('node:path');
|
||||||
|
const { fileURLToPath } = await import('node:url');
|
||||||
|
const path = fileURLToPath(fileURL(username));
|
||||||
|
await mkdir(dirname(path), { recursive: true });
|
||||||
|
await writeFile(path, JSON.stringify({ fetchedAt: Date.now(), decks }, null, 2));
|
||||||
|
} catch {
|
||||||
|
/* cache write is best-effort */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reads stale cache regardless of TTL (used as a fallback on fetch failure). */
|
||||||
|
async function readStale(username: string): Promise<ArchidektDeck[] | null> {
|
||||||
|
try {
|
||||||
|
const { readFile } = await import('node:fs/promises');
|
||||||
|
const raw = await readFile(fileURL(username), 'utf8');
|
||||||
|
const data = JSON.parse(raw) as { fetchedAt: number; decks: ArchidektDeck[] };
|
||||||
|
return Array.isArray(data?.decks) ? data.decks : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches Greg's public Archidekt decks, using a ~1h filesystem cache.
|
||||||
|
* Never throws — on any failure returns an empty list (page shows a fallback).
|
||||||
|
*/
|
||||||
|
export async function getDecks(username = ARCHIDEKT_USERNAME): Promise<ArchidektDeck[]> {
|
||||||
|
// 1. Fresh cache hit.
|
||||||
|
const cached = await readCache(username);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
// 2. Live fetch.
|
||||||
|
const url = `${API_BASE}/?ownerUsername=${encodeURIComponent(username)}`;
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { 'User-Agent': USER_AGENT, Accept: 'application/json' },
|
||||||
|
// Don't let a slow Archidekt hang the page render.
|
||||||
|
signal: AbortSignal.timeout(8000),
|
||||||
|
});
|
||||||
|
if (!res.ok) throw new Error(`Archidekt responded ${res.status}`);
|
||||||
|
const data = (await res.json()) as { results?: RawDeck[] };
|
||||||
|
const decks: ArchidektDeck[] = (data.results ?? [])
|
||||||
|
.filter((d) => !d.private && !d.unlisted)
|
||||||
|
.map((d) => ({
|
||||||
|
id: d.id,
|
||||||
|
name: d.name,
|
||||||
|
featured: d.customFeatured || d.featured || '',
|
||||||
|
colors: {
|
||||||
|
W: d.colors?.W ?? 0,
|
||||||
|
U: d.colors?.U ?? 0,
|
||||||
|
B: d.colors?.B ?? 0,
|
||||||
|
R: d.colors?.R ?? 0,
|
||||||
|
G: d.colors?.G ?? 0,
|
||||||
|
},
|
||||||
|
size: d.size ?? 0,
|
||||||
|
formatId: d.deckFormat ?? 0,
|
||||||
|
formatLabel: formatLabel(d.deckFormat ?? 0),
|
||||||
|
updatedAt: d.updatedAt ?? '',
|
||||||
|
href: `https://archidekt.com/fabric/decks/${d.id}`,
|
||||||
|
theorycrafted: Boolean(d.theorycrafted),
|
||||||
|
}));
|
||||||
|
|
||||||
|
await writeCache(username, decks);
|
||||||
|
return decks;
|
||||||
|
} catch {
|
||||||
|
// 3. Stale-while-revalidate fallback, else empty.
|
||||||
|
return (await readStale(username)) ?? [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The Archidekt search-URL for the full decks list (used in fallback links). */
|
||||||
|
export const archidektSearchUrl = (username = ARCHIDEKT_USERNAME): string =>
|
||||||
|
`https://archidekt.com/search/decks?ownerUsername=${encodeURIComponent(username)}`;
|
||||||
@@ -54,6 +54,17 @@ const interests = [
|
|||||||
"Resin dice in a pressure pot, fun and functional prints on the P1s, custom TCG proxies and decks. Passion and utility.",
|
"Resin dice in a pressure pot, fun and functional prints on the P1s, custom TCG proxies and decks. Passion and utility.",
|
||||||
image: "/images/making-resin.jpg",
|
image: "/images/making-resin.jpg",
|
||||||
alt: "Polyhedral dice on a dark bag",
|
alt: "Polyhedral dice on a dark bag",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/interests/tcgs",
|
||||||
|
icon: "cards",
|
||||||
|
title: "Trading Card Games",
|
||||||
|
tagline:
|
||||||
|
"Cardboard, deckbuilding, and the eternal question: one more pack?",
|
||||||
|
blurb:
|
||||||
|
"Magic: The Gathering Commander decks, Pokémon collecting, Riftbound, and the Gundam card game. Plus the proxies and props I make to play them.",
|
||||||
|
image: "/images/cards.jpg",
|
||||||
|
alt: "Trading cards and dice on a playmat",
|
||||||
} /*
|
} /*
|
||||||
{
|
{
|
||||||
href: '/interests/pokemon',
|
href: '/interests/pokemon',
|
||||||
|
|||||||
@@ -56,6 +56,12 @@ const related = [
|
|||||||
icon: "book-open-page-variant",
|
icon: "book-open-page-variant",
|
||||||
blurb: "Displays and props for the stories I love.",
|
blurb: "Displays and props for the stories I love.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: "/interests/tcgs",
|
||||||
|
title: "Trading Card Games",
|
||||||
|
icon: "cards",
|
||||||
|
blurb: "Proxies, tokens, and props are the TCG side of the workshop.",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
---
|
||||||
|
/**
|
||||||
|
* tcgs.astro — Trading Card Games
|
||||||
|
* ---------------------------------------------------------------------------
|
||||||
|
* Per-interest template. Sections: overview narrative, the games I play
|
||||||
|
* (MTG, Pokémon, Riftbound, Gundam), Magic: The Gathering decks pulled live
|
||||||
|
* from Archidekt, and proxy making. Related links to Tabletop and Making.
|
||||||
|
*/
|
||||||
|
import BaseLayout from "@layouts/BaseLayout.astro";
|
||||||
|
import Reveal from "@components/motion/Reveal.astro";
|
||||||
|
import InterestHero from "@components/layout/InterestHero.astro";
|
||||||
|
import InterestCard from "@components/cards/InterestCard.astro";
|
||||||
|
import DeckCard from "@components/cards/DeckCard.astro";
|
||||||
|
import SectionHeader from "@components/layout/SectionHeader.astro";
|
||||||
|
import RelatedInterests from "@components/layout/RelatedInterests.astro";
|
||||||
|
import Placeholder from "@components/layout/Placeholder.astro";
|
||||||
|
import Icon from "@components/Icon.astro";
|
||||||
|
import {
|
||||||
|
getDecks,
|
||||||
|
archidektSearchUrl,
|
||||||
|
ARCHIDEKT_USERNAME,
|
||||||
|
} from "@lib/archidekt";
|
||||||
|
|
||||||
|
const games = [
|
||||||
|
{
|
||||||
|
icon: "cards-playing",
|
||||||
|
title: "Magic: The Gathering",
|
||||||
|
body: "The main event. Mostly Commander (EDH), with a deckbuilding obsession that Archidekt enables far too well.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "pokeball",
|
||||||
|
title: "Pokémon TCG",
|
||||||
|
body: "Collector energy and casual play. Beautiful art pulls, deck drafts, and a soft spot for Steel/Psychic. Mostly on Pocket TCG.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "sword-cross",
|
||||||
|
title: "Riftbound",
|
||||||
|
body: "Riot's League of Legends TCG. A new game with a lot of potential. Interested in exploring deckbuilding and the evolving meta.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "robot",
|
||||||
|
title: "Gundam Card Game",
|
||||||
|
body: "Bandai's Gundam TCG. A new game with a lot of potential. Interested in how some of the figures will come into play with the other table top pieces for this entry.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const proxy = [
|
||||||
|
{
|
||||||
|
icon: "content-cut",
|
||||||
|
title: "Proxy TCG cards",
|
||||||
|
body: "Designing and cutting proxy cards on the Silhouette Cameo 5a for casual play",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "scissors-cutting",
|
||||||
|
title: "Custom stickers & decals",
|
||||||
|
body: "Vinyl cut for sleeves, playmats, and storage — life counters, clan badges, and the occasional silly sticker.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "diamond-stone",
|
||||||
|
title: "Tokens & counters",
|
||||||
|
body: "Resin-cast tokens, +1/+1 counters, and commander damage trackers cast in the pressure pot.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "printer-3d-nozzle",
|
||||||
|
title: "3D-printed props",
|
||||||
|
body: "Life counters, dice towers, deck boxes, and display stands off.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const related = [
|
||||||
|
{
|
||||||
|
href: "/interests/ttrpgs",
|
||||||
|
title: "TTRPGs & Tabletop",
|
||||||
|
icon: "dice-multiple",
|
||||||
|
blurb: "Cards and dice share the same shelf and the same table.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: "/interests/making",
|
||||||
|
title: "Making & Creating",
|
||||||
|
icon: "palette",
|
||||||
|
blurb: "Proxies, tokens, and props are the TCG side of the workshop.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Live Archidekt decks (server-side, cached ~1h, never throws).
|
||||||
|
const decks = await getDecks();
|
||||||
|
const decksHref = archidektSearchUrl();
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
surface="interest"
|
||||||
|
title="Trading Card Games"
|
||||||
|
description="Magic: The Gathering Commander decks, Pokémon collecting, Riftbound, and the Gundam card game. Plus the proxies, tokens, and props I make to play them."
|
||||||
|
>
|
||||||
|
<InterestHero
|
||||||
|
icon="cards"
|
||||||
|
title="Trading Card Games"
|
||||||
|
tagline="Cardboard, deckbuilding, and the eternal question: one more pack?"
|
||||||
|
image="/images/cards.jpg"
|
||||||
|
imageAlt="Trading cards and dice on a playmat"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="container-prose flex flex-col gap-20 py-12 sm:py-16">
|
||||||
|
{/* Overview */}
|
||||||
|
<section
|
||||||
|
class="flex flex-col gap-5 text-lg leading-relaxed"
|
||||||
|
style="color: var(--color-text-muted);"
|
||||||
|
>
|
||||||
|
<Reveal
|
||||||
|
><p>
|
||||||
|
Trading card games are where my collecting, deckbuilding, and making
|
||||||
|
instincts all collide. I primarily play Magic: The Gathering, mostly
|
||||||
|
Commander (Elder Dragon Highlander), where a hundred cards becomes a
|
||||||
|
personal expression and the brewing never really ends. But I keep a
|
||||||
|
hand in the Pokémon TCG, and I'm watching Riftbound and the new Gundam
|
||||||
|
card game closely. If it has nice cardboard, pretty art, and a
|
||||||
|
decision tree, I'm interested.
|
||||||
|
</p></Reveal
|
||||||
|
>
|
||||||
|
<Reveal delay={80}
|
||||||
|
><p>
|
||||||
|
The hobby overlaps the rest of the fun wing: the same workshop that
|
||||||
|
produces resin dice and 3D prints also turns out proxies, tokens, and
|
||||||
|
deckboxes and more. And the deckbuilding itch is a close relation of
|
||||||
|
the RPG character-builder in me. Optimizing a list tells a story about
|
||||||
|
how I want to play and the theme I build the deck around.
|
||||||
|
</p></Reveal
|
||||||
|
>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* The games I play */}
|
||||||
|
<section class="flex flex-col gap-6">
|
||||||
|
<Reveal
|
||||||
|
><SectionHeader
|
||||||
|
kicker="Collection"
|
||||||
|
icon="cards-outline"
|
||||||
|
title="The games I play"
|
||||||
|
/></Reveal
|
||||||
|
>
|
||||||
|
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-2">
|
||||||
|
{
|
||||||
|
games.map((g, i) => (
|
||||||
|
<Reveal delay={i * 70}>
|
||||||
|
<InterestCard icon={g.icon} title={g.title}>
|
||||||
|
{g.body}
|
||||||
|
</InterestCard>
|
||||||
|
</Reveal>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Magic: The Gathering decks (live from Archidekt) */}
|
||||||
|
<section class="flex flex-col gap-6">
|
||||||
|
<Reveal>
|
||||||
|
<div class="flex flex-wrap items-end justify-between gap-4">
|
||||||
|
<SectionHeader kicker="Magic" icon="magic-staff" title="MTG decks" />
|
||||||
|
<a
|
||||||
|
href={decksHref}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-flex items-center gap-1.5 text-sm transition-colors hover:underline"
|
||||||
|
style="color: var(--color-accent);"
|
||||||
|
>
|
||||||
|
Manage on Archidekt
|
||||||
|
<Icon name="open-in-new" class="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Reveal>
|
||||||
|
<Reveal delay={40}>
|
||||||
|
<p class="text-sm" style="color: var(--color-text-muted);">
|
||||||
|
Pulled live from my Archidekt account ({ARCHIDEKT_USERNAME}). Newest
|
||||||
|
decks first; click a card to open it.
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
{
|
||||||
|
decks.length > 0 ? (
|
||||||
|
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{decks
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt))
|
||||||
|
.map((d, i) => (
|
||||||
|
<Reveal delay={Math.min(i * 50, 400)}>
|
||||||
|
<DeckCard
|
||||||
|
name={d.name}
|
||||||
|
href={d.href}
|
||||||
|
featured={d.featured}
|
||||||
|
colors={d.colors}
|
||||||
|
size={d.size}
|
||||||
|
formatLabel={d.formatLabel}
|
||||||
|
updatedAt={d.updatedAt}
|
||||||
|
/>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Reveal delay={80}>
|
||||||
|
<Placeholder
|
||||||
|
label="Archidekt decks"
|
||||||
|
hint={`Couldn't reach Archidekt right now. View the full list at ${decksHref}.`}
|
||||||
|
/>
|
||||||
|
</Reveal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Proxy making */}
|
||||||
|
<section class="flex flex-col gap-6">
|
||||||
|
<Reveal
|
||||||
|
><SectionHeader
|
||||||
|
kicker="Workshop"
|
||||||
|
icon="auto-fix"
|
||||||
|
title="Proxy making & printed props"
|
||||||
|
/></Reveal
|
||||||
|
>
|
||||||
|
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-2">
|
||||||
|
{
|
||||||
|
proxy.map((p, i) => (
|
||||||
|
<Reveal delay={i * 70}>
|
||||||
|
<InterestCard icon={p.icon} title={p.title}>
|
||||||
|
{p.body}
|
||||||
|
</InterestCard>
|
||||||
|
</Reveal>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<RelatedInterests items={related} />
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
@@ -67,6 +67,12 @@ const related = [
|
|||||||
icon: "gamepad-variant",
|
icon: "gamepad-variant",
|
||||||
blurb: "Digital RPGs as another expression of the same impulse.",
|
blurb: "Digital RPGs as another expression of the same impulse.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: "/interests/tcgs",
|
||||||
|
title: "Trading Card Games",
|
||||||
|
icon: "cards",
|
||||||
|
blurb: "Cards and dice share the same shelf — and the same table.",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user