Files
buzz-sheet/lib/editor/draft.ts
T

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,
};
}