mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-07 19:58:24 +00:00
57721efbcd
Shelf game cards show the post about the game, as Library rows already do. A make's chip prefers its own build log and says Blog for other posts. Post chips and Connected pages count the links past the first few, so a roundup linking a hundred items stays readable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
127 lines
5.3 KiB
TypeScript
127 lines
5.3 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 post a make links to: its own build log first, else the newest post about it. */
|
|
export function makeLog(s: SiteIndex, make: Note<"make">): Note<"post"> | undefined {
|
|
const posts = s.backlinks(make, { type: "post" }) as Note<"post">[];
|
|
return posts.find((p) => p.data.postType === "build log") ?? posts[0];
|
|
}
|
|
|
|
/** 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}`;
|
|
|
|
/** Values Archidekt syncs onto a deck (see withSynced in content.ts). */
|
|
interface DeckSynced {
|
|
commanderArt?: string;
|
|
colors?: string[];
|
|
commanders?: string[];
|
|
}
|
|
|
|
/** The deck's own cover, else its synced commander art. */
|
|
export function deckArt(deck: Note<"deck">): string | undefined {
|
|
const d = deck.data as DeckSynced & { cover?: string };
|
|
return mediaSrc(d.cover ?? d.commanderArt);
|
|
}
|
|
|
|
const COLOR_NAMES: Record<string, string> = { W: "White", U: "Blue", B: "Black", R: "Red", G: "Green" };
|
|
|
|
/** The deck's color identity as names, WUBRG order; empty when not synced. */
|
|
export function deckColors(deck: Note<"deck">): string[] {
|
|
return ((deck.data as DeckSynced).colors ?? []).map((c) => COLOR_NAMES[c]).filter((c): c is string => !!c);
|
|
}
|
|
|
|
/** The deck's commanders, when synced. */
|
|
export const deckCommanders = (deck: Note<"deck">): string[] => (deck.data as DeckSynced).commanders ?? [];
|
|
|
|
/** 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;
|
|
}
|