"use server"; import * as z from "zod"; import type { AutosaveResponse } from "@/lib/editor/autosave"; import { createNamedRevision, saveDataSourceVersion, savePageDraft, } from "@/lib/content/mutations"; import { dataSourceSnapshotSchema } from "@/lib/content/data-source"; import { slugSchema } from "@/lib/content/lifecycle"; import type { DataSourceSnapshot, PageSnapshot } from "@/lib/content/types"; import { requireAdmin } from "@/lib/auth/server"; import { builtInTemplates } from "@/lib/templates"; import { persistTemplateApplication } from "@/lib/templates/persist"; import { createGuide, GuideVersionConflictError, permanentlyDeleteGuide, restoreGuide, runCatalogSync, saveArtifacts, saveConstellations, saveExtraSections, saveTeams, setGuideVisibility, saveWeapons, trashGuide, updateGuideBasics, } from "@/lib/guides/mutations"; const serializedBlockSchema = z.strictObject({ id: z.string().uuid(), type: z.string().min(1).max(64), schemaVersion: z.number().int().nonnegative(), config: z.unknown(), responsiveLayout: z.unknown().optional(), sortOrder: z.number().int().nonnegative(), }); const editorDraftSchema = z.strictObject({ pageId: z.string().uuid(), expectedVersion: z.number().int().positive(), title: z.string().max(300), slug: z.string().max(120), visible: z.boolean(), publicNote: z.string().max(10_000).nullable().optional(), blocks: z.array(serializedBlockSchema).max(500), }); export type EditorSaveInput = z.input; export type EditorSaveResult = AutosaveResponse; export async function saveEditorPage(input: unknown): Promise { const admin = await requireAdmin(); const parsed = editorDraftSchema.safeParse(input); if (!parsed.success) { return { status: "validation-error", issues: parsed.error.issues.map((issue) => issue.message), }; } if (process.env.BUZZ_DEMO_MODE === "true") { return { status: "saved", version: parsed.data.expectedVersion + 1 }; } try { const result = await savePageDraft({ ...parsed.data, authorId: admin.user.id, }); if (result.status === "accepted") { return { status: "saved", version: result.value.page.version, snapshot: result.value, }; } if (result.status === "not-found") { return { status: "validation-error", issues: ["ไม่พบหน้าที่ต้องการบันทึก"], }; } return result; } catch (cause) { return { status: "transient-error", message: cause instanceof Error ? cause.message : "บันทึกไม่สำเร็จ", }; } } export async function saveEditorDataSource( input: unknown, ): Promise> { const admin = await requireAdmin(); const parsed = dataSourceSnapshotSchema.safeParse(input); if (!parsed.success) { return { status: "validation-error", issues: parsed.error.issues.map((issue) => issue.message), }; } if (process.env.BUZZ_DEMO_MODE === "true") { return { status: "saved", version: parsed.data.version + 1, snapshot: { ...parsed.data, version: parsed.data.version + 1 }, }; } try { const result = await saveDataSourceVersion({ ...parsed.data, expectedVersion: parsed.data.version, authorId: admin.user.id, }); if (result.status === "accepted") { return { status: "saved", version: result.value.version, snapshot: result.value, }; } if (result.status === "not-found") { return { status: "validation-error", issues: ["ไม่พบแหล่งข้อมูล"] }; } return result; } catch (cause) { return { status: "transient-error", message: cause instanceof Error ? cause.message : "บันทึกข้อมูลไม่สำเร็จ", }; } } export async function createEditorRevision( pageId: string, label: string, ): Promise<{ status: "saved"; id: string } | { status: "error" }> { const admin = await requireAdmin(); const parsed = z.string().uuid().safeParse(pageId); const safeLabel = z.string().trim().min(1).max(160).safeParse(label); if (!parsed.success || !safeLabel.success) return { status: "error" }; if (process.env.BUZZ_DEMO_MODE === "true") { return { status: "saved", id: crypto.randomUUID() }; } const id = await createNamedRevision(parsed.data, safeLabel.data, admin.user.id); return id ? { status: "saved", id } : { status: "error" }; } const templateApplicationSchema = z.strictObject({ templateSlug: z.string().min(1).max(120), characterSlug: slugSchema, nameTh: z.string().trim().min(1).max(160), nameEn: z.string().trim().max(160).optional(), description: z.string().trim().max(2_000).optional(), }); export async function applyEditorTemplate(input: unknown): Promise< | { status: "created"; location: string } | { status: "validation-error"; issues: string[] } | { status: "error"; message: string } > { const admin = await requireAdmin(); const parsed = templateApplicationSchema.safeParse(input); if (!parsed.success) { return { status: "validation-error", issues: parsed.error.issues.map((issue) => issue.message), }; } const template = builtInTemplates.find( (candidate) => candidate.slug === parsed.data.templateSlug, ); if (!template) { return { status: "validation-error", issues: ["ไม่พบเทมเพลต"] }; } if (process.env.BUZZ_DEMO_MODE === "true") { return { status: "created", location: "/admin/astra/info" }; } try { const applied = await persistTemplateApplication( template, { slug: parsed.data.characterSlug, nameTh: parsed.data.nameTh, nameEn: parsed.data.nameEn, description: parsed.data.description, }, admin.user.id, ); return { status: "created", location: `/admin/${applied.characterSlug}/${applied.firstPageSlug}`, }; } catch (cause) { return { status: "error", message: cause instanceof Error ? cause.message : "สร้างคู่มือไม่สำเร็จ", }; } } type GuideActionResult = | { status: "saved"; version?: number; location?: string } | { status: "conflict"; currentVersion: number; message: string } | { status: "error"; message: string }; type CreateGuideActionResult = Exclude; async function runGuideAction(action: () => Promise<{ version?: number }>): Promise { try { const result = await action(); return { status: "saved", version: result.version }; } catch (cause) { if (cause instanceof GuideVersionConflictError) { return { status: "conflict", currentVersion: cause.currentVersion, message: cause.message }; } if (cause instanceof z.ZodError) { return { status: "error", message: cause.issues.map((issue) => issue.message).join(" • ") }; } return { status: "error", message: cause instanceof Error ? cause.message : "บันทึกไม่สำเร็จ" }; } } export async function createStructuredGuide(input: unknown): Promise { await requireAdmin(); try { const guide = await createGuide(input); return { status: "saved", version: guide.version, location: `/admin/${guide.characterKey}` }; } catch (cause) { return { status: "error", message: cause instanceof Error ? cause.message : "สร้าง Guide ไม่สำเร็จ" }; } } export async function saveWeaponSection(guideId: string, input: unknown) { await requireAdmin(); return runGuideAction(() => saveWeapons(guideId, input)); } export async function saveGuideOverview(guideId: string, input: unknown) { await requireAdmin(); return runGuideAction(() => updateGuideBasics(guideId, input)); } export async function saveArtifactSection(guideId: string, input: unknown) { await requireAdmin(); return runGuideAction(() => saveArtifacts(guideId, input)); } export async function saveConstellationSection(guideId: string, input: unknown) { await requireAdmin(); return runGuideAction(() => saveConstellations(guideId, input)); } export async function saveTeamSection(guideId: string, input: unknown) { await requireAdmin(); return runGuideAction(() => saveTeams(guideId, input)); } export async function saveExtraSectionList(guideId: string, input: unknown) { await requireAdmin(); return runGuideAction(() => saveExtraSections(guideId, input)); } export async function moveGuideToTrash(guideId: string) { await requireAdmin(); return runGuideAction(() => trashGuide(guideId)); } export async function restoreStructuredGuide(guideId: string) { await requireAdmin(); return runGuideAction(() => restoreGuide(guideId)); } export async function setStructuredGuideVisibility( guideId: string, isPublic: boolean, ) { await requireAdmin(); return runGuideAction(() => setGuideVisibility(guideId, isPublic)); } export async function deleteStructuredGuide(guideId: string) { await requireAdmin(); return runGuideAction(() => permanentlyDeleteGuide(guideId)); } export async function startCatalogSync() { await requireAdmin(); try { return await runCatalogSync(); } catch (cause) { return { status: "error" as const, message: cause instanceof Error ? cause.message : "เริ่ม Sync ไม่สำเร็จ" }; } }