feat: comprehensive CMS improvements
CI / runtime-diagnostics (push) Skipped
CI / release (push) Skipped
CI / check (push) Failing after 0s
CI / deploy (push) Skipped

- Fix DOMPurify SSR crash (use isomorphic-dompurify)
- Fix SanitizedHtml to sanitize by default
- Add auth guards to studio/catalog maintenance pages
- Add update/edit to vouchers CRUD
- Add update/edit to rare-values CRUD
- Add approve workflow to applications page
- Add edit form to guilds detail page
- Add SEO metadata to all public pages (21 pages)
- Fix mobile nav accessibility (focus trap, aria attributes)
- Fix missing labels and table accessibility
- Add dynamic imports for heavy client components (6 components)
- Fix silent error swallowing (40+ locations)
- Add content scheduling for articles (publishAt, status)
- Wire up 12 missing webhook notification triggers
- Add global search to admin panel
- Add bulk actions to admin users table
- Fix JSON formatting and a11y issues
This commit is contained in:
openhands committed 2026-09-03 16:00:32 +02:00
1 parent b810b16672
commit 30c95b1a5c
78 files changed
+2252 -134

No files matched your search

+34 -2
View File
@@ -12,6 +12,7 @@ import {
} from "@/lib/db";
import { slugify } from "@/lib/format";
import { PERMS } from "@/lib/permissions";
import { notify } from "@/lib/services/webhook";
async function uniqueSlug(title: string): Promise<string> {
const base = slugify(title);
@@ -43,12 +44,16 @@ export async function createArticle(formData: FormData): Promise<void> {
.normalize("NFC")
.trim();
const rawSlug = String(formData.get("slug") ?? "").trim();
const status = String(formData.get("status") ?? "published");
const rawPublishAt = String(formData.get("publishAt") ?? "").trim();
if (!title) return;
try {
const now = new Date();
const publishAt = rawPublishAt ? new Date(rawPublishAt) : null;
const slug = rawSlug ? await uniqueSlug(rawSlug) : await uniqueSlug(title);
await db.insert(WebsiteArticles).values({
slug: rawSlug ? await uniqueSlug(rawSlug) : await uniqueSlug(title),
slug,
title: title.slice(0, 255),
shortStory: shortStory.slice(0, 255),
fullStory,
@@ -56,6 +61,16 @@ export async function createArticle(formData: FormData): Promise<void> {
userId: staff.id,
createdAt: now,
updatedAt: now,
status: status || "published",
publishAt,
publishedAt: status === "published" ? now : null,
});
notify({
action: "news_publish",
actor: staff.username,
target: title,
details: slug,
});
} catch {
// Database error — re-render unchanged with error.
@@ -70,7 +85,10 @@ 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();
const status = String(formData.get("status") ?? "published");
const rawPublishAt = String(formData.get("publishAt") ?? "").trim();
try {
const publishAt = rawPublishAt ? new Date(rawPublishAt) : null;
await db
.update(WebsiteArticles)
.set({
@@ -90,6 +108,9 @@ export async function updateArticle(formData: FormData): Promise<void> {
.normalize("NFC")
.trim()
.slice(0, 255),
status,
publishAt,
publishedAt: status === "published" ? new Date() : undefined,
updatedAt: new Date(),
})
.where(eq(WebsiteArticles.id, id));
@@ -101,8 +122,13 @@ export async function updateArticle(formData: FormData): Promise<void> {
}
export async function deleteArticle(formData: FormData): Promise<void> {
await requirePermission(PERMS.NEWS_EDIT);
const staff = await requirePermission(PERMS.NEWS_EDIT);
const id = BigInt(String(formData.get("id")));
const [article] = await db
.select({ title: WebsiteArticles.title })
.from(WebsiteArticles)
.where(eq(WebsiteArticles.id, id))
.limit(1);
try {
await db.transaction(async (tx) => {
await tx
@@ -113,6 +139,12 @@ export async function deleteArticle(formData: FormData): Promise<void> {
.where(eq(WebsiteArticleComments.articleId, id));
await tx.delete(WebsiteArticles).where(eq(WebsiteArticles.id, id));
});
notify({
action: "news_delete",
actor: staff.username,
target: article?.title ?? String(id),
});
} catch {
redirect("/admin/articles?error=Delete failed");
}