mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Phase 5: every page of the site, the resume PDF and RSS
Landing, About, Now, Resume (with a generated PDF), Work and case studies, Inquiries and the sent page, Library and the Shelf, Workshop and craft pages, Tabletop, Blog with topics and posts, RSS, the Store, product lines, the order form and the 404, all rendered from the content service's notes. Schema: experience previousRoles, skills stack core items, page stats, make tags and deck favorite. Icon names from notes map to brand icons in one place, and HTML comments are dropped when Markdown renders. 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:
@@ -0,0 +1,481 @@
|
||||
---
|
||||
import Button from "@gk/ui/components/Button.astro";
|
||||
import GiltCorners from "@gk/ui/components/GiltCorners.astro";
|
||||
import PageShell from "@gk/ui/components/PageShell.astro";
|
||||
import Seal from "@gk/ui/components/Seal.astro";
|
||||
import Star from "@gk/ui/components/Star.astro";
|
||||
import StarChartArt from "@gk/ui/components/StarChartArt.astro";
|
||||
import StatusPill from "@gk/ui/components/StatusPill.astro";
|
||||
import TickRing from "@gk/ui/components/TickRing.astro";
|
||||
import PageHero from "../../components/PageHero.astro";
|
||||
import CtaBand from "../../components/world/CtaBand.astro";
|
||||
import NowStrip, { type NowItem } from "../../components/world/NowStrip.astro";
|
||||
import WorldSection from "../../components/world/WorldSection.astro";
|
||||
import type { Note } from "../../lib/content.ts";
|
||||
import { plural, site } from "../../lib/page.ts";
|
||||
import { buildLogs, byDateDesc, byOrder, craftPill, craftSeal, isSword, makePhotos, makesOf, statusesFor, toolsAt, toolsOf } from "../../lib/world.ts";
|
||||
|
||||
const s = await site();
|
||||
const text = s.pageText("/workshop");
|
||||
const intro = s.render(text?.body);
|
||||
|
||||
const crafts = byOrder(s.all("craft")).map((craft) => {
|
||||
const makes = byDateDesc(makesOf(s, craft));
|
||||
const photos = makes.flatMap((m) => makePhotos(m).slice(0, 1)).slice(0, 3);
|
||||
return { craft, makes, photos, logs: buildLogs(s, craft).length, tools: toolsOf(s, craft).map((t) => t.title) };
|
||||
});
|
||||
|
||||
const stations = byOrder(s.all("station")).map((station) => ({ station, tools: toolsAt(s, station) }));
|
||||
const loose = s.all("tool").filter((t) => !s.byName(t.data.station));
|
||||
|
||||
const allMakes = s.all("make");
|
||||
const swords = allMakes.filter(isSword);
|
||||
const recent = byDateDesc(allMakes.filter((m) => !isSword(m)))
|
||||
.sort((a, b) => Number(!!b.data.date) - Number(!!a.data.date) || Number(makePhotos(b).length > 0) - Number(makePhotos(a).length > 0))
|
||||
.slice(0, 4);
|
||||
|
||||
const nowItems: NowItem[] = statusesFor(s, "/workshop").map((st) => {
|
||||
const subject = st.data.subject ? s.link(st.data.subject) : null;
|
||||
const img = subject?.note?.type === "make" ? makePhotos(subject.note as Note<"make">)[0] : undefined;
|
||||
return {
|
||||
id: st.slug,
|
||||
label: st.data.kind === "Making" ? "On the bench" : st.data.kind,
|
||||
title: subject?.title ?? st.title,
|
||||
note: st.data.note,
|
||||
href: subject?.href,
|
||||
img,
|
||||
};
|
||||
});
|
||||
---
|
||||
<PageShell title="Workshop" family="workshop" current="workshop" description={text?.data.lede ?? "What I make, what I make it with, and what I am learning next."}>
|
||||
<Fragment slot="hero">
|
||||
<PageHero eyebrow="My world" title={text?.title ?? "Workshop"} lede={text?.data.lede} />
|
||||
</Fragment>
|
||||
|
||||
{intro && <div class="gk-prose intro" set:html={intro} />}
|
||||
|
||||
{nowItems.length > 0 && <NowStrip id="ws-now" heading="Right now" items={nowItems} seal="craft-print" />}
|
||||
|
||||
{
|
||||
stations.length > 0 && (
|
||||
<WorldSection id="ws-space" title="The workspace" intro={`${plural(stations.length, "station")} around the office.`}>
|
||||
<ol class="stations">
|
||||
{stations.map(({ station, tools }) => (
|
||||
<li id={station.slug} class="station">
|
||||
<span class="st-head">
|
||||
<TickRing size={32}>{station.data.order}</TickRing>
|
||||
<h3>{station.title}</h3>
|
||||
</span>
|
||||
<span class="where">{station.data.where}</span>
|
||||
{tools.length > 0 && (
|
||||
<ul class="tools">
|
||||
{tools.map((t) => (
|
||||
<li id={t.slug} title={t.data.blurb}>
|
||||
<Star size={9} />
|
||||
{t.title}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
{station.data.goodFor && <span class="good">{station.data.goodFor}</span>}
|
||||
</li>
|
||||
))}
|
||||
{loose.length > 0 && (
|
||||
<li class="station">
|
||||
<span class="st-head">
|
||||
<TickRing size={32}><Star size={10} /></TickRing>
|
||||
<h3>Around the shop</h3>
|
||||
</span>
|
||||
<ul class="tools">
|
||||
{loose.map((t) => (
|
||||
<li id={t.slug}>
|
||||
<Star size={9} />
|
||||
{t.title}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
)}
|
||||
</ol>
|
||||
</WorldSection>
|
||||
)
|
||||
}
|
||||
|
||||
<WorldSection id="ws-crafts" title="Crafts" intro="Each craft keeps its own makes, tools and build logs, so the page stays short as the collection grows.">
|
||||
<ul class="crafts">
|
||||
{
|
||||
crafts.map(({ craft, makes, photos, logs, tools }) => (
|
||||
<li>
|
||||
<a href={s.href(craft) ?? undefined} class="gk-card craft">
|
||||
<span class="c-top">
|
||||
<h3 class="c-name">{craft.title}</h3>
|
||||
<StatusPill kind={craftPill(craft.data.status)}>{craft.data.status}</StatusPill>
|
||||
</span>
|
||||
<span class="thumbs" aria-hidden="true">
|
||||
{[0, 1, 2].map((i) =>
|
||||
photos[i] ? (
|
||||
<img src={photos[i]} alt="" loading="lazy" />
|
||||
) : (
|
||||
<span class="blank">
|
||||
<Seal name={craftSeal(craft) ?? "craft-print"} size={30} />
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</span>
|
||||
{craft.data.blurb && <span class="c-blurb">{craft.data.blurb}</span>}
|
||||
{tools.length > 0 && <span class="c-tools">{tools.join(" · ")}</span>}
|
||||
<span class="c-foot">
|
||||
<span>
|
||||
{plural(makes.length, "make")}
|
||||
{logs > 0 && ` · ${plural(logs, "build log")}`}
|
||||
</span>
|
||||
<span class="open">
|
||||
Open the craft
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true">
|
||||
<path d="M5 12h14M13 6l6 6-6 6" />
|
||||
</svg>
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</WorldSection>
|
||||
|
||||
{
|
||||
recent.length > 0 && (
|
||||
<WorldSection id="ws-recent" title="Recently finished">
|
||||
<ul class="recent">
|
||||
{recent.map((m) => {
|
||||
const found = s.byName(m.data.craft);
|
||||
const craft = found?.type === "craft" ? (found as Note<"craft">) : undefined;
|
||||
const log = s.backlinks(m, { type: "post" })[0];
|
||||
return (
|
||||
<li class="r-card">
|
||||
<a href={s.href(m) ?? undefined} class="r-art">
|
||||
<StarChartArt seal={(craft && craftSeal(craft)) ?? "craft-print"} ratio="auto" src={makePhotos(m)[0]} alt={m.data.blurb ?? m.title} class="r-img" />
|
||||
<span class="gk-visually-hidden">{m.title}</span>
|
||||
</a>
|
||||
<div class="r-txt">
|
||||
{craft && <span class="r-craft">{craft.title}</span>}
|
||||
<h3 class="r-name">
|
||||
<a href={s.href(m) ?? undefined}>{m.title}</a>
|
||||
</h3>
|
||||
{m.data.blurb && <span class="r-blurb">{m.data.blurb}</span>}
|
||||
{log && (
|
||||
<a href={s.href(log) ?? undefined} class="gk-text r-log">
|
||||
Blog: {log.title}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</WorldSection>
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
swords.length > 0 && (
|
||||
<section class="rack gk-featured" aria-labelledby="ws-swords">
|
||||
<GiltCorners />
|
||||
<div class="rack-words">
|
||||
<span class="rack-kick">The sword rack</span>
|
||||
<h2 id="ws-swords">Blades from the stories I love</h2>
|
||||
<span class="rack-line" aria-hidden="true" />
|
||||
<span class="rack-note">I always pick the sword class, and the spellsword when I can.</span>
|
||||
</div>
|
||||
<ul class="blades">
|
||||
{swords.map((m) => (
|
||||
<li>
|
||||
<Star size={10} />
|
||||
<a href={s.href(m) ?? undefined} title={m.data.blurb}>
|
||||
{m.title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
<CtaBand title="Want one of these?" note="Some of it is in the Store. Anything else can start as an inquiry.">
|
||||
<Button href="/store">Visit the Store</Button>
|
||||
<Button href="/inquiries" variant="secondary">Start an inquiry</Button>
|
||||
</CtaBand>
|
||||
</PageShell>
|
||||
|
||||
<style>
|
||||
.intro {
|
||||
max-width: 70ch;
|
||||
padding-top: 28px;
|
||||
color: var(--gk-soft);
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.intro :global(p) {
|
||||
margin: 0 0 14px;
|
||||
}
|
||||
|
||||
/* Stations */
|
||||
.stations {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.station {
|
||||
flex: 1 1 250px;
|
||||
min-width: 0;
|
||||
padding: 20px;
|
||||
background: var(--gk-midnight);
|
||||
border: 1px solid var(--gk-lines);
|
||||
border-top: 2px solid var(--gk-gold);
|
||||
border-radius: var(--gk-radius);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
scroll-margin-top: 24px;
|
||||
}
|
||||
.st-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.st-head h3 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.where {
|
||||
font-size: 13px;
|
||||
color: var(--gk-muted);
|
||||
}
|
||||
.tools {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
.tools li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.good {
|
||||
margin-top: auto;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--gk-lines);
|
||||
font-size: 13px;
|
||||
color: var(--gk-soft);
|
||||
}
|
||||
|
||||
/* Crafts */
|
||||
.crafts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr));
|
||||
gap: 16px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.craft {
|
||||
height: 100%;
|
||||
gap: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
.c-top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.c-name {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.thumbs {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.thumbs > * {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 72px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.thumbs .blank {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--gk-sky);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.c-blurb {
|
||||
font-size: 14px;
|
||||
color: var(--gk-soft);
|
||||
line-height: 1.55;
|
||||
}
|
||||
.c-tools {
|
||||
font-size: 13px;
|
||||
color: var(--gk-muted);
|
||||
}
|
||||
.c-foot {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--gk-soft);
|
||||
}
|
||||
.open {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--gk-teal-bright);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Recently finished */
|
||||
.recent {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
.r-card {
|
||||
flex: 1 1 240px;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--gk-midnight);
|
||||
border: 1px solid var(--gk-lines);
|
||||
border-radius: var(--gk-radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
.r-art {
|
||||
display: block;
|
||||
}
|
||||
.r-art :global(.r-img) {
|
||||
height: 170px;
|
||||
border-radius: 0;
|
||||
}
|
||||
.r-txt {
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.r-craft {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.3em;
|
||||
font-weight: 600;
|
||||
color: var(--gk-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.r-name {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
.r-name a {
|
||||
color: var(--gk-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
.r-name a:hover {
|
||||
color: var(--gk-gold-bright);
|
||||
}
|
||||
.r-blurb {
|
||||
font-size: 13px;
|
||||
color: var(--gk-muted);
|
||||
}
|
||||
.r-log {
|
||||
font-size: 13px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
/* Sword rack */
|
||||
.rack {
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
padding: 24px 28px;
|
||||
}
|
||||
.rack-words {
|
||||
flex: 1 1 300px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.rack-kick {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.3em;
|
||||
font-weight: 600;
|
||||
color: var(--gk-gold-bright);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.rack h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
.rack-line {
|
||||
display: block;
|
||||
width: 180px;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background:
|
||||
radial-gradient(circle at 18% 50%, var(--gk-teal-bright) 0 2px, transparent 3px),
|
||||
linear-gradient(90deg, var(--gk-gold), rgba(201, 162, 74, 0.15) 70%, transparent);
|
||||
box-shadow: 0 0 8px rgba(201, 162, 74, 0.35);
|
||||
}
|
||||
.rack-note {
|
||||
font-size: 14px;
|
||||
color: var(--gk-muted);
|
||||
}
|
||||
.blades {
|
||||
flex: 2 1 480px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
gap: 8px 16px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.blades li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.blades a {
|
||||
color: var(--gk-text);
|
||||
text-decoration: none;
|
||||
}
|
||||
.blades a:hover {
|
||||
color: var(--gk-gold-bright);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.rack {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
.blades {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user