initial commit

This commit is contained in:
Hunter
2026-11-01 13:23:44 -05:00
commit e03ea4f770
53 changed files with 10802 additions and 0 deletions

View File

@@ -0,0 +1,35 @@
import { HttpError } from "@/lib/errors";
import { readJson, route } from "@/lib/http";
import { categoryCounts, mutatePosts, readPosts } from "@/lib/store";
import { SLUG_RE } from "@/lib/validate";
export const dynamic = "force-dynamic";
export const GET = route(async () => {
const { posts, revision } = await readPosts();
return Response.json({ categories: categoryCounts(posts), revision });
});
/**
* Body: { from, to, revision? }. Moves every post in `from` to `to`; if `to`
* is already in use this is a merge. Categories are derived from posts, so a
* category disappears once no post uses it.
*/
export const PATCH = route(async (req) => {
const { from, to, revision: expected } = await readJson<{ from?: string; to?: string; revision?: string }>(req);
const target = to?.trim() ?? "";
if (!from) throw new HttpError(400, "from is required");
if (!SLUG_RE.test(target)) throw new HttpError(400, "category must be lowercase kebab-case");
const { result, revision } = await mutatePosts(expected, (posts) => {
let changed = 0;
for (const post of posts) {
if (post.category !== from) continue;
post.category = target;
changed++;
}
if (!changed) throw new HttpError(404, `no posts in category "${from}"`);
return changed;
});
return Response.json({ changed: result, revision });
});