front.ts

4.0 KB · raw

1import type { Hono } from "hono";2import { languageLines, objectHashes } from "../mirror/queries.ts";3import { setting } from "../server/db.ts";4import { type Html, html } from "./html.ts";5import { cacheFor, type PageContext, page, plural } from "./layout.ts";6import { type LibraryRepo, publicLibrary, recentSaves, repoCards } from "./library.ts";78const TICKER_ROWS = 7;9const TICKER_ITEMS = 10;10const LATEST_OFFERED = 4;1112/** §21.1's front page and §21.3's browse. */13export function registerFrontPages(app: Hono, ctx: PageContext): void {14	app.get("/", (c) => {15		const library = publicLibrary(ctx);16		const tagline = setting(ctx.kepr, "tagline") ?? "";17		// §21.1: distinct objects across every public repo's saves, as Kepr 2 counted object refs.18		const known = new Set(library.flatMap((repo) => objectHashes(repo.mirror))).size;19		const latest = library20			.filter((repo) => repo.stats.updated !== null)21			.sort((a, b) => (b.stats.updated ?? 0) - (a.stats.updated ?? 0))22			.slice(0, LATEST_OFFERED);23		cacheFor(c, false);24		return c.html(25			page(ctx, {26				title: "",27				description: tagline,28				path: "/",29				body: html`${ticker(library)}30<section class="headline">31<div class="headline-text">32<h1>The library knows<br>${plural(known, "thing")}.</h1>33${tagline && html`<p class="headline-tagline">${tagline}</p>`}34</div>35${wordCloud(library)}36</section>37${latest.length > 0 && html`<section class="featured"><h2 class="featured-label mono">latest offered</h2>${repoCards(latest)}</section>`}38<p class="landing-self-host mono"><a href="/docs">run your own library →</a></p>`,39			}),40		);41	});42	app.get("/browse", (c) => {43		const library = publicLibrary(ctx);44		cacheFor(c, false);45		return c.html(46			page(ctx, {47				title: "browse",48				description: `Every public repository, ${plural(library.length, "repo")}.`,49				path: "/browse",50				body: html`<h1 class="file-page-title">browse</h1>51<p class="muted mono">${plural(library.length, "repo")}</p>52${library.length > 0 ? repoCards(library) : html`<p class="muted mono save-empty">nothing public yet</p>`}`,53			}),54		);55	});56}5758// Rows of recent save messages drifting at different depths; the motion is CSS, and stops under59// reduced motion. Each row's messages are written twice so sliding by half its width loops.60function ticker(library: LibraryRepo[]): Html {61	const messages = recentSaves(library, TICKER_ROWS * TICKER_ITEMS).map(62		({ save }) => save.message.split("\n")[0] as string,63	);64	if (messages.length === 0) return html``;65	const rows = Array.from({ length: TICKER_ROWS }, (_, row) => {66		const items = Array.from(67			{ length: TICKER_ITEMS },68			(_, i) => messages[(row * 3 + i) % messages.length],69		);70		const spans = items.map((message) => html`<span>${message}</span>`);71		return html`<div class="ticker-row"><div class="ticker-track">${spans}${spans}</div></div>`;72	});73	return html`<div class="ticker" aria-hidden="true">${rows}</div>`;74}7576// §21.1: file types (the header's languages) mixed with categories, each weighed by how many77// public repos have it, as Kepr 2 weighed its cloud.78function wordCloud(library: LibraryRepo[]): Html {79	const weights = new Map<string, number>();80	for (const repo of library) {81		const labels = languageLines(repo.mirror).map(([language]) => language);82		if (repo.settings.category) labels.push(repo.settings.category);83		for (const label of new Set(labels)) weights.set(label, (weights.get(label) ?? 0) + 1);84	}85	if (weights.size === 0) return html``;86	const max = Math.max(...weights.values());87	// Ordered by a hash of the label, so the big words scatter instead of leading the cloud.88	const tags = [...weights].sort((a, b) => scatter(a[0]) - scatter(b[0]));89	return html`<div class="headline-cloud" aria-label="file types and categories">${tags.map(([label, weight]) => html`<span class="tc-tag tc-${Math.ceil((weight / max) * 5)}">${label}</span>`)}</div>`;90}9192function scatter(label: string): number {93	let hash = 0x9e3779b9;94	for (const char of label) hash = (Math.imul(31, hash) + (char.codePointAt(0) ?? 0)) | 0;95	return hash;96}