From 249af29e9be0845cb0094e2f4cab7461ae4473ca Mon Sep 17 00:00:00 2001 From: gunshiz Date: Thu, 10 Sep 2026 19:45:06 +0700 Subject: [PATCH] feat : remove legacy feature --- README.md | 42 +- app/admin/actions.ts | 185 -- app/media/[id]/route.ts | 3 +- bun.lock | 3 - components/admin/admin-command.tsx | 137 -- components/admin/admin-sidebar.tsx | 148 -- components/admin/block-canvas.tsx | 273 --- components/admin/editor-inspector.tsx | 457 ---- components/admin/editor-workspace.tsx | 439 ---- components/admin/template-gallery.tsx | 104 - components/public/block-renderer.tsx | 905 ------- components/public/character-directory.tsx | 210 -- components/public/comparison-chart.tsx | 198 -- components/public/guide-shell.tsx | 116 - components/public/media-image.tsx | 55 - db/schema.ts | 304 --- drizzle/0041_bizarre_guardian.sql | 12 + drizzle/0042_burly_the_fallen.sql | 1 + drizzle/meta/0041_snapshot.json | 2701 +++++++++++++++++++++ drizzle/meta/0042_snapshot.json | 2694 ++++++++++++++++++++ drizzle/meta/_journal.json | 16 +- lib/blocks/index.ts | 24 - lib/blocks/registry.test.ts | 128 - lib/blocks/registry.ts | 165 -- lib/blocks/schemas.ts | 283 --- lib/cache/public.ts | 36 - lib/cache/tags.test.ts | 8 - lib/cache/tags.ts | 23 - lib/charts/comparison.test.ts | 131 - lib/charts/comparison.ts | 250 -- lib/content/data-source.ts | 125 - lib/content/demo.ts | 362 --- lib/content/lifecycle.test.ts | 379 --- lib/content/lifecycle.ts | 772 ------ lib/content/loaders.ts | 37 - lib/content/mutations.ts | 762 ------ lib/content/queries.ts | 364 --- lib/content/types.ts | 78 - lib/editor/draft.test.ts | 40 - lib/editor/draft.ts | 179 -- lib/events/invalidation.ts | 3 - lib/formula/engine.test.ts | 132 - lib/formula/engine.ts | 574 ----- lib/formula/index.ts | 11 - lib/formula/types.ts | 62 - lib/media/repository.ts | 64 +- lib/templates/apply.ts | 131 - lib/templates/built-in.ts | 310 --- lib/templates/index.ts | 10 - lib/templates/persist.ts | 138 -- lib/templates/templates.test.ts | 60 - lib/templates/types.ts | 64 - package.json | 2 - scripts/seed-templates.ts | 31 - tests/fixtures/formula-corpus.ts | 78 - tests/formula-corpus.test.ts | 65 - 56 files changed, 5491 insertions(+), 9393 deletions(-) delete mode 100644 components/admin/admin-command.tsx delete mode 100644 components/admin/admin-sidebar.tsx delete mode 100644 components/admin/block-canvas.tsx delete mode 100644 components/admin/editor-inspector.tsx delete mode 100644 components/admin/editor-workspace.tsx delete mode 100644 components/admin/template-gallery.tsx delete mode 100644 components/public/block-renderer.tsx delete mode 100644 components/public/character-directory.tsx delete mode 100644 components/public/comparison-chart.tsx delete mode 100644 components/public/guide-shell.tsx delete mode 100644 components/public/media-image.tsx create mode 100644 drizzle/0041_bizarre_guardian.sql create mode 100644 drizzle/0042_burly_the_fallen.sql create mode 100644 drizzle/meta/0041_snapshot.json create mode 100644 drizzle/meta/0042_snapshot.json delete mode 100644 lib/blocks/index.ts delete mode 100644 lib/blocks/registry.test.ts delete mode 100644 lib/blocks/registry.ts delete mode 100644 lib/blocks/schemas.ts delete mode 100644 lib/charts/comparison.test.ts delete mode 100644 lib/charts/comparison.ts delete mode 100644 lib/content/data-source.ts delete mode 100644 lib/content/demo.ts delete mode 100644 lib/content/lifecycle.test.ts delete mode 100644 lib/content/lifecycle.ts delete mode 100644 lib/content/loaders.ts delete mode 100644 lib/content/mutations.ts delete mode 100644 lib/content/queries.ts delete mode 100644 lib/content/types.ts delete mode 100644 lib/editor/draft.test.ts delete mode 100644 lib/editor/draft.ts delete mode 100644 lib/formula/engine.test.ts delete mode 100644 lib/formula/engine.ts delete mode 100644 lib/formula/index.ts delete mode 100644 lib/formula/types.ts delete mode 100644 lib/templates/apply.ts delete mode 100644 lib/templates/built-in.ts delete mode 100644 lib/templates/index.ts delete mode 100644 lib/templates/persist.ts delete mode 100644 lib/templates/templates.test.ts delete mode 100644 lib/templates/types.ts delete mode 100644 scripts/seed-templates.ts delete mode 100644 tests/fixtures/formula-corpus.ts delete mode 100644 tests/formula-corpus.test.ts diff --git a/README.md b/README.md index 4a3b50c..9cfd6d9 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,18 @@ # Buzz Sheet Buzz Sheet is a Thai-first guide CMS for turning structured character, build, -team, rotation, and comparison data into responsive game-guide pages. Authors -work in a visual block editor; visitors get readable guides rather than a -spreadsheet interface. +team, rotation, and comparison data into responsive game-guide pages. Production target: `https://guide.sudloh.com` ## Included capabilities -- Searchable character directory with element and role filters. -- Unlimited character pages with custom slugs, ordering, notes, and visibility. -- Visual admin workspace with inline editing, inspector, preview, keyboard and - pointer reordering, 750 ms autosave, conflict recovery, and revisions. -- Six reusable templates covering classic guides, rotations, one-page builds, - multi-role builds, team databases, and calculation-backed comparisons. -- Validated block schemas with migrations and a safe fallback for unknown - future block types. -- Restricted decimal formula engine with named references, dependency ordering, - cycle detection, typed failures, and presentation-only rounding. -- Read-only comparison charts with visible values and accessible table fallbacks. +- Searchable character guide directory. +- Structured editors for overview, weapons, artifacts, constellations, teams, + and custom sections with autosave and conflict recovery. - Better Auth email/password login with administrator-managed accounts. - Private S3-compatible media uploads with reference-aware same-origin delivery. -- PostgreSQL transactions, immutable data-source versions, revisions, and a - retryable outbox. +- PostgreSQL transactions and a retryable outbox. - Shared Redis cache invalidation and privacy-safe Server-Sent Events across replicas. - Discord-triggered Lunaris catalog updates with exact-version synchronization. @@ -75,13 +64,9 @@ Prepare the database and start the application: ```bash bun run db:migrate -bun run db:seed bun run dev ``` -The seed command installs the six starter built-in templates; it does not publish -reference workbook content. - ### Fixture-only demo The UI can be inspected without PostgreSQL, Redis, S3, or authentication @@ -106,16 +91,14 @@ and external services. Never enable it in a deployed environment. | `bun run build` | Compile the production application | | `bun run db:generate` | Generate a Drizzle migration from schema changes | | `bun run db:migrate` | Apply committed Drizzle migrations | -| `bun run db:seed` | Upsert the built-in templates | | `bun run import:guides` | Preview the one-time Google Sheets guide migration (`-- --apply` to write) | | `bun run worker:discord` | Listen for Lunaris update announcements | | `bun run worker:outbox` | Process cache and event outbox records | ## Runtime design -Accepted edits use an `expectedVersion` and commit content, immutable data-source -versions, revision snapshots, media references, and outbox records in one -PostgreSQL transaction. The outbox worker then invalidates affected Redis tags +Accepted edits use an `expectedVersion` and commit guide content, media reference +counts, and outbox records in one PostgreSQL transaction. The outbox worker then invalidates affected Redis tags before publishing an opaque `{ type, id, version }` event. Connected clients refetch authoritative state; SSE never contains page content and is not the source of truth. @@ -128,18 +111,13 @@ Worker and sync results are sent to `DISCORD_LOG_CHANNEL_ID`. Enable the Discord Message Content intent and grant the bot View Channel and Send Messages in the configured channels. -Public snapshots use a shared Redis-backed Next.js cache. Admin routes remain +Public guides use a shared Redis-backed Next.js cache. Admin routes remain dynamic. SSE streams send a 90-second heartbeat and close after 30 minutes so clients reconnect. Traefik buffering is disabled by the response headers. Media objects remain private in S3. `/media/[id]` serves an object publicly only -while a visible current snapshot references it; otherwise administrator -authorization is required. Retained revisions keep their referenced objects -alive. - -Formula fields accept arithmetic only: `+`, `-`, `*`, `/`, parentheses, -percentages, unary signs, and stable field references. They cannot execute HTML, -JavaScript, React, CSS, external scripts, packages, or network calls. +while a published guide references it; otherwise administrator authorization is +required. ## Verification diff --git a/app/admin/actions.ts b/app/admin/actions.ts index 88eed86..5e25a8d 100644 --- a/app/admin/actions.ts +++ b/app/admin/actions.ts @@ -3,23 +3,12 @@ import { updateTag } from "next/cache"; import * as z from "zod"; -import type { AutosaveResponse } from "@/lib/editor/autosave"; import { GLOSSARY_CACHE_TAG, pageCacheTag } from "@/lib/cache/tags"; import { createGlossaryAlias, deleteGlossaryAlias, } from "@/lib/glossary-repository"; -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 { createExtraSection, createGuide, @@ -48,180 +37,6 @@ import { listAdminTeamImportGuides, } from "@/lib/guides/queries"; -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 } diff --git a/app/media/[id]/route.ts b/app/media/[id]/route.ts index 32b1d6e..ea87daf 100644 --- a/app/media/[id]/route.ts +++ b/app/media/[id]/route.ts @@ -1,8 +1,7 @@ import * as z from "zod"; import { getAdminSession } from "@/lib/auth/server"; -import { getMediaVisibility } from "@/lib/content/queries"; -import { getReadyMedia } from "@/lib/media/repository"; +import { getMediaVisibility, getReadyMedia } from "@/lib/media/repository"; import { getMediaStorage } from "@/lib/media/storage"; import { parseByteRange } from "@/lib/media/validation"; diff --git a/bun.lock b/bun.lock index a04c6fa..4271f31 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,6 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "decimal.js": "^10.6.0", "discord.js": "^14.27.0", "drizzle-orm": "^0.45.2", "embla-carousel-react": "^8.6.0", @@ -871,8 +870,6 @@ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="], - "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], "dedent": ["dedent@1.7.2", "", { "peerDependencies": { "babel-plugin-macros": "^3.1.0" }, "optionalPeers": ["babel-plugin-macros"] }, "sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA=="], diff --git a/components/admin/admin-command.tsx b/components/admin/admin-command.tsx deleted file mode 100644 index d62f216..0000000 --- a/components/admin/admin-command.tsx +++ /dev/null @@ -1,137 +0,0 @@ -"use client"; - -import { useRouter } from "next/navigation"; -import { - BarChart3Icon, - FileTextIcon, - Heading2Icon, - ImageIcon, - LayoutGridIcon, - ListOrderedIcon, - MessageSquareIcon, - Rows3Icon, - SeparatorHorizontalIcon, - Table2Icon, - TextIcon, - UsersIcon, - type LucideIcon, -} from "lucide-react"; - -import type { AdminNavigation } from "@/components/admin/admin-sidebar"; -import { - Command, - CommandDialog, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - CommandSeparator, - CommandShortcut, -} from "@/components/ui/command"; -import type { SerializedBlock } from "@/lib/blocks"; -import type { DataSourceSnapshot } from "@/lib/content/types"; -import { createEditorBlock } from "@/lib/editor/draft"; - -const catalog: Array<{ - type: string; - label: string; - icon: LucideIcon; - source?: boolean; - media?: boolean; -}> = [ - { type: "profile", label: "โปรไฟล์", icon: LayoutGridIcon }, - { type: "rich-text", label: "ข้อความ", icon: TextIcon }, - { type: "heading", label: "หัวข้อ", icon: Heading2Icon }, - { type: "callout", label: "กล่องหมายเหตุ", icon: MessageSquareIcon }, - { type: "stat-cards", label: "การ์ดค่าสถานะ", icon: LayoutGridIcon }, - { type: "key-value", label: "ข้อมูลกำหนดเอง", icon: Rows3Icon }, - { type: "ranking", label: "อันดับเปรียบเทียบ", icon: ListOrderedIcon, source: true }, - { type: "team-composition", label: "องค์ประกอบทีม", icon: UsersIcon, source: true }, - { type: "rotation", label: "ลำดับคอมโบ", icon: ListOrderedIcon }, - { type: "table", label: "ตารางข้อมูล", icon: Table2Icon, source: true }, - { type: "comparison-chart", label: "กราฟเปรียบเทียบ", icon: BarChart3Icon, source: true }, - { type: "tabs", label: "แท็บ", icon: LayoutGridIcon }, - { type: "accordion", label: "แอคคอร์เดียน", icon: Rows3Icon }, - { type: "section", label: "ส่วนเนื้อหา", icon: Heading2Icon }, - { type: "columns", label: "คอลัมน์", icon: LayoutGridIcon }, - { type: "links", label: "ลิงก์", icon: FileTextIcon }, - { type: "separator", label: "เส้นคั่น", icon: SeparatorHorizontalIcon }, - { type: "image", label: "รูปภาพ", icon: ImageIcon, media: true }, - { type: "gallery", label: "แกลเลอรี", icon: ImageIcon, media: true }, -]; - -export function AdminCommandDialog({ - open, - onOpenChange, - navigation, - dataSources, - onInsert, -}: { - open: boolean; - onOpenChange: (open: boolean) => void; - navigation: AdminNavigation; - dataSources: Record; - onInsert: (block: SerializedBlock) => void; -}) { - const router = useRouter(); - const source = Object.values(dataSources)[0]; - - return ( - - - - - ไม่พบคำสั่งที่ตรงกัน - - {navigation.characters.flatMap((character) => - character.pages.map((page) => ( - { - onOpenChange(false); - router.push(`/admin/${character.slug}/${page.slug}`); - }} - > - - )), - )} - - - - {catalog.map((item) => { - const disabled = (item.source && !source) || item.media; - return ( - { - const block = createEditorBlock(item.type, source); - if (!block) return; - onInsert(block); - onOpenChange(false); - }} - > - - ); - })} - - - - - ); -} diff --git a/components/admin/admin-sidebar.tsx b/components/admin/admin-sidebar.tsx deleted file mode 100644 index a512795..0000000 --- a/components/admin/admin-sidebar.tsx +++ /dev/null @@ -1,148 +0,0 @@ -"use client"; - -import Link from "next/link"; -import { useRouter } from "next/navigation"; -import { - BookOpenIcon, - BoxesIcon, - ChevronRightIcon, - LayoutTemplateIcon, - LogOutIcon, -} from "lucide-react"; - -import { Button } from "@/components/ui/button"; -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, - SidebarRail, -} from "@/components/ui/sidebar"; -import type { CharacterIdentity, PageIdentity } from "@/lib/content/types"; -import { authClient } from "@/lib/auth/client"; - -export interface AdminNavigation { - characters: Array; - templates: Array<{ - id: string; - slug: string; - name: string; - description: string | null; - category: string; - builtIn: boolean; - }>; -} - -export function AdminSidebar({ - navigation, - currentCharacter, - currentPage, -}: { - navigation: AdminNavigation; - currentCharacter?: string; - currentPage?: string; -}) { - const router = useRouter(); - - return ( - - - - - } - > - - - - Buzz Sheet - ตัวแก้ไขคู่มือ - - - - - - - - ตัวละครและหน้า - - - {navigation.characters.map((character) => ( - - - - - {character.pages.map((page) => ( - - } - isActive={character.slug === currentCharacter && page.slug === currentPage} - > - {page.title} - - - ))} - - - ))} - - - - - โครงสร้าง - - - - } - > - - - - - - - - - - - - ); -} diff --git a/components/admin/block-canvas.tsx b/components/admin/block-canvas.tsx deleted file mode 100644 index efdf4f7..0000000 --- a/components/admin/block-canvas.tsx +++ /dev/null @@ -1,273 +0,0 @@ -"use client"; - -import { - ArrowDownIcon, - ArrowUpIcon, - CopyIcon, - GripVerticalIcon, - MoreHorizontalIcon, - PlusIcon, - Trash2Icon, -} from "lucide-react"; -import { useState } from "react"; - -import { DeleteConfirmationDialog } from "@/components/admin/delete-confirmation-dialog"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; -import type { SerializedBlock } from "@/lib/blocks"; -import { - duplicateBlock, - moveBlock, - moveBlockTo, - type EditorDraft, -} from "@/lib/editor/draft"; -import { cn } from "@/lib/utils"; - -export const blockLabels: Record = { - profile: "โปรไฟล์", - "rich-text": "ข้อความ", - heading: "หัวข้อ", - callout: "กล่องหมายเหตุ", - "stat-cards": "การ์ดค่าสถานะ", - "key-value": "ข้อมูลกำหนดเอง", - ranking: "อันดับเปรียบเทียบ", - "team-composition": "องค์ประกอบทีม", - rotation: "ลำดับคอมโบ", - table: "ตารางข้อมูล", - "comparison-chart": "กราฟเปรียบเทียบ", - image: "รูปภาพ", - gallery: "แกลเลอรี", - tabs: "แท็บ", - accordion: "แอคคอร์เดียน", - section: "ส่วนเนื้อหา", - columns: "คอลัมน์", - links: "ลิงก์", - separator: "เส้นคั่น", -}; - -interface BlockCanvasProps { - draft: EditorDraft; - selectedId: string | null; - onSelect: (id: string) => void; - onChange: (draft: EditorDraft) => void; - onOpenInsert: () => void; -} - -export function BlockCanvas({ - draft, - selectedId, - onSelect, - onChange, - onOpenInsert, -}: BlockCanvasProps) { - const changeBlocks = (blocks: SerializedBlock[]) => onChange({ ...draft, blocks }); - const patchBlock = (blockId: string, config: Record) => { - changeBlocks( - draft.blocks.map((block) => - block.id === blockId ? { ...block, config: { ...block.config as object, ...config } } : block, - ), - ); - }; - - return ( -
- - - - - ชื่อหน้า - onChange({ ...draft, title: event.target.value })} - /> - - - หมายเหตุสาธารณะ -