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:
Claude
2026-10-12 03:33:15 +00:00
parent 3f6167455d
commit 151bed230a
45 changed files with 671 additions and 166 deletions
+57 -2
View File
@@ -128,6 +128,7 @@ export const sorts = [
{ key: "played", label: "Last played" },
{ key: "az", label: "A to Z" },
{ key: "added", label: "Recently added" },
{ key: "time", label: "Play time" },
] as const;
export type SortKey = (typeof sorts)[number]["key"];
@@ -138,6 +139,8 @@ export function sortGames(games: Game[], sort: SortKey): Game[] {
played: (a, b) => desc(a.data.lastPlayed, b.data.lastPlayed) || az(a, b),
az,
added: (a, b) => desc(a.data.created, b.data.created) || az(a, b),
// Shortest first; games BoardGameGeek has not timed go last.
time: (a, b) => (minutesOf(a) ?? Infinity) - (minutesOf(b) ?? Infinity) || az(a, b),
};
return [...games].sort(cmp[sort]);
}
@@ -148,9 +151,29 @@ export interface ShelfFilter {
owned: boolean;
want: boolean;
favorites: boolean;
blog: boolean;
players: PlayersKey | "";
time: TimeKey | "";
sort: SortKey;
}
/** The Players pills: who a game has to seat. */
export const playerOptions = [
{ key: "1", label: "1", lo: 1, hi: 1 },
{ key: "2", label: "2", lo: 2, hi: 2 },
{ key: "3-4", label: "3–4", lo: 3, hi: 4 },
{ key: "5+", label: "5+", lo: 5, hi: Infinity },
] as const;
export type PlayersKey = (typeof playerOptions)[number]["key"];
/** The Play time pills, in minutes. */
export const timeOptions = [
{ key: "short", label: "Under 30 min", lo: 0, hi: 29 },
{ key: "mid", label: "30–60", lo: 30, hi: 60 },
{ key: "long", label: "60+", lo: 61, hi: Infinity },
] as const;
export type TimeKey = (typeof timeOptions)[number]["key"];
export function shelfFilter(params: URLSearchParams): ShelfFilter {
const submitted = params.has("f");
return {
@@ -159,19 +182,27 @@ export function shelfFilter(params: URLSearchParams): ShelfFilter {
owned: submitted ? params.get("owned") === "1" : true,
want: params.get("want") === "1",
favorites: params.get("fav") === "1",
blog: params.get("blog") === "1",
players: playerOptions.find((o) => o.key === params.get("players"))?.key ?? "",
time: timeOptions.find((o) => o.key === params.get("time"))?.key ?? "",
sort: sorts.find((s) => s.key === params.get("sort"))?.key ?? "played",
};
}
/**
* Owned and Want to play add games to the list (either one); with neither
* ticked, every game shows. Favorites only and How it plays narrow it.
* ticked, every game shows. Favorites only, Has a blog post, How it plays,
* Players and Play time narrow it. A game BoardGameGeek has not synced yet
* drops out while Players or Play time is set.
*/
export function gameShows(g: Game, f: ShelfFilter, text: string): boolean {
export function gameShows(g: Game, f: ShelfFilter, text: string, hasPost = false): boolean {
const owned = !!g.data.owned;
const want = !!g.data.wantToPlay;
if ((f.owned || f.want) && !((f.owned && owned) || (f.want && want))) return false;
if (f.favorites && !g.data.favorite) return false;
if (f.blog && !hasPost) return false;
if (f.players && !seats(g, playerOptions.find((o) => o.key === f.players)!)) return false;
if (f.time && !lasts(g, timeOptions.find((o) => o.key === f.time)!)) return false;
if (f.plays.size && !(g.data.howItPlays ?? []).some((p) => f.plays.has(p.toLowerCase()))) return false;
return !f.q || text.includes(f.q);
}
@@ -185,6 +216,30 @@ interface GameSynced {
maxPlayTime?: number;
}
/** The player range BoardGameGeek gives, when it has synced. */
export function playersOf(g: Game): [number, number] | undefined {
const d = g.data as GameSynced;
const lo = d.minPlayers || d.maxPlayers;
const hi = d.maxPlayers || d.minPlayers;
return lo && hi ? [lo, hi] : undefined;
}
/** A game's usual length in minutes: the listed play time, else the longest. */
export function minutesOf(g: Game): number | undefined {
const d = g.data as GameSynced;
return d.playTime || d.maxPlayTime || d.minPlayTime || undefined;
}
/** Whether a game plays at some count in the option's range. */
const seats = (g: Game, o: { lo: number; hi: number }) => {
const r = playersOf(g);
return !!r && r[0] <= o.hi && r[1] >= o.lo;
};
const lasts = (g: Game, o: { lo: number; hi: number }) => {
const m = minutesOf(g);
return m !== undefined && m >= o.lo && m <= o.hi;
};
const range = (lo?: number, hi?: number) => (lo && hi && lo !== hi ? `${lo}–${hi}` : String(lo || hi || ""));
/** "2–4 players", "1 player" or undefined when BoardGameGeek has not been synced. */