repos.ts

3.8 KB · raw

1import type { Context, Hono } from "hono";2import { syncAll } from "../bucket/sync.ts";3import { html } from "../pages/html.ts";4import { formatDate } from "../pages/layout.ts";5import { type AdminContext, adminPage, formField, notice, textFields } from "./console.ts";6import { type AdminRepo, allRepos, findRepo, type RepoEdit, updateRepo } from "./store.ts";78const MAX_FIELD = 500;9const HTTP_URL = /^https?:\/\/[^\s]+$/i;10const SORT_ORDER = /^-?\d{1,6}$/;1112/** §24's Repos section: visibility (§20), card settings, sync status and "sync now". */13export function registerRepos(app: Hono, ctx: AdminContext): void {14	app.get("/admin/repos", (c) => c.html(reposPage(c, ctx, "")));1516	app.post("/admin/sync", async (c) => {17		await syncAll(ctx.sync);18		return c.redirect("/admin/repos", 303);19	});2021	app.post("/admin/repos/:repo", async (c) => {22		const repo = findRepo(ctx.kepr, c.req.param("repo"));23		if (!repo) return c.notFound();24		const fields = await textFields(c);25		const text = (name: string) => (fields[name] ?? "").trim();26		const edit: RepoEdit = {27			public: fields.public === "1" ? 1 : 0,28			category: text("category"),29			homepage: text("homepage"),30			sort_order: Number(text("sort_order") || "0"),31			install1: text("install1"),32			install2: text("install2"),33			install3: text("install3"),34		};35		const refuse = (error: string) => c.html(reposPage(c, ctx, `${repo.name}: ${error}`), 400);36		if (Object.values(fields).some((value) => value.length > MAX_FIELD)) {37			return refuse(`keep each field under ${MAX_FIELD} characters.`);38		}39		if (edit.homepage !== "" && !HTTP_URL.test(edit.homepage)) {40			return refuse("the homepage must be an http or https address.");41		}42		if (!SORT_ORDER.test(text("sort_order") || "0")) return refuse("the order must be a number.");43		if ([edit.install1, edit.install2, edit.install3].some((line) => /[\r\n]/.test(line))) {44			return refuse("an install line is one line.");45		}46		updateRepo(ctx.kepr, repo.name, edit);47		return c.redirect(`/admin/repos#repo-${repo.name}`, 303);48	});49}5051function reposPage(c: Context, ctx: AdminContext, error: string): string {52	const repos = allRepos(ctx.kepr);53	const body = html`${notice(error)}54<form method="post" action="/admin/sync">55${formField(c)}<button class="issues-submit" type="submit">sync now</button></form>56${repos.length === 0 && html`<p class="muted mono save-empty">no repos found in the bucket yet</p>`}57${repos.map((repo) => repoForm(c, repo))}`;58	return adminPage(c, ctx, "repos", body);59}6061function repoForm(c: Context, repo: AdminRepo) {62	const input = (name: keyof RepoEdit, label: string) =>63		html`<label class="issues-label" for="${repo.name}-${name}">${label}</label>64<input class="issues-input" id="${repo.name}-${name}" name="${name}" maxlength="${MAX_FIELD}" value="${String(repo[name])}">`;65	const synced =66		repo.last_sync_at === null ? "never synced" : `last sync ${formatDate(repo.last_sync_at)} UTC`;67	return html`<article class="issue-post" id="repo-${repo.name}">68<p class="issue-post-head mono">69<span class="issue-author">70${repo.public ? html`<a href="/${repo.name}">${repo.name}</a>` : repo.name}</span>71<span>${repo.public ? "public" : "private"}</span><span class="history-when">${synced}</span></p>72${73	repo.last_sync_error && html`<p class="issues-error mono">sync error: ${repo.last_sync_error}</p>`74}75<form class="issues-form" method="post" action="/admin/repos/${repo.name}">76${formField(c)}77<label class="issues-label">78<input type="checkbox" name="public" value="1"${repo.public ? html` checked` : ""}> public</label>79${input("category", "category")}80${input("homepage", "homepage")}81${input("sort_order", "card order")}82${input("install1", "install line 1")}83${input("install2", "install line 2")}84${input("install3", "install line 3")}85<button class="issues-submit" type="submit">save ${repo.name}</button>86</form>87</article>`;88}