285 lines
8.9 KiB
TypeScript
285 lines
8.9 KiB
TypeScript
"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,
|
|
enqueueCatalogSync,
|
|
permanentlyDeleteGuide,
|
|
restoreGuide,
|
|
saveArtifacts,
|
|
saveConstellations,
|
|
saveExtraSections,
|
|
saveTeams,
|
|
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<typeof editorDraftSchema>;
|
|
export type EditorSaveResult = AutosaveResponse<PageSnapshot>;
|
|
|
|
export async function saveEditorPage(input: unknown): Promise<EditorSaveResult> {
|
|
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<AutosaveResponse<DataSourceSnapshot>> {
|
|
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: "error"; message: string };
|
|
|
|
async function runGuideAction(action: () => Promise<{ version?: number }>): Promise<GuideActionResult> {
|
|
try {
|
|
const result = await action();
|
|
return { status: "saved", version: result.version };
|
|
} catch (cause) {
|
|
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<GuideActionResult> {
|
|
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 deleteStructuredGuide(guideId: string) {
|
|
await requireAdmin();
|
|
return runGuideAction(() => permanentlyDeleteGuide(guideId));
|
|
}
|
|
|
|
export async function startCatalogSync() {
|
|
const admin = await requireAdmin();
|
|
try {
|
|
const job = await enqueueCatalogSync(admin.user.id);
|
|
return { status: "saved" as const, jobId: job.id };
|
|
} catch (cause) {
|
|
return { status: "error" as const, message: cause instanceof Error ? cause.message : "เริ่ม Sync ไม่สำเร็จ" };
|
|
}
|
|
}
|