Start the rebuild: note schema, vault kit and data migration

Phase 1 of the functional design plan.

- Move the old site into legacy/, unchanged, for reference. It still
  builds from there until cutover.
- Add packages/schema: the one definition of all 20 note types, with
  field titles and descriptions, note and vault checks, and JSON Schema
  export so later editing tools and the MCP server share it.
- Add vault-kit: Obsidian templates generated from the schema, and the
  Site guide for 24.01_PersonalWebsite.
- Add tools/migrate: turns the legacy JSON, collections, store data and
  page text into 545 Site folder notes and 84 images, and checks the
  result with the schema.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
This commit is contained in:
Claude
2026-10-11 18:40:55 +00:00
parent 4ded4b277b
commit a2f723ca2d
324 changed files with 11538 additions and 7556 deletions
+118
View File
@@ -0,0 +1,118 @@
---
/**
* anime.astro — Anime
* ---------------------------------------------------------------------------
* The primary pillar: 150+ completed series. Favorites render as poster cards
* up top (populated by scripts/fetch-anime-art.mjs); the full library is
* grouped by category below. Follows the canonical interest-page template.
*/
import BaseLayout from "@layouts/BaseLayout.astro";
import Reveal from "@components/motion/Reveal.astro";
import InterestHero from "@components/layout/InterestHero.astro";
import MediaCard from "@components/cards/MediaCard.astro";
import CategoryList from "@components/media/CategoryList.astro";
import SectionHeader from "@components/layout/SectionHeader.astro";
import RelatedInterests from "@components/layout/RelatedInterests.astro";
import Placeholder from "@components/layout/Placeholder.astro";
import { getFavorites, getGrouped, mediaKinds, getCount } from "@lib/media";
const meta = mediaKinds.anime;
const favorites = getFavorites("anime");
const groups = getGrouped("anime");
const count = getCount("anime");
const related = [
{
href: "/interests/tv",
title: "TV Shows",
icon: "television-classic",
blurb: "When I step away from animation, the live-action backlog is waiting.",
},
{
href: "/interests/reading",
title: "Reading",
icon: "book-open-page-variant",
blurb: "Half of these have light novels and manga I'm reading too.",
},
];
---
<BaseLayout
surface="interest"
title={meta.title}
description={`${count}+ completed anime series. Shonen, isekai, dark psychological. The primary pillar of my watchlist, not a side shelf.`}
>
<InterestHero
icon={meta.icon}
title={meta.title}
tagline={meta.tagline}
image={meta.heroImage}
imageAlt={meta.heroImageAlt}
/>
<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>{meta.overview}</p></Reveal>
</section>
{/* Favorites */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker="The rewatch pile"
icon="star"
title="Favorites"
/></Reveal
>
{
favorites.length > 0 ? (
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{favorites.map((item, i) => (
<Reveal delay={i * 70}>
<MediaCard
kind="anime"
title={item.title}
image={item.image}
status={item.status}
blurb={item.blurb}
/>
</Reveal>
))}
</div>
) : (
<Placeholder
label="Anime favorites"
hint='Mark items with "favorite": true in src/data/media/anime.json.'
/>
)
}
</section>
{/* Full library */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker={`${count}+ and counting`}
icon="format-list-bulleted"
title="The full watch history"
/></Reveal
>
<Reveal delay={80}>
{groups.length > 0 ? (
<CategoryList groups={groups} pillIcon="play-circle" />
) : (
<Placeholder
label="Anime library"
hint="Add categorized items to src/data/media/anime.json."
/>
)}
</Reveal>
</section>
<RelatedInterests items={related} />
</div>
</BaseLayout>
+281
View File
@@ -0,0 +1,281 @@
---
/**
* gaming.astro — Gaming
* ---------------------------------------------------------------------------
* Per-interest template: header + tagline, verbatim overview, platforms card
* grid, favorite genres, currently playing placeholder, and related interests.
*/
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 GameCard from "@components/cards/GameCard.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 { getCollection, getEntry } from "astro:content";
import { platformToIcon } from "@lib/platforms";
const platforms = [
{
icon: "sony-playstation",
title: "PlayStation 5",
body: "Mainly for single player and exclusives.",
},
{
icon: "nintendo-switch",
title: "Nintendo Switch / Switch 2",
body: "Handheld and docked. Pokemon, Mario, and many other Nintendo and indie gems.",
},
{
icon: "steam",
title: "Steam",
body: "The primary game library. Strategy, RPGs, Adventure, oh my! And an ever-growing backlog.",
},
];
/**
* Favorite genres. Each is a small card with an mdi icon and a one-line blurb
* on what Greg loves about that genre. Page-local; edit freely.
*/
const genres = [
{
icon: "sword-cross",
name: "RPGs (Western & JRPG)",
blurb:
"Character progression and stories I can live in for 100+ hours and then turn around and do it again.",
},
{
icon: "pokeball",
name: "Creature Collecting",
blurb: "If I can catch, train, or befriend a roster, I'm sold.",
},
{
icon: "chess-king",
name: "Strategy & Tactics",
blurb:
"Turn-based puzzles and tactical challengeswhere the right plan is oh so rewarding.",
},
{
icon: "hammer",
name: "Building & Crafting",
blurb:
"Give me a grid, resources, and a goal, and watch the evening vanish.",
},
{
icon: "factory",
name: "Automation",
blurb: "Optimizing a production line until its perfect is my zen.",
},
{
icon: "compass-outline",
name: "Open-world Exploration",
blurb:
"The map IS the point. I will try to climb every mountain and I will check every waterfall for treasure.",
},
{
icon: "star-four-points",
name: "Indie Darlings",
blurb:
"The small, weird, passionate projects that punch far above their size.",
},
];
// Gaming Hall of Fame — one file per game in src/content/games/hall-of-fame/.
const hallOfFame = (await getCollection("games")).sort(
(a, b) => a.data.order - b.data.order,
);
// Currently playing — single list in src/content/games/now-playing.yaml.
const nowPlayingEntry = await getEntry("nowPlaying", "now-playing");
const nowPlaying = nowPlayingEntry?.data.games ?? [];
const related = [
/*{
href: "/interests/pokemon",
title: "Pokémon",
icon: "pokeball",
blurb: "Nintendo console overlap and a lifelong obsession.",
},*/
{
href: "/interests/ttrpgs",
title: "TTRPGs & Tabletop",
icon: "dice-multiple",
blurb: "Digital RPGs as another expression of the same impulse.",
},
];
---
<BaseLayout
surface="interest"
title="Gaming"
description="PlayStation 5, Nintendo Switch 2, and a Steam library that has gotten politely out of hand. RPGs, strategy, and deeply layered worlds."
>
<InterestHero
icon="gamepad-variant"
title="Gaming"
tagline="Walking in another's shoes, escaping reality, or obsessively completing collections, one game at a time."
image="/images/gaming-setup.jpg"
imageAlt="A gaming setup with controller and console"
/>
<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>
Gaming has always been a passion in my life. I span console
generations from the Sega Genesis to the modern era. I play mainly
with a PlayStation 5, Nintendo Switch 2, and a Steam library that has
gotten politely out of hand. I'm particularly drawn to RPGs, strategy
games, and anything with a creature to tame or a deeply layered world
to explore. But a good game loop will get me sucked in harder than the
sunk cost fallacy of a League of Legends streak. I love to play with
friends, but have found myself leaning more towards single-player
experiences. Someone should have told younger me that scheduling would
be the final boss!
</p></Reveal
>
</section>
{/* Platforms */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker="Platforms"
icon="gamepad-variant"
title="Where I play"
/></Reveal
>
<div class="grid gap-5 sm:grid-cols-3">
{
platforms.map((p, i) => (
<Reveal delay={i * 80}>
<InterestCard icon={p.icon} title={p.title}>
{p.body}
</InterestCard>
</Reveal>
))
}
</div>
</section>
{/* Genres */}
<section class="flex flex-col gap-5">
<Reveal
><SectionHeader
kicker="Tastes"
icon="shape"
title="Favorite genres"
/></Reveal
>
<Reveal delay={80}>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{
genres.map((g, i) => (
<Reveal delay={i * 60}>
<article class="card card-interactive flex h-full flex-col gap-2 p-4">
<div class="flex items-center gap-2.5">
<span
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-lg"
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent); color: var(--color-accent);"
aria-hidden="true"
>
<Icon name={g.icon} class="h-5 w-5" />
</span>
<h3
class="font-display text-base"
style="color: var(--color-text);"
>
{g.name}
</h3>
</div>
<p
class="text-sm leading-relaxed"
style="color: var(--color-text-muted);"
>
{g.blurb}
</p>
</article>
</Reveal>
))
}
</div>
</Reveal>
</section>
{/* Currently */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker="Currently"
icon="calendar-clock"
title="Now playing"
/></Reveal
>
{
nowPlaying.length > 0 ? (
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{nowPlaying.map((g, i) => (
<Reveal delay={i * 60}>
<InterestCard
icon={platformToIcon(g.platform)}
title={g.title}
currently
>
{g.platform}
</InterestCard>
</Reveal>
))}
</div>
) : (
<Placeholder
label="Currently playing"
hint="Add games to src/content/games/now-playing.yaml."
/>
)
}
</section>
{/* Gaming Hall of Fame */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker="Hall of Fame"
icon="trophy"
title="All-time favorites"
/></Reveal
>
{
hallOfFame.length > 0 ? (
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{hallOfFame.map((g, i) => (
<Reveal delay={i * 80}>
<GameCard
title={g.data.title}
platform={g.data.platform}
blurb={g.data.blurb}
art={g.data.art}
artAlt={g.data.artAlt}
year={g.data.year}
icon={g.data.icon}
/>
</Reveal>
))}
</div>
) : (
<Placeholder
label="Gaming Hall of Fame"
hint="Add games to src/content/games/hall-of-fame/."
/>
)
}
</section>
<RelatedInterests items={related} />
</div>
</BaseLayout>
+227
View File
@@ -0,0 +1,227 @@
---
/**
* interests/index.astro — Interests Hub
* ---------------------------------------------------------------------------
* The "fun wing" landing page. A warm grid of cards linking into each interest
* sub-page, using the spec's taglines. Sets surface="interest" so the whole
* page reads as the craft-side night sky regardless of the global light/dark toggle.
*/
import BaseLayout from "@layouts/BaseLayout.astro";
import Reveal from "@components/motion/Reveal.astro";
import Icon from "@components/Icon.astro";
import SectionHeader from "@components/layout/SectionHeader.astro";
import AnimatedBackground from "@components/hero/AnimatedBackground.tsx";
import Motif from "@components/brand/Motif.astro";
import ConstellationField from "@components/mystic/ConstellationField.tsx";
const interests = [
{
href: "/interests/ttrpgs",
icon: "dice-multiple",
title: "TTRPGs & Tabletop",
tagline: "Have dice, will roll.",
blurb:
"Card Games, Board Games, and Long-running D&D and Daggerheart campaigns on Foundry VTT. Worldbuilding, NPC relationships, and player-driven narrative.",
image: "/images/ttrpg-dice.jpg",
alt: "Boardgame Shelf",
},
{
href: "/interests/gaming",
icon: "gamepad-variant",
title: "Gaming",
tagline: "We do not talk about my backlog.",
blurb:
"PlayStation 5, Nintendo Switch 2, and a politely out-of-hand Steam library. RPGs, strategy, and anything with a deeply layered world.",
image: "/images/gaming-setup.jpg",
alt: "Gaming setup desk",
},
{
href: "/interests/reading",
icon: "book-open-page-variant",
title: "Reading",
tagline: "Perpetually three books ahead of my to-read list.",
blurb:
"The Cosmere, Dune, the Pendergast saga, and Throne of Glass. Plus light novels, manga, manhwa, and webtoons.",
image: "/images/fantasy-books.jpg",
alt: "Fantasy bookshelf",
},
{
href: "/interests/anime",
icon: "animation-play",
title: "Anime",
tagline: "Subbed, dubbed, and somehow still behind.",
blurb:
"150+ completed series. The shonen classics, the modern hits, the isekai rabbit hole, and Frieren living rent-free in my head.",
image: "/images/anime-hero.jpg",
alt: "A cosplayer in a colorful anime-inspired costume at a convention",
},
{
href: "/interests/tv",
icon: "television-classic",
title: "TV Shows",
tagline: "My rewatch rotation has a rewatch rotation.",
blurb:
"200+ series across every prestige drama, NBC comedy, streaming original, and the full Marvel and Star Wars rosters.",
image: "/images/tv-hero.jpg",
alt: "A cozy living room with a couch facing a TV",
},
{
href: "/interests/movies",
icon: "movie-open",
title: "Movies",
tagline: "I stayed for the post-credits scene.",
blurb:
"160+ films across 17+ franchises. When I love a series I commit all the way, and The Princess Bride is always within reach.",
image: "/images/movies-hero.jpg",
alt: "Empty red seats in a vintage movie theater",
},
{
href: "/interests/making",
icon: "palette",
title: "Making & Creating",
tagline: "A magnet for super glue and nice cardboard.",
blurb:
"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",
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',
icon: 'pokeball',
title: 'Pokémon',
tagline: 'Metagross supremacy. This is not up for debate.',
blurb: 'A lifelong obsession from Red and Blue to modern competitive. Steel/Psychic, beautiful design, absolutely devastating in battle.',
image: '/images/pokemon-cards.jpg',
alt: 'Trading card collection',
},*/,
];
---
<BaseLayout
surface="interest"
title="Interests"
description="The fun wing: tabletop RPGs, gaming, reading, resin dice and 3D printing, and Pokémon. Where Greg's passions live as stories and collections."
>
<!-- Hero -->
<section class="on-sky relative overflow-hidden pb-8 pt-28 sm:pt-32">
{
/* Full animated Milky Way field + stars, matching the Home hero treatment. */
}
<AnimatedBackground client:idle className="vt-sky" />
<div
class="container-prose relative z-10 flex flex-col items-start gap-6 py-8"
>
<Reveal>
<SectionHeader
tone="hero"
kicker="The personal wing"
icon="heart"
title="Interests"
subtitle="Warmer, more visual, personality-driven. Not a list of hobbies but a collection of stories."
/>
</Reveal>
</div>
</section>
<!-- Interest grid -->
<section class="relative overflow-hidden py-12 sm:py-16">
<ConstellationField client:idle className="opacity-45" tint="teal" />
<Motif
name="tria-prima-array-seal-light"
class="absolute -left-48 bottom-0 z-0 h-[38rem] w-[38rem]"
style="opacity:.12;"
/>
<div class="container-prose relative z-10">
<div class="grid gap-5 md:grid-cols-2 lg:grid-cols-3">
{
interests.map((item, i) => (
<Reveal delay={i * 80}>
<a
href={item.href}
class="card card-interactive group flex h-full flex-col gap-4 overflow-hidden p-0"
>
<div class="aspect-[16/10] w-full overflow-hidden">
<img
src={item.image}
alt={item.alt}
loading="lazy"
decoding="async"
class="h-full w-full object-cover transition-transform duration-700 group-hover:scale-105"
/>
</div>
<div class="flex flex-col gap-3 px-5 pb-5">
<div class="flex items-center justify-between">
<span
class="inline-flex h-12 w-12 items-center justify-center rounded-xl"
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent); color: var(--color-accent);"
aria-hidden="true"
>
<Icon name={item.icon} class="h-6 w-6" />
</span>
<Icon
name="arrow-right"
class="h-5 w-5 transition-transform group-hover:translate-x-1"
style="color: var(--color-accent);"
/>
</div>
<div class="flex flex-col gap-2">
<h3 class="font-display" style="color: var(--color-text);">
{item.title}
</h3>
<p
class="font-sans text-sm italic"
style="color: var(--color-secondary);"
>
{item.tagline}
</p>
</div>
<p
class="grow text-sm leading-relaxed"
style="color: var(--color-text-muted);"
>
{item.blurb}
</p>
</div>
</a>
</Reveal>
))
}
{/* Cross-link back to professional wing */}
<Reveal delay={interests.length * 80}>
<a
href="/professional"
class="card card-interactive group flex h-full flex-col justify-center gap-2 border-dashed p-5 text-center"
style="border-color: color-mix(in srgb, var(--color-accent) 40%, transparent);"
>
<Icon
name="briefcase"
class="mx-auto h-7 w-7"
style="color: var(--color-accent);"
/>
<p
class="font-sans font-semibold"
style="color: var(--color-text);"
>
The other wing
</p>
<p class="text-sm" style="color: var(--color-text-muted);">
See where curiosity meets the day job.
</p>
</a>
</Reveal>
</div>
</div>
</section>
</BaseLayout>
+240
View File
@@ -0,0 +1,240 @@
---
/**
* making.astro — Making & Creating
* ---------------------------------------------------------------------------
* Per-interest template with equipment card grid, a gallery placeholder, and the
* spec's connection-to-TTRPGs callout. Related links back to TTRPGs and Gaming.
*/
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 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 CelestialImage from "@components/mystic/CelestialImage.astro";
const equipment = [
{
icon: "dice-multiple",
title: "Resin Dice Making",
body: "Custom molds, pressure pot curing, pigment experimentation. Producing dice sets for personal use and gifts.",
},
{
icon: "printer-3d",
title: "Elegoo Mars 4",
body: "Resin 3D printer. Miniatures, terrain, and high-detail props.",
},
{
icon: "printer-3d-nozzle",
title: "Bambu Labs P1S",
body: "FDM 3D printer. Functional prints, terrain pieces, and larger prototypes.",
},
{
icon: "scissors-cutting",
title: "Silhouette Cameo 5a",
body: "Multi material cutting machine. Silly stickers, custom vinyl decals, and custom/proxy TCG cards.",
},
{
icon: "wrench",
title: "General Making",
body: "Always experimenting and hoarding nice cardboard. If there is a new technique or material, I want to try it.",
},
];
const related = [
{
href: "/interests/ttrpgs",
title: "TTRPGs & Tabletop",
icon: "dice-multiple",
blurb: "Much of what I make ends up on the virtual table.",
},
{
href: "/interests/reading",
title: "Reading & Media",
icon: "book-open-page-variant",
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.",
},
];
---
<BaseLayout
surface="interest"
title="Making & Creating"
description="Resin dice in a pressure pot, miniatures and terrain on the Elegoo Mars 4 and Bambu Labs P1S. If it involves layers and precision, I am in."
>
<InterestHero
icon="palette"
title="Making & Creating"
tagline="A magnet for super glue and nice cardboard."
image="/images/making-print.jpg"
imageAlt="A 3D printer producing a detailed model"
/>
<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>
Making is part of me. Ever since I can remember, I had been dreaming
of making inventions and things from cardboard and clay. I took things
apart to see how they worked, and rarely got them back together. But
now I have the tools and the skills to take ideas from my imagination
and physically produce them. I cast resin dice using pressure pots and
custom molds, print miniatures and props on my Elegoo Mars 4 (resin)
and Bambu Labs P1S (FDM), and generally fill my workspace with
projects-in-progress. I make beatiful cards as gifts and even make
proxy mockups of trading cards to enjoy games casually.
</p></Reveal
>
<Reveal>
<p>
There's something deeply satisfying about taking raw material through
a process and holding the finished piece, whether that's a perfect
polyhedral set or a hook that fits just right in that weird spot in
the bathroom. The ability to blend my technial skills and my creative
impulses into tangible objects is a fantasy come true my child self
only dreamed of.
</p></Reveal
>
</section>
{/* Equipment */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker="Equipment"
icon="toolbox"
title="What is on the bench"
/></Reveal
>
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-2">
{
equipment.map((e, i) => (
<Reveal delay={i * 70}>
<InterestCard icon={e.icon} title={e.title}>
{e.body}
</InterestCard>
</Reveal>
))
}
</div>
</section>
{/* Gallery */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker="Gallery"
icon="image-multiple"
title="Finished pieces"
/></Reveal
>
<Reveal delay={80}>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<CelestialImage
src="/images/making-resin.jpg"
alt="Resin dice set teal with gold leaf inclusions"
aspect="4/3"
/>
<CelestialImage
src="/images/dice_golden_moss.jpg"
alt="Resin dice set moss like pattern with gold lettering"
aspect="4/3"
/>
<CelestialImage
src="/images/dice_scarlet.jpg"
alt="Resin dice set scarlet with gold lettering"
aspect="4/3"
/>
<CelestialImage
src="/images/dice_galaxy.jpg"
alt="Resin dice set dark galaxy pattern with gold powder swirls"
aspect="4/3"
/>
<CelestialImage
src="/images/minis.jpg"
alt="3D printed miniature"
aspect="4/3"
/>
<CelestialImage
src="/images/deckboxes.jpg"
alt="Deckboxes (Design by
@Iceenchanted on MakerWorld)"
aspect="4/3"
/>
<CelestialImage
src="/images/proxies.jpg"
alt="Proxy cards for Kynaios and Tiro deck"
aspect="4/3"
/>
<CelestialImage
src="/images/proxies2.jpg"
alt="Successful Foil Proxies MTG"
aspect="4/3"
/>
<CelestialImage
src="/images/xmasgifts.jpg"
alt="CS:GO Cases, Nightblood, Runescape Helmet, ACOTAR drawers"
aspect="4/3"
/>
<CelestialImage
src="/images/resin_rings.jpg"
alt="Resin and painted rings"
aspect="4/3"
/>
</div>
</Reveal>
</section>
{/* Connection to TTRPGs */}
<section class="flex flex-col gap-5">
<Reveal
><SectionHeader
kicker="Cross-pollination"
icon="link-variant"
title="Where it all connects"
/></Reveal
>
<Reveal delay={80}>
<a
href="/interests/ttrpgs"
class="card card-interactive group flex items-center gap-4"
>
<Icon
name="dice-multiple"
class="h-8 w-8 shrink-0"
style="color: var(--color-accent);"
/>
<span class="flex flex-col gap-1">
<span
class="font-sans font-semibold"
style="color: var(--color-text);"
>Much of what I make ends up on the virtual table.</span
>
<span class="text-sm" style="color: var(--color-text-muted);"
>Custom tokens, terrain, and props for Foundry VTT campaigns.</span
>
</span>
<Icon
name="arrow-right"
class="ml-auto h-5 w-5 shrink-0 transition-transform group-hover:translate-x-1"
style="color: var(--color-accent);"
/>
</a>
</Reveal>
</section>
<RelatedInterests items={related} />
</div>
</BaseLayout>
+119
View File
@@ -0,0 +1,119 @@
---
/**
* movies.astro — Movies
* ---------------------------------------------------------------------------
* 160+ films across 17+ franchises. Favorite films render as poster cards;
* the full watch history below lists every film grouped by franchise (plus a
* Standalone group for single films). Posters populated by
* scripts/fetch-tmdb-art.mjs.
*/
import BaseLayout from "@layouts/BaseLayout.astro";
import Reveal from "@components/motion/Reveal.astro";
import InterestHero from "@components/layout/InterestHero.astro";
import MediaCard from "@components/cards/MediaCard.astro";
import CategoryList from "@components/media/CategoryList.astro";
import SectionHeader from "@components/layout/SectionHeader.astro";
import RelatedInterests from "@components/layout/RelatedInterests.astro";
import Placeholder from "@components/layout/Placeholder.astro";
import { getFavorites, getGrouped, mediaKinds, getCount } from "@lib/media";
const meta = mediaKinds.movies;
const favorites = getFavorites("movies");
const groups = getGrouped("movies");
const count = getCount("movies");
const related = [
{
href: "/interests/tv",
title: "TV Shows",
icon: "television-classic",
blurb: "The MCU and Star Wars sagas continue here.",
},
{
href: "/interests/anime",
title: "Anime",
icon: "animation-play",
blurb: "The animated half of my watchlist.",
},
];
---
<BaseLayout
surface="interest"
title={meta.title}
description={`${count}+ films across 17+ franchises. When I love a series I commit all the way, and The Princess Bride is always within reach.`}
>
<InterestHero
icon={meta.icon}
title={meta.title}
tagline={meta.tagline}
image={meta.heroImage}
imageAlt={meta.heroImageAlt}
/>
<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>{meta.overview}</p></Reveal>
</section>
{/* Favorites */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker="Stop on these any time"
icon="star"
title="Favorites"
/></Reveal
>
{
favorites.length > 0 ? (
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{favorites.map((item, i) => (
<Reveal delay={i * 70}>
<MediaCard
kind="movies"
title={item.title}
image={item.image}
year={item.year}
blurb={item.blurb}
/>
</Reveal>
))}
</div>
) : (
<Placeholder
label="Movie favorites"
hint='Mark items with "favorite": true in src/data/media/movies.json.'
/>
)
}
</section>
{/* Full franchise watch history */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker={`${count}+ films across ${groups.length} franchises`}
icon="filmstrip"
title="By franchise"
/></Reveal
>
<Reveal delay={80}>
{groups.length > 0 ? (
<CategoryList groups={groups} pillIcon="movie-open" />
) : (
<Placeholder
label="Movie library"
hint="Add categorized films to src/data/media/movies.json."
/>
)}
</Reveal>
</section>
<RelatedInterests items={related} />
</div>
</BaseLayout>
+95
View File
@@ -0,0 +1,95 @@
---
/**
* pokemon.astro — Pokémon
* ---------------------------------------------------------------------------
* Per-interest template: favorite callout (Metagross), games overview, a
* Showdown-format roster placeholder (spec calls for standard Showdown text),
* and related links to Gaming.
*/
import BaseLayout from '@layouts/BaseLayout.astro';
import Reveal from '@components/motion/Reveal.astro';
import InterestHero from '@components/layout/InterestHero.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';
const related = [
{ href: '/interests/gaming', title: 'Gaming', icon: 'gamepad-variant', blurb: 'Nintendo console overlap, from Game Boy to Switch 2.' },
{ href: '/interests/making', title: 'Making & Creating', icon: 'palette', blurb: 'Displays and props for collections.' },
];
---
<BaseLayout
surface="interest"
title="Pokémon"
description="A lifelong obsession from Red and Blue to modern competitive. Metagross supremacy: Steel/Psychic, beautiful design, absolutely devastating in battle."
>
<InterestHero
icon="pokeball"
title="Pokémon"
tagline="Metagross supremacy. This is not up for debate."
image="/images/pokemon-cards.jpg"
imageAlt="A collection of trading cards"
/>
<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>Pokémon has been a lifelong obsession. From Red and Blue on the original Game Boy to modern competitive formats, I've followed the franchise through every generation. My favorite? Metagross, the iron leg Pokémon. Steel/Psychic, beautiful design, absolutely devastating in battle. No further questions.</p></Reveal>
</section>
{/* Favorite callout */}
<Reveal>
<div
class="card relative flex flex-col items-center gap-4 overflow-hidden text-center sm:flex-row sm:items-center sm:gap-6 sm:text-left"
style="background: color-mix(in srgb, var(--color-accent) 8%, var(--color-surface-raised));"
>
<figure
class="relative aspect-square w-full shrink-0 overflow-hidden sm:w-28"
>
<img src="/images/pokemon-cards.jpg" alt="Trading card collection" loading="lazy" decoding="async" class="h-full w-full object-cover" />
</figure>
<span
class="inline-flex h-20 w-20 items-center justify-center rounded-2xl"
style="background: color-mix(in srgb, var(--color-accent) 22%, transparent); color: var(--color-accent);"
aria-hidden="true"
>
<Icon name="robot" class="h-10 w-10" />
</span>
<div class="flex flex-col gap-1">
<span class="font-eyebrow text-xs uppercase tracking-[0.22em]" style="color: var(--color-accent);">Favorite Pokémon</span>
<h2 style="color: var(--color-text);">Metagross</h2>
<p class="text-sm" style="color: var(--color-text-muted);">Steel / Psychic. The iron leg Pokémon.</p>
</div>
</div>
</Reveal>
{/* Roster */}
<section class="flex flex-col gap-6">
<Reveal><SectionHeader kicker="Battle team" icon="sword-cross" title="Roster" /></Reveal>
<Reveal delay={80}>
<p class="text-sm" style="color: var(--color-text-subtle);">
Rosters use standard Pokémon Showdown text format.
</p>
<Placeholder label="Pokémon Showdown roster" hint="Paste your preferred roster in Showdown text format.">
<pre
class="mt-3 overflow-x-auto rounded-lg p-4 font-mono text-xs"
style="background: var(--color-surface-sunken); color: var(--color-text-muted);"
>## Pokémon
[Greg's preferred roster in Showdown format]</pre>
</Placeholder>
</Reveal>
</section>
{/* Games */}
<section class="flex flex-col gap-6">
<Reveal><SectionHeader kicker="Across the generations" icon="gamepad-variant" title="Games" /></Reveal>
<Reveal delay={80}>
<Placeholder label="Mainline & spinoff titles" hint="List the mainline and spinoff titles played, if desired." />
</Reveal>
</section>
<RelatedInterests items={related} />
</div>
</BaseLayout>
+119
View File
@@ -0,0 +1,119 @@
---
/**
* reading.astro — Reading
* ---------------------------------------------------------------------------
* Books, comics, light novels, webtoons, and manga/manhwa. Favorites render as
* cover cards up top (populated by scripts/fetch-book-covers.mjs); the full
* library is grouped by category below. Follows the canonical interest-page
* template. The anime and movies/TV content that used to live here now has its
* own pages under /interests/anime, /tv, and /movies.
*/
import BaseLayout from "@layouts/BaseLayout.astro";
import Reveal from "@components/motion/Reveal.astro";
import InterestHero from "@components/layout/InterestHero.astro";
import MediaCard from "@components/cards/MediaCard.astro";
import CategoryList from "@components/media/CategoryList.astro";
import SectionHeader from "@components/layout/SectionHeader.astro";
import RelatedInterests from "@components/layout/RelatedInterests.astro";
import Placeholder from "@components/layout/Placeholder.astro";
import { getFavorites, getGrouped, mediaKinds } from "@lib/media";
const meta = mediaKinds.reading;
const favorites = getFavorites("reading");
const groups = getGrouped("reading");
const related = [
{
href: "/interests/anime",
title: "Anime",
icon: "animation-play",
blurb: "A lot of these light novels and manga have adaptations I've watched.",
},
{
href: "/interests/making",
title: "Making & Creating",
icon: "palette",
blurb: "Props and displays for the stories I love.",
},
];
---
<BaseLayout
surface="interest"
title={meta.title}
description="The Cosmere, Dune, the Pendergast saga, Throne of Glass. Plus light novels, manga, manhwa, and webtoons. If a story exists, I am probably consuming it."
>
<InterestHero
icon={meta.icon}
title={meta.title}
tagline={meta.tagline}
image={meta.heroImage}
imageAlt={meta.heroImageAlt}
/>
<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>{meta.overview}</p></Reveal>
</section>
{/* Favorites */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker="On the shelf"
icon="bookshelf"
title="Favorites"
/></Reveal
>
{
favorites.length > 0 ? (
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{favorites.map((item, i) => (
<Reveal delay={i * 70}>
<MediaCard
kind="reading"
title={item.title}
image={item.image}
status={item.status}
blurb={item.blurb}
/>
</Reveal>
))}
</div>
) : (
<Placeholder
label="Reading favorites"
hint='Mark items with "favorite": true in src/data/reading.json.'
/>
)
}
</section>
{/* Full library */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker="The rest of the shelf"
icon="format-list-bulleted"
title="What I'm reading"
/></Reveal
>
<Reveal delay={80}>
{groups.length > 0 ? (
<CategoryList groups={groups} pillIcon="book-open-page-variant" />
) : (
<Placeholder
label="Reading library"
hint="Add categorized items to src/data/reading.json."
/>
)}
</Reveal>
</section>
<RelatedInterests items={related} />
</div>
</BaseLayout>
+231
View File
@@ -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, and precision for 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>
+204
View File
@@ -0,0 +1,204 @@
---
/**
* ttrpgs.astro — TTRPGs & Tabletop
* ---------------------------------------------------------------------------
* The spec's per-interest template: Header (title + tagline), Overview
* (narrative, verbatim), Highlights (card grid, spec emojis -> mdi), Currently,
* then the Board Games sub-section, then RelatedInterests cross-links.
*/
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 SectionHeader from "@components/layout/SectionHeader.astro";
import RelatedInterests from "@components/layout/RelatedInterests.astro";
import Placeholder from "@components/layout/Placeholder.astro";
import BoardGameShelf from "@components/ttrpgs/BoardGameShelf.tsx";
import boardgames from "@/data/boardgames.json";
const highlights = [
{
icon: "castle",
title: "Dungeons & Dragons",
body: "Lately 5th Edition. Long-running campaigns on and off Foundry VTT. Focus on rich worldbuilding, meaningful NPC relationships, and player-driven narrative.",
},
{
icon: "sword-cross",
title: "Daggerheart",
body: "Darrington Press's dice-building RPG. Currently running a campaign and exploring how its mechanics create different story rhythms than D&D.",
},
{
icon: "target",
title: "Draw Steel",
body: "MCDM's fantasy tactical cinematic combat game. Interested in running it when I get a chance.",
},
{
icon: "shield-moon-outline",
title: "Cypher System",
body: "Monte Cook's narrative-focused system. Played a few one-shots and short form campaigns.",
},
{
icon: "knife-military",
title: "Blades in the Dark",
body: "A gritty, cinematic system for heists and criminal enterprises. Interested in playing short campaigns in it one day.",
},
{
icon: "weather-windy",
title: "Avatar the Last Airbender RPG",
body: "A narrative-focused system set in the world of Avatar. Interested in exploring bending mechanics and character-driven stories.",
},
{
icon: "monitor",
title: "Foundry VTT",
body: "Self-hosted on the home server. Building custom modules, maps, and automation for campaigns.",
},
];
const related = [
{
href: "/interests/making",
title: "Making & Creating",
icon: "palette",
blurb: "Dice and terrain made for campaigns.",
},
{
href: "/interests/gaming",
title: "Gaming",
icon: "gamepad-variant",
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.",
},
];
---
<BaseLayout
surface="interest"
title="TTRPGs & Tabletop"
description="Long-running D&D and Daggerheart campaigns on Foundry VTT, the MCDM Crows playtest, and a growing board game collection."
>
<InterestHero
icon="dice-multiple"
title="TTRPGs & Tabletop"
tagline="Minis and dice sets all sat round the table, card games commander all gathered when able. Games about dragons and spanning their wings, These are a few of my favorite things."
image="/images/ttrpg-mini.jpg"
imageAlt="A tabletop RPG spread with miniatures and dice"
/>
<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>
I love playing tabletop games from trading card games to role-playing
games. I run Dungeons & Dragons and Daggerheart campaigns that span
years of real-time play, maintaining worlds, NPCs, and narrative
threads that my players remember years later. I host everything on
Foundry VTT, which lets me build the kind of dynamic, visually rich
experiences that keep players invested session after session and lets
me flex my technical skills.
</p></Reveal
>
<Reveal delay={60}
><p>
I also love board games, from strategy to social deduction. My
collection is growing, and I enjoy hosting game nights with friends. I
have a particular fondness for games that challenge players to think
critically and creatively, and I appreciate the social aspect of
gaming as much as the gameplay itself. Although, I do love the ability
to play some of the engine building and deck-building games solo.
Spirit Island and Wyrmspan have been my favorites lately, and I am
always on the lookout for new games to add to my collection.
</p></Reveal
>
<Reveal delay={100}
><p>
I have played a few trading card games and collected many cards. I
have been a lifelong Pokémon and Magic: The Gathering fan, and I enjoy
the strategy and collection aspects of both games in different ways.
While I mostly enjoy looking at Pokemon cards, I spend most of my time
with Magic deck building and playing Elder Dragon Highlander (or
Commander). I recently picked up Riftbound and have had my eye on the
Gundam TCG game as well. The mechanical differences of these games are
fascinating, and I enjoy exploring the different strategies and
deck-building techniques that each game offers.
</p></Reveal
>
</section>
{/* Highlights */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker="Highlights"
icon="view-grid"
title="Systems & tools"
/></Reveal
>
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
{
highlights.map((h, i) => (
<Reveal delay={i * 70}>
<InterestCard icon={h.icon} title={h.title}>
{h.body}
</InterestCard>
</Reveal>
))
}
</div>
</section>
{
/* Currently
<section class="flex flex-col gap-6">
<Reveal><SectionHeader kicker="Currently" icon="calendar-clock" title="On the schedule" /></Reveal>
<div class="grid gap-4 sm:grid-cols-3">
<Placeholder label="Campaign 1 name" hint="D&D, [day of week]s." />
<Placeholder label="Campaign 2 name" hint="Daggerheart, [day of week]s." />
<Placeholder label="Crows playtest" hint="Ongoing sessions." />
</div>
</section>
*/
}
{/* Board games */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker="Beyond RPGs"
icon="cards-playing"
title="Board games"
/></Reveal
>
<Reveal delay={80}>
<blockquote
class="border-l-2 pl-5 text-lg leading-relaxed"
style="border-color: var(--color-gold); color: var(--color-text-muted);"
>
Beyond RPGs, I maintain a growing board and card game collection.
Favorites span strategy, deck-building, and social deduction. Game
night is a regular event, and yes, there's always room for one more.
Hover a title to preview its cover; click to open it on BoardGameGeek.
</blockquote>
</Reveal>
<Reveal delay={140}>
<BoardGameShelf
client:idle
games={boardgames}
shelfImage="/images/ttrpg-boardgame-shelf.jpg"
shelfAlt="Greg's board game shelf"
shelfCaption="The shelf"
/>
</Reveal>
</section>
<RelatedInterests items={related} />
</div>
</BaseLayout>
+118
View File
@@ -0,0 +1,118 @@
---
/**
* tv.astro — TV Shows
* ---------------------------------------------------------------------------
* 200+ completed series across prestige drama, streaming originals, workplace
* comedy, procedural, Marvel/Star Wars universes, animation, and reality.
* Favorites as poster cards; full library grouped by category below.
*/
import BaseLayout from "@layouts/BaseLayout.astro";
import Reveal from "@components/motion/Reveal.astro";
import InterestHero from "@components/layout/InterestHero.astro";
import MediaCard from "@components/cards/MediaCard.astro";
import CategoryList from "@components/media/CategoryList.astro";
import SectionHeader from "@components/layout/SectionHeader.astro";
import RelatedInterests from "@components/layout/RelatedInterests.astro";
import Placeholder from "@components/layout/Placeholder.astro";
import { getFavorites, getGrouped, mediaKinds, getCount } from "@lib/media";
const meta = mediaKinds.tv;
const favorites = getFavorites("tv");
const groups = getGrouped("tv");
const count = getCount("tv");
const related = [
{
href: "/interests/anime",
title: "Anime",
icon: "animation-play",
blurb: "The other half of my watchlist, honestly.",
},
{
href: "/interests/movies",
title: "Movies",
icon: "movie-open",
blurb: "The MCU, the Matrix, and everything between.",
},
];
---
<BaseLayout
surface="interest"
title={meta.title}
description={`${count}+ completed series across prestige drama, workplace comedy, streaming originals, and the full Marvel and Star Wars rosters.`}
>
<InterestHero
icon={meta.icon}
title={meta.title}
tagline={meta.tagline}
image={meta.heroImage}
imageAlt={meta.heroImageAlt}
/>
<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>{meta.overview}</p></Reveal>
</section>
{/* Favorites */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker="On heavy rotation"
icon="star"
title="Favorites"
/></Reveal
>
{
favorites.length > 0 ? (
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{favorites.map((item, i) => (
<Reveal delay={i * 70}>
<MediaCard
kind="tv"
title={item.title}
image={item.image}
status={item.status}
blurb={item.blurb}
/>
</Reveal>
))}
</div>
) : (
<Placeholder
label="TV favorites"
hint='Mark items with "favorite": true in src/data/media/tv.json.'
/>
)
}
</section>
{/* Full library */}
<section class="flex flex-col gap-6">
<Reveal
><SectionHeader
kicker={`${count}+ series`}
icon="format-list-bulleted"
title="Everything I've finished"
/></Reveal
>
<Reveal delay={80}>
{groups.length > 0 ? (
<CategoryList groups={groups} pillIcon="television-classic" />
) : (
<Placeholder
label="TV library"
hint="Add categorized items to src/data/media/tv.json."
/>
)}
</Reveal>
</section>
<RelatedInterests items={related} />
</div>
</BaseLayout>