180 lines
5.9 KiB
TypeScript
180 lines
5.9 KiB
TypeScript
import type { SerializedBlock } from "@/lib/blocks";
|
|
import type { DataSourceSnapshot, PageSnapshot } from "@/lib/content/types";
|
|
|
|
export interface EditorDraft {
|
|
pageId: string;
|
|
title: string;
|
|
slug: string;
|
|
visible: boolean;
|
|
publicNote: string | null;
|
|
blocks: SerializedBlock[];
|
|
}
|
|
|
|
export function draftFromSnapshot(snapshot: PageSnapshot): EditorDraft {
|
|
return {
|
|
pageId: snapshot.page.id,
|
|
title: snapshot.page.title,
|
|
slug: snapshot.page.slug,
|
|
visible: snapshot.page.visible,
|
|
publicNote: snapshot.page.publicNote,
|
|
blocks: structuredClone(snapshot.blocks),
|
|
};
|
|
}
|
|
|
|
export function normalizeBlockOrder(
|
|
blocks: readonly SerializedBlock[],
|
|
): SerializedBlock[] {
|
|
return blocks.map((block, sortOrder) => ({ ...block, sortOrder }));
|
|
}
|
|
|
|
export function moveBlock(
|
|
blocks: readonly SerializedBlock[],
|
|
blockId: string,
|
|
direction: -1 | 1,
|
|
): SerializedBlock[] {
|
|
const current = blocks.findIndex((block) => block.id === blockId);
|
|
const target = current + direction;
|
|
if (current < 0 || target < 0 || target >= blocks.length) return [...blocks];
|
|
const next = [...blocks];
|
|
[next[current], next[target]] = [next[target], next[current]];
|
|
return normalizeBlockOrder(next);
|
|
}
|
|
|
|
export function moveBlockTo(
|
|
blocks: readonly SerializedBlock[],
|
|
blockId: string,
|
|
targetId: string,
|
|
): SerializedBlock[] {
|
|
const from = blocks.findIndex((block) => block.id === blockId);
|
|
const target = blocks.findIndex((block) => block.id === targetId);
|
|
if (from < 0 || target < 0 || from === target) return [...blocks];
|
|
const next = [...blocks];
|
|
const [moved] = next.splice(from, 1);
|
|
next.splice(target, 0, moved);
|
|
return normalizeBlockOrder(next);
|
|
}
|
|
|
|
export function duplicateBlock(
|
|
blocks: readonly SerializedBlock[],
|
|
blockId: string,
|
|
id = crypto.randomUUID(),
|
|
): SerializedBlock[] {
|
|
const index = blocks.findIndex((block) => block.id === blockId);
|
|
if (index < 0) return [...blocks];
|
|
const clone = structuredClone(blocks[index]);
|
|
clone.id = id;
|
|
const next = [...blocks];
|
|
next.splice(index + 1, 0, clone);
|
|
return normalizeBlockOrder(next);
|
|
}
|
|
|
|
function sourceFields(source: DataSourceSnapshot | undefined) {
|
|
const text = source?.fields.find((field) => field.type !== "number")?.key ?? "name";
|
|
const numbers = source?.fields.filter((field) => field.type === "number" || field.formula) ?? [];
|
|
return { text, numbers };
|
|
}
|
|
|
|
export function createEditorBlock(
|
|
type: string,
|
|
source?: DataSourceSnapshot,
|
|
id = crypto.randomUUID(),
|
|
): SerializedBlock | null {
|
|
const nestedId = () => crypto.randomUUID();
|
|
const { text, numbers } = sourceFields(source);
|
|
const numeric = numbers[0]?.key ?? "value";
|
|
const configs: Record<string, Record<string, unknown> | null> = {
|
|
profile: { title: "หัวข้อโปรไฟล์", summary: "เพิ่มคำแนะนำสั้น ๆ", badges: [] },
|
|
"rich-text": { content: [{ type: "paragraph", text: "เริ่มเขียนเนื้อหาที่นี่" }] },
|
|
heading: { text: "หัวข้อใหม่", level: 2 },
|
|
callout: { title: "ข้อควรรู้", body: "เพิ่มรายละเอียด", tone: "info" },
|
|
"stat-cards": {
|
|
title: "ค่าสำคัญ",
|
|
items: [{ id: nestedId(), label: "ค่า", value: "-" }],
|
|
},
|
|
"key-value": {
|
|
items: [{ id: nestedId(), label: "หัวข้อ", value: "รายละเอียด" }],
|
|
columns: 2,
|
|
},
|
|
ranking: source
|
|
? {
|
|
title: "อันดับ",
|
|
dataSourceId: source.id,
|
|
labelField: text,
|
|
valueField: numeric,
|
|
higherIsBetter: true,
|
|
}
|
|
: null,
|
|
"team-composition": source
|
|
? {
|
|
title: "ทีมแนะนำ",
|
|
dataSourceId: source.id,
|
|
teamField: text,
|
|
memberFields: [text],
|
|
}
|
|
: null,
|
|
rotation: {
|
|
title: "ลำดับการเล่น",
|
|
steps: [{ id: nestedId(), action: "เพิ่มขั้นตอน" }],
|
|
},
|
|
table: source
|
|
? {
|
|
dataSourceId: source.id,
|
|
columns: source.fields.slice(0, 4).map((field) => ({
|
|
field: field.key,
|
|
label: field.label,
|
|
align: field.type === "number" ? "end" : "start",
|
|
})),
|
|
}
|
|
: null,
|
|
"comparison-chart": source && numbers.length > 0
|
|
? {
|
|
title: "กราฟเปรียบเทียบ",
|
|
dataSourceId: source.id,
|
|
categoryField: text,
|
|
series: [{ field: numeric, label: numbers[0].label, color: "chart-1" }],
|
|
presentation: "horizontal-bar",
|
|
normalization: "raw",
|
|
sort: "authored",
|
|
showValues: true,
|
|
showLegend: false,
|
|
tableFallback: true,
|
|
}
|
|
: null,
|
|
image: null,
|
|
gallery: null,
|
|
tabs: {
|
|
tabs: [
|
|
{ id: nestedId(), label: "ตัวเลือก 1", content: "เพิ่มรายละเอียด" },
|
|
{ id: nestedId(), label: "ตัวเลือก 2", content: "เพิ่มรายละเอียด" },
|
|
],
|
|
},
|
|
accordion: {
|
|
items: [{ id: nestedId(), title: "หัวข้อ", content: "เพิ่มรายละเอียด" }],
|
|
},
|
|
section: { title: "ส่วนใหม่" },
|
|
columns: { columns: 2, gap: "normal" },
|
|
links: {
|
|
links: [{ id: nestedId(), label: "ลิงก์", href: "/", description: "เพิ่มคำอธิบาย" }],
|
|
},
|
|
separator: {},
|
|
};
|
|
const config = configs[type];
|
|
if (!config) return null;
|
|
return {
|
|
id,
|
|
type,
|
|
schemaVersion: 1,
|
|
config,
|
|
responsiveLayout: {
|
|
width: ["ranking", "table", "comparison-chart", "team-composition", "gallery"].includes(type)
|
|
? "wide"
|
|
: "content",
|
|
mobileSpan: 4,
|
|
tabletSpan: 8,
|
|
desktopSpan: 12,
|
|
hideOnMobile: false,
|
|
},
|
|
sortOrder: 0,
|
|
};
|
|
}
|