import type { Hono } from "hono"; import { isSafePath } from "../bucket/repo.ts"; import { publicIssueCount } from "../issues/store.ts"; import { languageLines } from "../mirror/queries.ts"; import { publicDownloadCount } from "../releases/releases.ts"; import { setting } from "../server/db.ts"; import { type Html, html } from "./html.ts"; import { cacheFor, notFound, type PageContext, page, relativeTime } from "./layout.ts"; import { type LibraryRepo, publicLibrary, type RecentSave, recentSaves, repoCards, } from "./library.ts"; const HEX_COLUMNS = 48; const RECENT_ACTIVITY = 20; const BAR_LANGUAGES = 4; const HTTP_URL = /^https?:\/\//i; /** §21.2's profile of the one operator; any other handle is a 404. */ export function registerProfilePage(app: Hono, ctx: PageContext): void { app.get("/:handle{@[^/]+}", (c) => { const handle = setting(ctx.kepr, "handle") ?? ""; if (handle === "" || c.req.param("handle") !== `@${handle}`) return notFound(c, ctx); const library = publicLibrary(ctx); const bio = setting(ctx.kepr, "bio") ?? ""; cacheFor(c, false); return c.html( page(ctx, { title: `@${handle}`, description: bio || `@${handle}'s repositories.`, path: `/@${handle}`, body: html`
${cover(ctx, handle, recentSaves(library, HEX_COLUMNS))}
${identityCard(ctx, bio)} ${stats(ctx, library)} ${languageBar(library)}
${activity(recentSaves(library, RECENT_ACTIVITY))}
${repoCards(library)}
`, }), ); }); } function cover(ctx: PageContext, handle: string, recent: RecentSave[]): Html { const since = setting(ctx.kepr, "building_since"); const signature = setting(ctx.kepr, "signature"); // The backdrop repeats the newest IDs when there are fewer saves than columns. const columns = recent.length === 0 ? [] : Array.from({ length: HEX_COLUMNS }, (_, i) => recent[i % recent.length]?.save.id); // Without an uploaded signature, the handle in Sacramento stands in for it, as on Kepr 2. const name = signature && isSafePath(signature) && !signature.includes("/") ? html`${handle}` : handle; return html`

${name}

${since && html`

building since ${since}

`} ◈ operator
`; } function identityCard(ctx: PageContext, bio: string): Html | null { const location = setting(ctx.kepr, "location"); const link = setting(ctx.kepr, "link") ?? ""; const hasLink = HTTP_URL.test(link); if (!bio && !location && !hasLink) return null; return html`
${bio && html`

${bio}

`} ${location && html`

${location}

`} ${ hasLink && html`${link}` }
`; } function stats(ctx: PageContext, library: LibraryRepo[]): Html { const tiles: [string, number][] = [ ["repos", library.length], ["saves", library.reduce((sum, repo) => sum + repo.stats.saves, 0)], ["issues", publicIssueCount(ctx.kepr)], ["downloads", publicDownloadCount(ctx.kepr)], ]; return html`
${tiles.map( ([label, n]) => html`
${n.toLocaleString("en-US")}
${label}
`, )}
`; } // Lines per language summed across public repos, as SVG so the widths need no inline style (§25). function languageBar(library: LibraryRepo[]): Html | null { const lines = new Map(); for (const repo of library) { for (const [language, n] of languageLines(repo.mirror)) { lines.set(language, (lines.get(language) ?? 0) + n); } } const top = [...lines].sort((a, b) => b[1] - a[1]).slice(0, BAR_LANGUAGES); const total = top.reduce((sum, [, n]) => sum + n, 0); if (total === 0) return null; let x = 0; const segments = top.map(([language, n]) => { const width = (100 * n) / total; const segment = html`${language} ${Math.round(width)}%`; x += width; return segment; }); return html`
${ segments }
${top .slice(0, 3) .map(([language]) => html`${language}`)}
`; } function activity(recent: RecentSave[]): Html { if (recent.length === 0) return html`

no activity yet

`; return html`
${recent.map( ({ repo, save }) => html`
${repo}✦${save.message.split("\n")[0]}${relativeTime(save.created_at)}
`, )}
`; }