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${38 latest.length > 0 &&39 html`<section class="featured"><h2 class="featured-label mono">latest offered</h2>${repoCards(latest)}</section>`40}41<p class="landing-self-host mono"><a href="/docs">run your own library →</a></p>`,42 }),43 );44 });45 app.get("/browse", (c) => {46 const library = publicLibrary(ctx);47 cacheFor(c, false);48 return c.html(49 page(ctx, {50 title: "browse",51 description: `Every public repository, ${plural(library.length, "repo")}.`,52 path: "/browse",53 body: html`<h1 class="file-page-title">browse</h1>54<p class="muted mono">${plural(library.length, "repo")}</p>55${56 library.length > 057 ? repoCards(library)58 : html`<p class="muted mono save-empty">nothing public yet</p>`59}`,60 }),61 );62 });63}64656667function ticker(library: LibraryRepo[]): Html {68 const messages = recentSaves(library, TICKER_ROWS * TICKER_ITEMS).map(69 ({ save }) => save.message.split("\n")[0] as string,70 );71 if (messages.length === 0) return html``;72 const rows = Array.from({ length: TICKER_ROWS }, (_, row) => {73 const items = Array.from(74 { length: TICKER_ITEMS },75 (_, i) => messages[(row * 3 + i) % messages.length],76 );77 const spans = items.map((message) => html`<span>${message}</span>`);78 return html`<div class="ticker-row"><div class="ticker-track">${spans}${spans}</div></div>`;79 });80 return html`<div class="ticker" aria-hidden="true">${rows}</div>`;81}82838485function wordCloud(library: LibraryRepo[]): Html {86 const weights = new Map<string, number>();87 for (const repo of library) {88 const labels = languageLines(repo.mirror).map(([language]) => language);89 if (repo.settings.category) labels.push(repo.settings.category);90 for (const label of new Set(labels)) weights.set(label, (weights.get(label) ?? 0) + 1);91 }92 if (weights.size === 0) return html``;93 const max = Math.max(...weights.values());94 95 const tags = [...weights].sort((a, b) => scatter(a[0]) - scatter(b[0]));96 return html`<div class="headline-cloud" aria-label="file types and categories">${tags.map(97 ([label, weight]) =>98 html`<span class="tc-tag tc-${Math.ceil((weight / max) * 5)}">${label}</span>`,99 )}</div>`;100}101102function scatter(label: string): number {103 let hash = 0x9e3779b9;104 for (const char of label) hash = (Math.imul(31, hash) + (char.codePointAt(0) ?? 0)) | 0;105 return hash;106}