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;111213export function registerFrontPages(app: Hono, ctx: PageContext): void {14 app.get("/", (c) => {15 const library = publicLibrary(ctx);16 const tagline = setting(ctx.kepr, "tagline") ?? "";17 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}57585960function 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}75767778function 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 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}