1import type { Database } from "bun:sqlite";2import type { Context } from "hono";3import { openMirror } from "../mirror/store.ts";4import { publicRepo, type RepoSettings, setting } from "../server/db.ts";5import { type Html, html } from "./html.ts";67export type PageContext = { domain: string; kepr: Database; mirrorDir: string };89export type PublicRepo = { settings: RepoSettings; mirror: Database };1011const HEAD_CACHE = "public, max-age=60";121314const SAVE_CACHE = "private, max-age=31536000";15const FONTS =16 "https://fonts.bunny.net/css?family=bellota:700|comfortaa:700|martian-mono:400|quicksand:400,600|sacramento:400&display=swap";171819export function openPublicRepo(ctx: PageContext, name: string): PublicRepo | null {20 const settings = publicRepo(ctx.kepr, name);21 return settings ? { settings, mirror: openMirror(ctx.mirrorDir, name) } : null;22}2324export function cacheFor(c: Context, atSave: boolean): void {25 c.header("Cache-Control", atSave ? SAVE_CACHE : HEAD_CACHE);26}2728export function notFound(c: Context, ctx: PageContext): Response {29 c.header("Cache-Control", "no-store");30 const body = html`<h1 class="file-page-title">Not found</h1>31 <p class="muted">Nothing lives at this address.</p>`;32 return c.html(page(ctx, { title: "Not found", description: "", path: c.req.path, body }), 404);33}3435type PageOptions = { title: string; description: string; path: string; body: Html };3637export function page(ctx: PageContext, options: PageOptions): string {38 const site = setting(ctx.kepr, "site_name") ?? "kepr";39 const branding = setting(ctx.kepr, "branding");40 41 const title = options.title ? `${options.title} — ${site}` : site;42 const url = `https://${ctx.domain}${options.path}`;43 return html`<!doctype html>44<html lang="en">45<head>46<meta charset="utf-8">47<meta name="viewport" content="width=device-width, initial-scale=1">48<title>${title}</title>49<meta name="description" content="${options.description}">50<meta property="og:site_name" content="${site}">51<meta property="og:type" content="website">52<meta property="og:url" content="${url}">53<meta property="og:title" content="${title}">54<meta property="og:description" content="${options.description}">55<meta name="twitter:card" content="summary">56<meta name="twitter:title" content="${title}">57<meta name="twitter:description" content="${options.description}">58<meta name="theme-color" content="#090706">59<link rel="canonical" href="${url}">60<link rel="icon" href="/favicon.svg" type="image/svg+xml">61<link rel="preconnect" href="https://fonts.bunny.net">62<link rel="stylesheet" href="${FONTS}">63<link rel="stylesheet" href="/static/css/kepr.css">64<script src="/static/js/kepr.js" defer></script>65</head>66<body>67<nav>68<a class="wordmark" href="/" aria-label="${site}"><svg viewBox="0 0 32 32" class="wordmark-icon" aria-hidden="true"><g class="tri-outer"><polygon points="16,3 27,23 5,23"/></g><g class="tri-inner"><polygon points="16,10 22,21 10,21"/></g></svg></a>69${branding && html`<span class="wordmark-by">${branding}</span>`}70<span class="nav-links"><a href="/browse">browse</a><a href="/docs">docs</a><a href="/support">support</a></span>71</nav>72<main>73${options.body}74</main>75<footer><div class="credit">${site}</div></footer>76</body>77</html>`.value;78}7980const UNITS: [number, string][] = [81 [365 * 86400, "year"],82 [30 * 86400, "month"],83 [7 * 86400, "week"],84 [86400, "day"],85 [3600, "hour"],86 [60, "minute"],87];8889export function relativeTime(unixSeconds: number, now = Date.now() / 1000): string {90 const elapsed = now - unixSeconds;91 for (const [seconds, unit] of UNITS) {92 const n = Math.floor(elapsed / seconds);93 if (n >= 1) return `${n} ${unit}${n === 1 ? "" : "s"} ago`;94 }95 return "just now";96}9798export function formatDate(unixSeconds: number): string {99 return new Date(unixSeconds * 1000).toISOString().slice(0, 16).replace("T", " ");100}101102export function plural(n: number, noun: string): string {103 return `${n.toLocaleString("en-US")} ${noun}${n === 1 ? "" : "s"}`;104}105106export function formatSize(bytes: number): string {107 if (bytes < 1024) return `${bytes} B`;108 if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;109 return `${(bytes / 1024 / 1024).toFixed(1)} MB`;110}111112113export function pathUrl(path: string): string {114 return path.split("/").map(encodeURIComponent).join("/");115}