library.ts

2.9 KB · raw

1import { type RepoStats, repoStats, type Save, timeline } from "../mirror/queries.ts";2import { publicRepoNames } from "../server/db.ts";3import { type Html, html } from "./html.ts";4import {5	openPublicRepo,6	type PageContext,7	type PublicRepo,8	plural,9	relativeTime,10} from "./layout.ts";11import { readmeAtHead, readmeIntro } from "./repo.ts";1213/** A public repo as the front page, browse and the profile show it. `saves` are oldest first. */14export type LibraryRepo = PublicRepo & { stats: RepoStats; description: string; saves: Save[] };1516export type RecentSave = { repo: string; save: Save };1718/** Every public repo in card order (§20: private ones never reach a page, count or card). */19export function publicLibrary(ctx: PageContext): LibraryRepo[] {20	return publicRepoNames(ctx.kepr).flatMap((name) => {21		const repo = openPublicRepo(ctx, name);22		if (!repo) return [];23		const description = readmeIntro(name, readmeAtHead(repo))?.text ?? "";24		return [{ ...repo, stats: repoStats(repo.mirror), description, saves: timeline(repo.mirror) }];25	});26}2728/** The newest saves across the library, checkpoints never among them. */29export function recentSaves(library: LibraryRepo[], limit: number): RecentSave[] {30	return library31		.flatMap((repo) => repo.saves.slice(-limit).map((save) => ({ repo: repo.settings.name, save })))32		.sort((a, b) => b.save.created_at - a.save.created_at)33		.slice(0, limit);34}3536export function repoCards(library: LibraryRepo[]): Html {37	return html`<div class="card-grid">${library.map(repoCard)}</div>`;38}3940function repoCard(repo: LibraryRepo): Html {41	const { name, category } = repo.settings;42	const { stats } = repo;43	return html`<a class="repo-card" href="/${name}">44<div class="repo-card-head"><span class="repo-card-name">${45		name46	}</span><span class="repo-card-saves mono">${plural(stats.saves, "save")}</span></div>47${repo.description && html`<p class="repo-card-desc">${repo.description}</p>`}48${ticks(repo.saves)}49<div class="repo-card-foot">${50		category && html`<span class="cat-chip">${category}</span>`51	}${stats.language && html`<span class="repo-card-lang">${stats.language}</span>`}${stats.updated !== null && html`<span class="repo-card-when">${relativeTime(stats.updated)}</span>`}</div>52</a>`;53}5455// §21.2's mini-timeline: one tick per save, placed by time like the repo page's dots.56function ticks(saves: Save[]): Html {57	const times = saves.map((save) => save.created_at);58	// Replayed saves keep their own times, so the line is not in time order.59	const first = times.reduce((a, b) => Math.min(a, b), times[0] ?? 0);60	const span = times.reduce((a, b) => Math.max(a, b), first) - first;61	const lines = saves.map((save) => {62		const x = span === 0 ? 50 : 1 + (98 * (save.created_at - first)) / span;63		return html`<line x1="${x.toFixed(2)}" x2="${x.toFixed(2)}" y1="9" y2="27"/>`;64	});65	return html`<svg class="repo-card-ticks" viewBox="0 0 100 36" preserveAspectRatio="none" aria-hidden="true">${66		lines67	}</svg>`;68}