feat: comprehensive CMS improvements
- 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:
1 parent
b810b16672
commit
30c95b1a5c
78 files changed
+2252
-134
No files matched your search
@@ -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");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user