feat(housekeeping): deliver content vertical

This commit is contained in:
Simo committed 2026-08-30 01:54:43 +02:00
1 parent bdab924bdf
commit fd68819d9b
67 files changed
+5742 -1916

No files matched your search

+39 -98
View File
@@ -1,120 +1,61 @@
"use server";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { requirePermission } from "@/lib/admin/guard";
import {
db,
WebsiteArticleComments,
WebsiteArticleReactions,
WebsiteArticles,
} from "@/lib/db";
import { slugify } from "@/lib/format";
contentMutationService,
createContentMutationInvocation,
} from "@/features/housekeeping/domains/content/services/mutations";
import { createCorrelationId } from "@/features/housekeeping/foundation/contracts";
import { requirePermission } from "@/lib/admin/guard";
import { PERMS } from "@/lib/permissions";
async function uniqueSlug(title: string): Promise<string> {
const base = slugify(title);
let slug = base;
let n = 2;
for (;;) {
const [existing] = await db
.select({ id: WebsiteArticles.id })
.from(WebsiteArticles)
.where(eq(WebsiteArticles.slug, slug))
.limit(1);
if (!existing) return slug;
slug = `${base}-${n++}`;
}
function articleInput(formData: FormData) {
return {
title: String(formData.get("title") ?? "").normalize("NFC").trim(),
shortStory: String(formData.get("shortStory") ?? "").normalize("NFC").trim(),
fullStory: String(formData.get("fullStory") ?? "").normalize("NFC").trim(),
image: String(formData.get("image") ?? "").normalize("NFC").trim(),
slug: String(formData.get("slug") ?? "").trim(),
};
}
export async function createArticle(formData: FormData): Promise<void> {
const staff = await requirePermission(PERMS.NEWS_EDIT);
const title = String(formData.get("title") ?? "")
.normalize("NFC")
.trim();
const shortStory = String(formData.get("shortStory") ?? "")
.normalize("NFC")
.trim();
const fullStory = String(formData.get("fullStory") ?? "")
.normalize("NFC")
.trim();
const image = String(formData.get("image") ?? "")
.normalize("NFC")
.trim();
const rawSlug = String(formData.get("slug") ?? "").trim();
if (!title) return;
try {
const now = new Date();
await db.insert(WebsiteArticles).values({
slug: rawSlug ? await uniqueSlug(rawSlug) : await uniqueSlug(title),
title: title.slice(0, 255),
shortStory: shortStory.slice(0, 255),
fullStory,
image: image.slice(0, 255),
userId: staff.id,
createdAt: now,
updatedAt: now,
});
} catch {
// Database error — re-render unchanged with error.
redirect(
"/admin/articles/new?error=Database error while creating article. Please try again.",
);
const input = articleInput(formData);
if (!input.title) return;
const result = await contentMutationService.execute(
createContentMutationInvocation(staff, createCorrelationId()),
"article.change",
{ action: "create", ...input },
);
if (!result.ok) {
redirect("/admin/articles/new?error=Database error while creating article. Please try again.");
}
redirect("/admin/articles");
}
export async function updateArticle(formData: FormData): Promise<void> {
await requirePermission(PERMS.NEWS_EDIT);
const id = BigInt(String(formData.get("id")));
const rawSlug = String(formData.get("slug") ?? "").trim();
try {
await db
.update(WebsiteArticles)
.set({
title: String(formData.get("title") ?? "")
.normalize("NFC")
.trim()
.slice(0, 255),
...(rawSlug ? { slug: await uniqueSlug(rawSlug) } : {}),
shortStory: String(formData.get("shortStory") ?? "")
.normalize("NFC")
.trim()
.slice(0, 255),
fullStory: String(formData.get("fullStory") ?? "")
.normalize("NFC")
.trim(),
image: String(formData.get("image") ?? "")
.normalize("NFC")
.trim()
.slice(0, 255),
updatedAt: new Date(),
})
.where(eq(WebsiteArticles.id, id));
} catch {
redirect("/admin/articles?error=Update failed");
}
revalidatePath(`/admin/articles/${id}`);
const staff = await requirePermission(PERMS.NEWS_EDIT);
const id = String(formData.get("id") ?? "");
const result = await contentMutationService.execute(
createContentMutationInvocation(staff, createCorrelationId()),
"article.change",
{ action: "update", id, ...articleInput(formData) },
);
if (!result.ok) redirect("/admin/articles?error=Update failed");
revalidatePath("/admin/articles/" + id);
redirect("/admin/articles");
}
export async function deleteArticle(formData: FormData): Promise<void> {
await requirePermission(PERMS.NEWS_EDIT);
const id = BigInt(String(formData.get("id")));
try {
await db.transaction(async (tx) => {
await tx
.delete(WebsiteArticleReactions)
.where(eq(WebsiteArticleReactions.articleId, id));
await tx
.delete(WebsiteArticleComments)
.where(eq(WebsiteArticleComments.articleId, id));
await tx.delete(WebsiteArticles).where(eq(WebsiteArticles.id, id));
});
} catch {
redirect("/admin/articles?error=Delete failed");
}
const staff = await requirePermission(PERMS.NEWS_EDIT);
const id = String(formData.get("id") ?? "");
const result = await contentMutationService.execute(
createContentMutationInvocation(staff, createCorrelationId()),
"article.change",
{ action: "delete", id },
);
if (!result.ok) redirect("/admin/articles?error=Delete failed");
redirect("/admin/articles");
}