mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Match the site to the approved designs
Denser star field, the page nebula, a favicon package, the filament banner without its box, the About portrait, Kashi and tea photos and Scorpius, the Store's own header, a header logo glow, a resume PDF that fills its page, one divider on the Library and Shelf, star-chart art for coverless favorites, Shelf Players, Play time and blog filters with a Show all fold, make kind pills on craft pages, and full-width closing bands. 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:
@@ -7,9 +7,9 @@
|
||||
import GiltCorners from "@gk/ui/components/GiltCorners.astro";
|
||||
import Icon from "@gk/ui/components/Icon.astro";
|
||||
import PageShell from "@gk/ui/components/PageShell.astro";
|
||||
import Seal from "@gk/ui/components/Seal.astro";
|
||||
import SectionHeading from "@gk/ui/components/SectionHeading.astro";
|
||||
import Star from "@gk/ui/components/Star.astro";
|
||||
import StarChartArt from "@gk/ui/components/StarChartArt.astro";
|
||||
import TickScale from "@gk/ui/components/TickScale.astro";
|
||||
import PageHero from "../components/PageHero.astro";
|
||||
import DoorLink from "../components/home/DoorLink.astro";
|
||||
@@ -17,7 +17,8 @@ import Portrait from "../components/home/Portrait.astro";
|
||||
import { splitIntro, viewStatus } from "../lib/home.ts";
|
||||
import { brandIcon } from "../lib/site.ts";
|
||||
import { site } from "../lib/page.ts";
|
||||
import { formatDate } from "../lib/site.ts";
|
||||
import { formatDate, mediaSrc } from "../lib/site.ts";
|
||||
import scorpius from "@gk/ui/assets/motifs/scorpius.svg?url";
|
||||
|
||||
const s = await site();
|
||||
const text = s.pageText("/about");
|
||||
@@ -25,6 +26,7 @@ const settings = s.settings();
|
||||
const { intro, sections } = splitIntro(text?.body ?? "");
|
||||
const introHtml = s.render(intro);
|
||||
const facts = text?.data.quickFacts ?? [];
|
||||
const photos = text?.data.photos ?? [];
|
||||
const now = s.statuses().slice(0, 4).map((st) => viewStatus(s, st));
|
||||
const nowUpdated = now.map((v) => v.date).sort().pop();
|
||||
const socials = settings?.socials ?? [];
|
||||
@@ -43,7 +45,7 @@ const SOCIAL_ICONS: Record<string, string> = { github: "web", linkedin: "briefca
|
||||
{introHtml ? <blockquote class="iam" set:html={introHtml} /> : <blockquote class="iam"><p>[PLACEHOLDER]</p></blockquote>}
|
||||
</div>
|
||||
<div class="intro-portrait">
|
||||
<Portrait variant="panel" seal="astrolabe" />
|
||||
<Portrait variant="panel" seal="astrolabe" src={mediaSrc(text?.data.portrait)} alt="Greg, a candid portrait" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -60,6 +62,18 @@ const SOCIAL_ICONS: Record<string, string> = { github: "web", linkedin: "briefca
|
||||
</section>
|
||||
))
|
||||
}
|
||||
{
|
||||
photos.length > 0 && (
|
||||
<div class="photos">
|
||||
{photos.map((ph) => (
|
||||
<figure class="photo">
|
||||
<StarChartArt seal="astrolabe" ratio="4 / 3" src={mediaSrc(ph.file)} alt={ph.alt} class="photo-art" />
|
||||
{ph.caption && <figcaption>{ph.caption}</figcaption>}
|
||||
</figure>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<aside aria-label="Quick facts" class="facts">
|
||||
@@ -127,7 +141,7 @@ const SOCIAL_ICONS: Record<string, string> = { github: "web", linkedin: "briefca
|
||||
Two wings of the same house
|
||||
</SectionHeading>
|
||||
<div class="wing-wrap">
|
||||
<div class="wing-seal" aria-hidden="true"><Seal name="astrolabe" size={360} /></div>
|
||||
<div class="wing-stars" aria-hidden="true"><img src={scorpius} alt="" width="1100" height="400" /></div>
|
||||
<div class="wing-row">
|
||||
<div class="wing featured">
|
||||
<GiltCorners inset={6} size={30} />
|
||||
@@ -226,6 +240,27 @@ const SOCIAL_ICONS: Record<string, string> = { github: "web", linkedin: "briefca
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.photos {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.photo {
|
||||
flex: 1 1 240px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.photo :global(.photo-art) {
|
||||
border: 1px solid rgba(201, 162, 74, 0.55);
|
||||
}
|
||||
.photo figcaption {
|
||||
font-size: 12px;
|
||||
color: var(--gk-muted);
|
||||
}
|
||||
|
||||
.facts {
|
||||
flex: 1 1 280px;
|
||||
max-width: 340px;
|
||||
@@ -361,7 +396,8 @@ const SOCIAL_ICONS: Record<string, string> = { github: "web", linkedin: "briefca
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
}
|
||||
.wing-seal {
|
||||
/* Scorpius, from catalog positions, showing through the 70% opaque wings. */
|
||||
.wing-stars {
|
||||
position: absolute;
|
||||
inset: -70px 0;
|
||||
z-index: -1;
|
||||
@@ -369,9 +405,14 @@ const SOCIAL_ICONS: Record<string, string> = { github: "web", linkedin: "briefca
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
opacity: 0.12;
|
||||
opacity: 0.55;
|
||||
overflow: hidden;
|
||||
}
|
||||
.wing-stars img {
|
||||
width: 1100px;
|
||||
max-width: none;
|
||||
height: 400px;
|
||||
}
|
||||
.wing-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -17,6 +17,7 @@ import NowCard from "../components/home/NowCard.astro";
|
||||
import Portrait from "../components/home/Portrait.astro";
|
||||
import { currentRole, viewStatus, worldPhoto } from "../lib/home.ts";
|
||||
import { site } from "../lib/page.ts";
|
||||
import { mediaSrc } from "../lib/site.ts";
|
||||
|
||||
const s = await site();
|
||||
const settings = s.settings();
|
||||
@@ -50,7 +51,7 @@ const listFormat = new Intl.ListFormat("en", { style: "long", type: "conjunction
|
||||
{intro ? <div class="lede" set:html={intro} /> : <p class="lede">[PLACEHOLDER]</p>}
|
||||
</div>
|
||||
<div class="portrait-wrap">
|
||||
<Portrait variant="hero" seal="transmutation-star-paths" />
|
||||
<Portrait variant="hero" seal="transmutation-star-paths" src={mediaSrc(text?.data.portrait)} alt={name} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
* constellation, then the list grouped Now / Up next / Finished, filtered by
|
||||
* medium, state, favorites and blog posts.
|
||||
*/
|
||||
import Divider from "@gk/ui/components/Divider.astro";
|
||||
import PageShell from "@gk/ui/components/PageShell.astro";
|
||||
import SearchField from "@gk/ui/components/SearchField.astro";
|
||||
import Constellation from "../../components/library/Constellation.astro";
|
||||
@@ -45,10 +44,10 @@ const typeHref = (key?: string) => {
|
||||
const FIRST = 12;
|
||||
---
|
||||
<PageShell title="Library" family="library" current="library" description="What Greg is reading, watching and playing now, what's next, and what stuck with him.">
|
||||
<Fragment slot="hero"><LibraryHero current="library" lede={lede} /></Fragment>
|
||||
|
||||
<Constellation items={constellation(items)} />
|
||||
<div class="divide"><Divider seed="library-list" /></div>
|
||||
<Fragment slot="hero">
|
||||
<LibraryHero current="library" lede={lede} />
|
||||
<Constellation items={constellation(items)} />
|
||||
</Fragment>
|
||||
|
||||
<FilterLayout action="/library" formId="lib-filters">
|
||||
<Fragment slot="rail">
|
||||
@@ -88,9 +87,6 @@ const FIRST = 12;
|
||||
</PageShell>
|
||||
|
||||
<style>
|
||||
.divide {
|
||||
padding: 24px 0 4px;
|
||||
}
|
||||
.types {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* counts, play times and box art sync from BoardGameGeek (a cover set in the
|
||||
* note wins); no filter for them yet.
|
||||
*/
|
||||
import Divider from "@gk/ui/components/Divider.astro";
|
||||
import FilterGroup from "@gk/ui/components/FilterGroup.astro";
|
||||
import GiltCorners from "@gk/ui/components/GiltCorners.astro";
|
||||
import PageShell from "@gk/ui/components/PageShell.astro";
|
||||
@@ -18,7 +17,7 @@ import FilterLayout from "../../components/library/FilterLayout.astro";
|
||||
import GameCard from "../../components/library/GameCard.astro";
|
||||
import LibraryHero from "../../components/library/LibraryHero.astro";
|
||||
import RailGroup from "../../components/library/RailGroup.astro";
|
||||
import { gameShows, onTheTable, playStyles, searchText, shelfFilter, sortGames, sorts, type Game } from "../../lib/library.ts";
|
||||
import { gameShows, minutesOf, onTheTable, playerOptions, playersOf, playStyles, searchText, shelfFilter, sortGames, sorts, timeOptions, type Game } from "../../lib/library.ts";
|
||||
import { site } from "../../lib/page.ts";
|
||||
import { formatDate, mediaSrc } from "../../lib/site.ts";
|
||||
|
||||
@@ -31,7 +30,16 @@ const owned = all.filter((g) => g.data.owned).length;
|
||||
const lede = s.pageText("/library/shelf")?.data.lede ?? "My board game collection. Filter by how it plays, sort by what hit the table last.";
|
||||
|
||||
const text = (g: Game) => searchText([g.title, g.data.blurb, ...(g.data.howItPlays ?? [])]);
|
||||
const visible = new Set(games.filter((g) => gameShows(g, f, text(g))).map((g) => g.slug));
|
||||
const hasPost = (g: Game) => s.backlinks(g, { type: "post" }).length > 0;
|
||||
const visible = new Set(games.filter((g) => gameShows(g, f, text(g), hasPost(g))).map((g) => g.slug));
|
||||
// Players and play time come from BoardGameGeek; their pills wait for a sync.
|
||||
const synced = { players: all.some((g) => playersOf(g)), time: all.some((g) => minutesOf(g) !== undefined) };
|
||||
const anyPost = all.some(hasPost);
|
||||
/** Games shown before "Show all", while no filter narrows the list. */
|
||||
const FIRST = 12;
|
||||
const narrowed = !!(f.q || f.plays.size || f.want || !f.owned || f.favorites || f.blog || f.players || f.time);
|
||||
const folded = !narrowed && visible.size > FIRST;
|
||||
const firstShown = new Set(games.filter((g) => visible.has(g.slug)).slice(0, FIRST).map((g) => g.slug));
|
||||
|
||||
// On the table: board-game statuses, then the most recent play not already listed.
|
||||
const table: { label: string; game: Game; note?: string; live: boolean }[] = onTheTable(s).map(({ status, game }) => ({
|
||||
@@ -50,43 +58,42 @@ const sortHref = (key: string) => {
|
||||
};
|
||||
---
|
||||
<PageShell title="The Shelf" family="shelf" current="library" description="Greg's board game collection: what is on the table, how each one plays, and what hit the table last.">
|
||||
<Fragment slot="hero"><LibraryHero current="shelf" lede={lede} /></Fragment>
|
||||
|
||||
<section aria-labelledby="sh-table" class="top">
|
||||
<div class="count gk-featured">
|
||||
<StarChartArt class="count-art" ratio="auto" seal="tria-prima" />
|
||||
<GiltCorners />
|
||||
<p class="count-text"><span class="num">{owned}</span><span>{owned === 1 ? "game" : "games"} on the shelf</span></p>
|
||||
</div>
|
||||
<div class="table">
|
||||
<div class="kick">
|
||||
<h2 id="sh-table"><Star size={14} />On the table</h2>
|
||||
<TickScale width={150} height={10} count={30} />
|
||||
<Fragment slot="hero">
|
||||
<LibraryHero current="shelf" lede={lede} />
|
||||
<section aria-labelledby="sh-table" class="top">
|
||||
<div class="count gk-featured">
|
||||
<StarChartArt class="count-art" ratio="auto" seal="tria-prima" />
|
||||
<GiltCorners />
|
||||
<p class="count-text"><span class="num">{owned}</span><span>{owned === 1 ? "game" : "games"} on the shelf</span></p>
|
||||
</div>
|
||||
{
|
||||
table.length > 0 ? (
|
||||
<ul class="table-list">
|
||||
{table.map((t) => (
|
||||
<li>
|
||||
<a href={`#${t.game.slug}`} class:list={["gk-card", "table-card", { live: t.live }]}>
|
||||
<StarChartArt class="table-art" ratio="1 / 1" seal="tria-prima" src={mediaSrc(t.game.data.cover)} />
|
||||
<span class="table-body">
|
||||
<span class="table-label">{t.label}</span>
|
||||
<span class="table-title">{t.game.title}</span>
|
||||
{t.note && <span class="table-note">{t.note}</span>}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="quiet">Nothing on the table right now.</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="divide"><Divider seed="shelf-list" /></div>
|
||||
<div class="table">
|
||||
<div class="kick">
|
||||
<h2 id="sh-table"><Star size={14} />On the table</h2>
|
||||
<TickScale width={150} height={10} count={30} />
|
||||
</div>
|
||||
{
|
||||
table.length > 0 ? (
|
||||
<ul class="table-list">
|
||||
{table.map((t) => (
|
||||
<li>
|
||||
<a href={`#${t.game.slug}`} class:list={["gk-card", "table-card", { live: t.live }]}>
|
||||
<StarChartArt class="table-art" ratio="1 / 1" seal="tria-prima" src={mediaSrc(t.game.data.cover)} />
|
||||
<span class="table-body">
|
||||
<span class="table-label">{t.label}</span>
|
||||
<span class="table-title">{t.game.title}</span>
|
||||
{t.note && <span class="table-note">{t.note}</span>}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="quiet">Nothing on the table right now.</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</Fragment>
|
||||
|
||||
<FilterLayout action="/library/shelf" formId="shelf-filters">
|
||||
<Fragment slot="rail">
|
||||
@@ -99,11 +106,29 @@ const sortHref = (key: string) => {
|
||||
</RailGroup>
|
||||
)
|
||||
}
|
||||
{
|
||||
synced.players && (
|
||||
<RailGroup legend="Players" pills>
|
||||
<label class="gk-filter pill"><input type="radio" name="players" value="" checked={!f.players} class="sr" /><span class="gk-fs star"><Star size={9} /></span>Any</label>
|
||||
{playerOptions.map((o) => <label class="gk-filter pill"><input type="radio" name="players" value={o.key} checked={f.players === o.key} class="sr" /><span class="gk-fs star"><Star size={9} /></span>{o.label}</label>)}
|
||||
</RailGroup>
|
||||
)
|
||||
}
|
||||
{
|
||||
synced.time && (
|
||||
<RailGroup legend="Play time" pills>
|
||||
<label class="gk-filter pill"><input type="radio" name="time" value="" checked={!f.time} class="sr" /><span class="gk-fs star"><Star size={9} /></span>Any</label>
|
||||
{timeOptions.map((o) => <label class="gk-filter pill"><input type="radio" name="time" value={o.key} checked={f.time === o.key} class="sr" /><span class="gk-fs star"><Star size={9} /></span>{o.label}</label>)}
|
||||
</RailGroup>
|
||||
)
|
||||
}
|
||||
<RailGroup legend="Show">
|
||||
<label class="check"><input type="checkbox" name="owned" value="1" checked={f.owned} />Owned</label>
|
||||
<label class="check"><input type="checkbox" name="want" value="1" checked={f.want} />Want to play</label>
|
||||
<label class="check"><input type="checkbox" name="fav" value="1" checked={f.favorites} />Favorites only</label>
|
||||
{anyPost && <label class="check"><input type="checkbox" name="blog" value="1" checked={f.blog} />Has a blog post</label>}
|
||||
</RailGroup>
|
||||
{(synced.players || synced.time) && <p class="rail-note">Player counts and play times come from BoardGameGeek.</p>}
|
||||
</Fragment>
|
||||
|
||||
<section aria-label="Games" class="games">
|
||||
@@ -115,8 +140,9 @@ const sortHref = (key: string) => {
|
||||
</div>
|
||||
</div>
|
||||
<ul class="grid" data-grid>
|
||||
{games.map((g) => <GameCard game={g} site={s} hidden={!visible.has(g.slug)} />)}
|
||||
{games.map((g) => <GameCard game={g} site={s} hidden={!visible.has(g.slug) || (folded && !firstShown.has(g.slug))} />)}
|
||||
</ul>
|
||||
<button type="button" class="gk-text more" data-more hidden={!folded}>Show all {visible.size}</button>
|
||||
<p class="quiet" data-empty hidden={visible.size > 0}>No games on the Shelf match these filters.</p>
|
||||
</section>
|
||||
</FilterLayout>
|
||||
@@ -252,8 +278,36 @@ const sortHref = (key: string) => {
|
||||
font-size: 13px;
|
||||
color: var(--gk-muted);
|
||||
}
|
||||
.divide {
|
||||
padding: 24px 0 4px;
|
||||
.rail-note {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--gk-muted);
|
||||
}
|
||||
.sr {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.pill {
|
||||
position: relative;
|
||||
}
|
||||
.pill:has(:checked) {
|
||||
border-color: var(--gk-gold);
|
||||
background: var(--gk-gold-tint);
|
||||
color: var(--gk-text);
|
||||
}
|
||||
.pill:has(:checked) .star {
|
||||
display: inline-flex;
|
||||
}
|
||||
.pill:has(:focus-visible) {
|
||||
outline: 2px solid var(--gk-gold);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
.more {
|
||||
align-self: center;
|
||||
}
|
||||
.games {
|
||||
display: flex;
|
||||
@@ -322,7 +376,13 @@ const sortHref = (key: string) => {
|
||||
played: desc("lp"),
|
||||
az: (a, b) => title(a).localeCompare(title(b)),
|
||||
added: desc("c"),
|
||||
time: (a, b) => Number(a.dataset.t || Infinity) - Number(b.dataset.t || Infinity) || title(a).localeCompare(title(b)),
|
||||
};
|
||||
const FIRST = 12;
|
||||
const more = document.querySelector<HTMLButtonElement>("[data-more]");
|
||||
const players: Record<string, [number, number]> = { "1": [1, 1], "2": [2, 2], "3-4": [3, 4], "5+": [5, Infinity] };
|
||||
const times: Record<string, [number, number]> = { short: [0, 29], mid: [30, 60], long: [61, Infinity] };
|
||||
let expanded = false;
|
||||
|
||||
const apply = () => {
|
||||
const data = new FormData(form);
|
||||
@@ -331,16 +391,29 @@ const sortHref = (key: string) => {
|
||||
const owned = data.has("owned");
|
||||
const want = data.has("want");
|
||||
const fav = data.has("fav");
|
||||
const blog = data.has("blog");
|
||||
const seat = players[String(data.get("players") ?? "")];
|
||||
const time = times[String(data.get("time") ?? "")];
|
||||
const narrowed = !!(q || plays.length || want || !owned || fav || blog || seat || time);
|
||||
let n = 0;
|
||||
for (const c of cards) {
|
||||
for (const c of [...grid.children] as HTMLElement[]) {
|
||||
const d = c.dataset;
|
||||
const [lo, hi] = (d.pl ?? "").split("-").map(Number);
|
||||
const t = d.t ? Number(d.t) : NaN;
|
||||
const ok =
|
||||
(!(owned || want) || (owned && !!d.o) || (want && !!d.w)) &&
|
||||
(!fav || !!d.f) &&
|
||||
(!blog || !!d.b) &&
|
||||
(!seat || (!!d.pl && lo! <= seat[1] && hi! >= seat[0])) &&
|
||||
(!time || (t >= time[0] && t <= time[1])) &&
|
||||
(!plays.length || (d.p ?? "").split("|").some((p) => plays.includes(p))) &&
|
||||
(!q || text.get(c)!.includes(q));
|
||||
c.hidden = !ok;
|
||||
if (ok) n++;
|
||||
c.hidden = !ok || (!narrowed && !expanded && n > FIRST);
|
||||
}
|
||||
if (more) {
|
||||
more.hidden = narrowed || expanded || n <= FIRST;
|
||||
more.textContent = `Show all ${n}`;
|
||||
}
|
||||
if (count) count.textContent = String(n);
|
||||
if (noun) noun.textContent = n === 1 ? "game" : "games";
|
||||
@@ -350,8 +423,24 @@ const sortHref = (key: string) => {
|
||||
const sort = (key: string) => {
|
||||
sortInput.value = key;
|
||||
grid.append(...[...cards].sort(orders[key] ?? orders.played));
|
||||
apply();
|
||||
};
|
||||
|
||||
more?.addEventListener("click", () => {
|
||||
expanded = true;
|
||||
apply();
|
||||
});
|
||||
// Opening an anchor to a folded-away card unfolds the grid first.
|
||||
const reveal = () => {
|
||||
const target = location.hash && document.getElementById(decodeURIComponent(location.hash.slice(1)));
|
||||
if (target && target.hidden && cards.includes(target)) {
|
||||
expanded = true;
|
||||
apply();
|
||||
target.scrollIntoView();
|
||||
}
|
||||
};
|
||||
addEventListener("hashchange", reveal);
|
||||
reveal();
|
||||
form.addEventListener("input", apply);
|
||||
form.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -38,7 +38,7 @@ const isCore = (group: { core?: string[] }, item: string) => group.core?.some((c
|
||||
<TickScale width={120} count={24} />
|
||||
</span>
|
||||
<Button href="/resume.pdf" download="Greg-Kilmer-Resume.pdf">
|
||||
<Icon name="arrow-right" size={18} />Download PDF
|
||||
<Icon name="download" size={18} />Download PDF
|
||||
</Button>
|
||||
{r.updated && <span class="pdf-date">PDF · updated {formatDate(r.updated)}</span>}
|
||||
<Button variant="secondary" href="/inquiries?type=job">Start an inquiry</Button>
|
||||
|
||||
@@ -17,7 +17,7 @@ import Swatches from "../../components/store/Swatches.astro";
|
||||
import Terms from "../../components/store/Terms.astro";
|
||||
import { site } from "../../lib/page.ts";
|
||||
import { formatDate, mediaSrc } from "../../lib/site.ts";
|
||||
import { canOrder, channelOf, hasCapacity, optionGroups, priceSummary, shopLink, storeState } from "../../lib/store.ts";
|
||||
import { canOrder, channelOf, hasCapacity, optionGroups, priceSummary, shopLink, storeNav, storeState } from "../../lib/store.ts";
|
||||
|
||||
const s = await site();
|
||||
const slug = Astro.params.slug ?? "";
|
||||
@@ -40,7 +40,7 @@ const groups = optionGroups(line);
|
||||
const body = s.render(line.body);
|
||||
const terms = [...(d.terms ?? []), ...st.terms];
|
||||
---
|
||||
<PageShell title={line.title} family="store" current="store" description={d.summary}>
|
||||
<PageShell title={line.title} family="store" current="store" store={storeNav(st, `/store/${line.slug}`)} description={d.summary}>
|
||||
<div class="crumb-row">
|
||||
<Breadcrumb here={d.category} />
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ import Swatches from "../../components/store/Swatches.astro";
|
||||
import Terms from "../../components/store/Terms.astro";
|
||||
import { site } from "../../lib/page.ts";
|
||||
import { formatDate } from "../../lib/site.ts";
|
||||
import { channelOf, optionGroups, storeState } from "../../lib/store.ts";
|
||||
import { channelOf, optionGroups, storeNav, storeState } from "../../lib/store.ts";
|
||||
|
||||
const s = await site();
|
||||
const st = storeState(s);
|
||||
@@ -38,7 +38,7 @@ const colorStrips = selling.flatMap((l) =>
|
||||
.filter((x) => x.opts.length),
|
||||
);
|
||||
---
|
||||
<PageShell title="Store" family="store" current="store" description={st.settings?.capacityNote ?? "Things I make, in small batches."}>
|
||||
<PageShell title="Store" family="store" current="store" store={storeNav(st, "/store")} description={st.settings?.capacityNote ?? "Things I make, in small batches."}>
|
||||
<Fragment slot="hero">
|
||||
<div class="hero">
|
||||
<div class="hero-main">
|
||||
|
||||
@@ -14,7 +14,7 @@ import LineStatus from "../../components/store/LineStatus.astro";
|
||||
import OrderForm from "../../components/store/OrderForm.astro";
|
||||
import { site } from "../../lib/page.ts";
|
||||
import { formatDate } from "../../lib/site.ts";
|
||||
import { canOrder, channelOf, storeState } from "../../lib/store.ts";
|
||||
import { canOrder, channelOf, storeNav, storeState } from "../../lib/store.ts";
|
||||
import { errorMessages, parseDraft, parseErrorCode } from "../../lib/forms.ts";
|
||||
import { turnstileSiteKey } from "../../lib/forms-server.ts";
|
||||
|
||||
@@ -35,7 +35,7 @@ const shown = orderable.length ? orderable : selling;
|
||||
const turnarounds = [...new Set(st.groups.map(([c]) => st.category(c).turnaround).filter((t): t is string => !!t))];
|
||||
const reopens = st.closed.filter((c) => c.reopens).map((c) => `${c.name} reopens ${formatDate(c.reopens)}.`);
|
||||
---
|
||||
<PageShell title="Order request" family="store" current="store" description="Send an order request for something from the Store.">
|
||||
<PageShell title="Order request" family="store" current="store" store={storeNav(st, "/store/order")} description="Send an order request for something from the Store.">
|
||||
{turnstileKey && <script slot="head" src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer is:inline></script>}
|
||||
<section class="intro" aria-labelledby="order-h1">
|
||||
<Breadcrumb here="Order request" />
|
||||
|
||||
@@ -13,7 +13,7 @@ import TickScale from "@gk/ui/components/TickScale.astro";
|
||||
import Breadcrumb from "../../../components/store/Breadcrumb.astro";
|
||||
import { parseOrderSent } from "../../../lib/forms.ts";
|
||||
import { site } from "../../../lib/page.ts";
|
||||
import { storeState } from "../../../lib/store.ts";
|
||||
import { storeNav, storeState } from "../../../lib/store.ts";
|
||||
|
||||
const sent = parseOrderSent(Astro.cookies.get("gk-order")?.value);
|
||||
if (sent) Astro.cookies.delete("gk-order", { path: "/" });
|
||||
@@ -21,8 +21,9 @@ Astro.response.headers.set("Cache-Control", "private, no-store");
|
||||
|
||||
const s = await site().catch(() => null);
|
||||
const replyTime = s?.settings()?.replyTime ?? "a few days";
|
||||
const line = sent?.lineSlug && s ? storeState(s).lines.find((l) => l.slug === sent.lineSlug) : undefined;
|
||||
const turnaround = line && s ? storeState(s).category(line.data.category).turnaround : undefined;
|
||||
const st = s ? storeState(s) : undefined;
|
||||
const line = sent?.lineSlug ? st?.lines.find((l) => l.slug === sent.lineSlug) : undefined;
|
||||
const turnaround = line ? st?.category(line.data.category).turnaround : undefined;
|
||||
const rows = [
|
||||
...(sent?.line ? [{ label: "Line", value: sent.line }] : []),
|
||||
...(sent?.rows ?? []),
|
||||
@@ -35,7 +36,7 @@ const steps = [
|
||||
{ title: "I start once you say yes", text: turnaround ?? "Nothing is made until we agree." },
|
||||
];
|
||||
---
|
||||
<PageShell title="Order request sent" family="store" current="store" description="Your order request is on its way.">
|
||||
<PageShell title="Order request sent" family="store" current="store" store={st ? storeNav(st, "/store/order") : undefined} description="Your order request is on its way.">
|
||||
<meta slot="head" name="robots" content="noindex" />
|
||||
<section class="intro" aria-labelledby="sent-h1">
|
||||
<Breadcrumb here="Request sent" />
|
||||
|
||||
@@ -269,7 +269,7 @@ const workshopImg = s.all("make").flatMap((m) => makePhotos(m))[0];
|
||||
|
||||
.cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(260px, 100%), 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
|
||||
gap: 16px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import Button from "@gk/ui/components/Button.astro";
|
||||
import Chip from "@gk/ui/components/Chip.astro";
|
||||
import FilterGroup from "@gk/ui/components/FilterGroup.astro";
|
||||
import PageShell from "@gk/ui/components/PageShell.astro";
|
||||
import Seal from "@gk/ui/components/Seal.astro";
|
||||
import StarChartArt from "@gk/ui/components/StarChartArt.astro";
|
||||
@@ -52,6 +53,16 @@ const nowItems: NowItem[] = s
|
||||
};
|
||||
});
|
||||
|
||||
// Kinds of make, from their tags (the sword rack tag is the Workshop's own):
|
||||
// pills over the grid once there are two or more to choose between.
|
||||
const kindsOf = (m: Note<"make">) => (m.data.tags ?? []).filter((t) => t.toLowerCase() !== "sword rack");
|
||||
const kinds = [...new Set(makes.flatMap(kindsOf))].sort((a, b) => a.localeCompare(b));
|
||||
const untagged = makes.some((m) => kindsOf(m).length === 0);
|
||||
const kindPills =
|
||||
kinds.length + (untagged ? 1 : 0) >= 2
|
||||
? [{ label: "All", value: "", active: true }, ...kinds.map((k) => ({ label: k, value: k.toLowerCase() })), ...(untagged ? [{ label: "Other", value: "-" }] : [])]
|
||||
: [];
|
||||
|
||||
const meta = [plural(makes.length, "make"), logs.length > 0 && plural(logs.length, "build log")].filter(Boolean).join(" · ");
|
||||
const postCover = (p: Note<"post">) => mediaSrc((p.data as { cover?: string }).cover);
|
||||
---
|
||||
@@ -98,6 +109,7 @@ const postCover = (p: Note<"post">) => mediaSrc((p.data as { cover?: string }).c
|
||||
}
|
||||
|
||||
<WorldSection id="cr-makes" title="Makes" intro={makes.length > 1 ? "Newest first." : undefined}>
|
||||
{kindPills.length > 0 && <div slot="action" class="kinds" data-kinds hidden><FilterGroup label="Kind of make" items={kindPills} /></div>}
|
||||
{
|
||||
makes.length > 0 ? (
|
||||
<ul class="makes">
|
||||
@@ -105,7 +117,7 @@ const postCover = (p: Note<"post">) => mediaSrc((p.data as { cover?: string }).c
|
||||
const photos = makePhotos(m);
|
||||
const log = s.backlinks(m, { type: "post" })[0];
|
||||
return (
|
||||
<li id={m.slug} class="make">
|
||||
<li id={m.slug} class="make" data-k={kindsOf(m).map((k) => k.toLowerCase()).join("|") || "-"}>
|
||||
<StarChartArt seal={artSeal} ratio="auto" src={photos[0]} alt={m.data.blurb ?? m.title} class="m-art" />
|
||||
{photos.length > 1 && (
|
||||
<span class="m-more">
|
||||
@@ -170,6 +182,23 @@ const postCover = (p: Note<"post">) => mediaSrc((p.data as { cover?: string }).c
|
||||
</CtaBand>
|
||||
</PageShell>
|
||||
|
||||
<script>
|
||||
// The kind pills filter the makes grid; without a script every make shows.
|
||||
const box = document.querySelector<HTMLElement>("[data-kinds]");
|
||||
if (box) {
|
||||
const pills = [...box.querySelectorAll<HTMLButtonElement>("button[data-value]")];
|
||||
const makes = [...document.querySelectorAll<HTMLElement>(".makes > li[data-k]")];
|
||||
box.hidden = false;
|
||||
for (const b of pills) {
|
||||
b.addEventListener("click", () => {
|
||||
const v = b.dataset.value ?? "";
|
||||
pills.forEach((p) => p.setAttribute("aria-pressed", String(p === b)));
|
||||
for (const m of makes) m.hidden = !!v && !(m.dataset.k ?? "").split("|").includes(v);
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.crumb {
|
||||
font-size: 11px;
|
||||
|
||||
Reference in New Issue
Block a user