mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-07 19:58:24 +00:00
99ecdc2113
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
98 lines
4.1 KiB
TypeScript
98 lines
4.1 KiB
TypeScript
/**
|
|
* Helpers for the Workshop, craft and Tabletop pages: ordering, craft seals,
|
|
* make photos, the sword rack, and which pill a state wears.
|
|
*/
|
|
import type { SealName } from "@gk/ui/components/seals.ts";
|
|
import type { Note } from "./content.ts";
|
|
import { mediaSrc, type SiteIndex } from "./site.ts";
|
|
|
|
type Pill = "live" | "state" | "current" | "neutral";
|
|
|
|
/** By `order`, notes without one last, then by title. */
|
|
export function byOrder<T extends Note>(notes: T[]): T[] {
|
|
const o = (n: Note) => ((n.data as { order?: number }).order ?? Number.MAX_SAFE_INTEGER);
|
|
return [...notes].sort((a, b) => o(a) - o(b) || a.title.localeCompare(b.title));
|
|
}
|
|
|
|
/** Newest `date` first; undated notes keep their order after the dated ones. */
|
|
export function byDateDesc<T extends Note>(notes: T[]): T[] {
|
|
const d = (n: Note) => String((n.data as { date?: string }).date ?? "");
|
|
return [...notes].sort((a, b) => d(b).localeCompare(d(a)));
|
|
}
|
|
|
|
/** A craft's own seal: dice crafts, leather, print and paper. Undefined means the family seal. */
|
|
export function craftSeal(craft: Note<"craft">): SealName | undefined {
|
|
const text = `${craft.title} ${craft.data.blurb ?? ""}`.toLowerCase();
|
|
if (/leather/.test(text)) return "craft-leather";
|
|
if (/dice|resin/.test(text)) return "craft-dice";
|
|
if (/print|paper|bookbind|card|decal/.test(text)) return "craft-print";
|
|
return undefined;
|
|
}
|
|
|
|
/** A make's photos as image addresses (the `photos` list, else its `cover`). */
|
|
export function makePhotos(make: Note<"make">): string[] {
|
|
const d = make.data as { photos?: string[]; cover?: string };
|
|
const list = d.photos?.length ? d.photos : d.cover ? [d.cover] : [];
|
|
return list.map((p) => mediaSrc(p)).filter((x): x is string => !!x);
|
|
}
|
|
|
|
/** Makes tagged for the sword rack. */
|
|
export function isSword(make: Note<"make">): boolean {
|
|
const tags = make.data.tags ?? [];
|
|
return tags.some((t) => /sword/i.test(t));
|
|
}
|
|
|
|
/** The makes of a craft. */
|
|
export function makesOf(s: SiteIndex, craft: Note<"craft">): Note<"make">[] {
|
|
return s.all("make").filter((m) => s.byName(m.data.craft)?.path === craft.path);
|
|
}
|
|
|
|
/** Build logs for a craft: posts linking to it or its makes. */
|
|
export function buildLogs(s: SiteIndex, craft: Note<"craft">): Note<"post">[] {
|
|
return s.backlinks(craft, { type: "post" }) as Note<"post">[];
|
|
}
|
|
|
|
/** The tools of a craft, as notes where published, in the craft's order. */
|
|
export function toolsOf(s: SiteIndex, craft: Note<"craft">): { title: string; note?: Note<"tool"> }[] {
|
|
return (craft.data.tools ?? []).map((t) => {
|
|
const l = s.link(t);
|
|
return { title: l.title, note: l.note?.type === "tool" ? (l.note as Note<"tool">) : undefined };
|
|
});
|
|
}
|
|
|
|
/** The tools placed at a station. */
|
|
export function toolsAt(s: SiteIndex, station: Note<"station">): Note<"tool">[] {
|
|
return s.all("tool").filter((t) => s.byName(t.data.station)?.path === station.path);
|
|
}
|
|
|
|
export const craftPill = (status: string): Pill => (status === "Paused" ? "neutral" : "live");
|
|
|
|
export const systemPill = (state: string): Pill => (state === "running" ? "live" : "neutral");
|
|
|
|
/** Card game stances: the main event is gold, active ones live, the rest plain. */
|
|
export function stancePill(stance: string): Pill {
|
|
const s = stance.toLowerCase();
|
|
if (s === "main event") return "current";
|
|
if (/getting into|exploring|playing|learning/.test(s)) return "live";
|
|
return "neutral";
|
|
}
|
|
|
|
/** "want to run" -> "Want to run". */
|
|
export const sentence = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
|
|
/** Order systems are grouped in. */
|
|
export const systemStates = ["running", "played", "want to run", "want to play", "want to try"] as const;
|
|
|
|
/** Statuses whose kind belongs to a page (Making and Learning for /workshop). */
|
|
export function statusesFor(s: SiteIndex, page: string) {
|
|
return s.statuses().filter((st) => s.kind(st.data.kind).page === page);
|
|
}
|
|
|
|
/** Archidekt deck address. */
|
|
export const archidektUrl = (id: number) => `https://archidekt.com/decks/${id}`;
|
|
|
|
/** Deck's list address: Archidekt when set, else its own list link. */
|
|
export function deckUrl(deck: Note<"deck">): string | undefined {
|
|
return deck.data.archidektId ? archidektUrl(deck.data.archidektId) : deck.data.listUrl;
|
|
}
|