From 139a9081ed51c9351ed8df2a62c71bb1d2b9c3a5 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Fri, 28 Aug 2026 19:21:57 +0000 Subject: [PATCH] feat(content): add explicit demo mode --- lib/content/demo.ts | 362 +++++++++++++++++++++++++++++++++++++++++ lib/content/loaders.ts | 38 +++++ 2 files changed, 400 insertions(+) create mode 100644 lib/content/demo.ts create mode 100644 lib/content/loaders.ts diff --git a/lib/content/demo.ts b/lib/content/demo.ts new file mode 100644 index 0000000..bf94144 --- /dev/null +++ b/lib/content/demo.ts @@ -0,0 +1,362 @@ +import type { + PageSnapshot, + PublicCharacterCard, +} from "@/lib/content/types"; + +const ids = { + character: "10000000-0000-4000-8000-000000000001", + infoPage: "10000000-0000-4000-8000-000000000002", + comboPage: "10000000-0000-4000-8000-000000000003", + source: "10000000-0000-4000-8000-000000000004", + profile: "10000000-0000-4000-8000-000000000005", + section: "10000000-0000-4000-8000-000000000006", + stats: "10000000-0000-4000-8000-000000000007", + ranking: "10000000-0000-4000-8000-000000000008", + chart: "10000000-0000-4000-8000-000000000009", + rotation: "10000000-0000-4000-8000-000000000010", + callout: "10000000-0000-4000-8000-000000000011", + richText: "10000000-0000-4000-8000-000000000012", + table: "10000000-0000-4000-8000-000000000013", + links: "10000000-0000-4000-8000-000000000014", + statOne: "10000000-0000-4000-8000-000000000015", + statTwo: "10000000-0000-4000-8000-000000000016", + statThree: "10000000-0000-4000-8000-000000000017", + stepOne: "10000000-0000-4000-8000-000000000018", + stepTwo: "10000000-0000-4000-8000-000000000019", + stepThree: "10000000-0000-4000-8000-000000000020", + rowOne: "10000000-0000-4000-8000-000000000021", + rowTwo: "10000000-0000-4000-8000-000000000022", + rowThree: "10000000-0000-4000-8000-000000000023", + linkOne: "10000000-0000-4000-8000-000000000024", +} as const; + +const character = { + id: ids.character, + slug: "astra", + nameTh: "แอสตรา", + nameEn: "Astra", + description: "นักวางกลยุทธ์ธาตุแสงสำหรับทีมที่เน้นจังหวะสกิล", + portraitMediaId: null, + element: "แสง", + role: "สนับสนุน", + rarity: 5, + sortOrder: 0, + visible: true, +}; + +const pages = [ + { + id: ids.infoPage, + characterId: ids.character, + slug: "info", + title: "ภาพรวม", + navOrder: 0, + visible: true, + publicNote: "ตัวเลขตัวอย่างนี้ใช้เพื่อสาธิตโครงสร้างเท่านั้น", + version: 3, + }, + { + id: ids.comboPage, + characterId: ids.character, + slug: "combo", + title: "คอมโบ", + navOrder: 1, + visible: true, + publicNote: null, + version: 1, + }, +]; + +const dataSource = { + id: ids.source, + name: "อุปกรณ์ตัวอย่าง", + version: 2, + fields: [ + { key: "name", label: "ชื่อ", type: "text" as const }, + { key: "score", label: "คะแนน", type: "number" as const, displayPrecision: 1 }, + { key: "base", label: "ค่าฐาน", type: "number" as const }, + { + key: "boosted", + label: "เมื่อเสริมพลัง", + type: "number" as const, + formula: "base * 1.25", + displayPrecision: 2, + }, + { key: "detail", label: "รายละเอียด", type: "text" as const }, + { key: "tag", label: "ระดับ", type: "badge" as const }, + ], + rows: [ + { + id: ids.rowOne, + values: { + name: "ดาบดาวเหนือ", + score: "98.5", + base: "120", + boosted: null, + detail: "เหมาะกับรอบการเล่นสั้นและการฟื้นพลังงาน", + tag: "แนะนำ", + }, + }, + { + id: ids.rowTwo, + values: { + name: "คทาสายหมอก", + score: "91.25", + base: "108", + boosted: null, + detail: "ตัวเลือกสมดุลที่ใช้ได้กับหลายทีม", + tag: "ดีมาก", + }, + }, + { + id: ids.rowThree, + values: { + name: "หอกฝึกหัด", + score: "76", + base: "88", + boosted: null, + detail: "ตัวเลือกเข้าถึงง่ายสำหรับช่วงเริ่มต้น", + tag: "เริ่มต้น", + }, + }, + ], +}; + +function snapshot(pageSlug: string): PageSnapshot | null { + const page = pages.find((candidate) => candidate.slug === pageSlug); + if (!page) return null; + + const overviewBlocks: PageSnapshot["blocks"] = [ + { + id: ids.profile, + type: "profile", + schemaVersion: 1, + sortOrder: 0, + config: { + eyebrow: "คู่มือตัวละคร", + title: "แอสตรา — ผู้กำหนดจังหวะสนามรบ", + subtitle: "ตัวสนับสนุนที่เพิ่มพลังให้ทีมผ่านการจัดลำดับสกิล", + summary: + "ตัวอย่างนี้แสดงว่าคู่มือสามารถอ่านง่ายบนมือถือและยังรองรับข้อมูลคำนวณที่ตรวจสอบได้", + mediaId: null, + badges: [ + { label: "ธาตุ", value: "แสง", tone: "element" }, + { label: "ตำแหน่ง", value: "สนับสนุน", tone: "neutral" }, + { label: "ระดับ", value: "5 ดาว", tone: "rarity" }, + ], + }, + }, + { + id: ids.richText, + type: "rich-text", + schemaVersion: 1, + sortOrder: 1, + config: { + content: [ + { + type: "paragraph", + text: "วางสกิลสนับสนุนก่อนตัวทำความเสียหายหลัก และเผื่อช่วงเวลาให้เอฟเฟกต์ครอบคลุมท่าไม้ตาย", + }, + { + type: "bulleted-list", + items: [ + "ให้ความสำคัญกับความเร็วและการฟื้นพลังงาน", + "ปรับลำดับสกิลตามระยะเวลาบัฟของทีม", + ], + }, + ], + }, + }, + { + id: ids.section, + type: "section", + schemaVersion: 1, + sortOrder: 2, + config: { + title: "สรุปค่าสำคัญ", + description: "ค่าที่ควรตรวจสอบก่อนปรับอุปกรณ์", + anchor: "key-stats", + }, + }, + { + id: ids.stats, + type: "stat-cards", + schemaVersion: 1, + sortOrder: 3, + config: { + title: "เป้าหมายเบื้องต้น", + items: [ + { id: ids.statOne, label: "ความเร็ว", value: "134", suffix: "+" }, + { id: ids.statTwo, label: "ฟื้นพลังงาน", value: "120", suffix: "%" }, + { id: ids.statThree, label: "รอบบัฟ", value: "3", suffix: " เทิร์น" }, + ], + }, + }, + { + id: ids.ranking, + type: "ranking", + schemaVersion: 1, + sortOrder: 4, + responsiveLayout: { width: "wide" }, + config: { + title: "อันดับอุปกรณ์", + description: "แตะหรือชี้ที่ชื่อเพื่อดูเหตุผลประกอบ", + dataSourceId: ids.source, + labelField: "name", + valueField: "score", + detailField: "detail", + badgeField: "tag", + higherIsBetter: true, + }, + }, + { + id: ids.chart, + type: "comparison-chart", + schemaVersion: 1, + sortOrder: 5, + responsiveLayout: { width: "wide" }, + config: { + title: "ผลก่อนและหลังเสริมพลัง", + description: "ค่าทั้งหมดมาจากแหล่งข้อมูลเวอร์ชันเดียวกัน", + dataSourceId: ids.source, + categoryField: "name", + series: [ + { field: "base", label: "ค่าฐาน", color: "chart-1" }, + { field: "boosted", label: "เมื่อเสริมพลัง", color: "chart-2" }, + ], + presentation: "grouped-bar", + normalization: "raw", + sort: "authored", + showValues: true, + showLegend: true, + tableFallback: true, + }, + }, + { + id: ids.callout, + type: "callout", + schemaVersion: 1, + sortOrder: 6, + config: { + title: "หมายเหตุ", + body: "ผลลัพธ์จริงเปลี่ยนตามเพื่อนร่วมทีมและลำดับการออกแอ็กชัน", + tone: "tip", + }, + }, + { + id: ids.table, + type: "table", + schemaVersion: 1, + sortOrder: 7, + responsiveLayout: { width: "wide" }, + config: { + title: "ข้อมูลคำนวณแบบละเอียด", + dataSourceId: ids.source, + columns: [ + { field: "name", label: "อุปกรณ์", align: "start" }, + { field: "base", label: "ค่าฐาน", align: "end" }, + { field: "boosted", label: "เสริมพลัง", align: "end" }, + ], + }, + }, + { + id: ids.links, + type: "links", + schemaVersion: 1, + sortOrder: 8, + config: { + title: "อ่านต่อ", + links: [ + { + id: ids.linkOne, + label: "ไปยังหน้าคอมโบ", + href: "/astra/combo", + description: "ดูลำดับสกิลและจังหวะการเล่น", + }, + ], + }, + }, + ]; + + const comboBlocks: PageSnapshot["blocks"] = [ + { + id: ids.profile, + type: "profile", + schemaVersion: 1, + sortOrder: 0, + config: { + eyebrow: "ลำดับการเล่น", + title: "คอมโบพื้นฐาน", + subtitle: "หนึ่งรอบที่จำง่ายและปรับใช้ได้กับหลายทีม", + mediaId: null, + badges: [], + }, + }, + { + id: ids.rotation, + type: "rotation", + schemaVersion: 1, + sortOrder: 1, + config: { + title: "รอบการใช้สกิล", + description: "เรียงจากการเตรียมสนามไปจนถึงช่วงทำความเสียหาย", + steps: [ + { id: ids.stepOne, actor: "ตัวสนับสนุน", action: "เปิดบัฟทีม", duration: "1 เทิร์น" }, + { id: ids.stepTwo, actor: "แอสตรา", action: "ใช้สกิลหลัก", detail: "เก็บพลังงานไว้สำหรับท่าไม้ตาย" }, + { id: ids.stepThree, actor: "ตัวทำความเสียหาย", action: "ใช้ท่าไม้ตายและโจมตีต่อเนื่อง" }, + ], + }, + }, + ]; + + return { + character, + page, + pages, + blocks: pageSlug === "info" ? overviewBlocks : comboBlocks, + dataSources: { [ids.source]: dataSource }, + versionVector: { [ids.source]: dataSource.version }, + }; +} + +export const demoDirectory: PublicCharacterCard[] = [ + { + id: character.id, + slug: character.slug, + nameTh: character.nameTh, + nameEn: character.nameEn, + portraitMediaId: character.portraitMediaId, + element: character.element, + role: character.role, + rarity: character.rarity, + firstPageSlug: pages[0].slug, + }, +]; + +export function getDemoPage( + characterSlug: string, + pageSlug?: string, +): + | { type: "page"; snapshot: PageSnapshot } + | { type: "redirect"; location: string } + | { type: "not-found" } { + if (characterSlug !== character.slug) return { type: "not-found" }; + if (!pageSlug) return { type: "redirect", location: "/astra/info" }; + const pageSnapshot = snapshot(pageSlug); + return pageSnapshot + ? { type: "page", snapshot: pageSnapshot } + : { type: "not-found" }; +} + +export function getDemoAdminPage( + characterSlug: string, + pageSlug: string, +): PageSnapshot | null { + if (characterSlug !== character.slug) return null; + return snapshot(pageSlug); +} + +export const demoAdminNavigation = { + characters: [{ ...character, pages }], + templates: [], +}; diff --git a/lib/content/loaders.ts b/lib/content/loaders.ts new file mode 100644 index 0000000..63bea3b --- /dev/null +++ b/lib/content/loaders.ts @@ -0,0 +1,38 @@ +import "server-only"; + +import { + demoAdminNavigation, + demoDirectory, + getDemoAdminPage, + getDemoPage, +} from "@/lib/content/demo"; +import { + getAdminNavigation, + getAdminPage, + getPublicPage, + listPublicCharacters, +} from "@/lib/content/queries"; + +function demoMode(): boolean { + return process.env.BUZZ_DEMO_MODE === "true"; +} + +export async function loadPublicDirectory() { + return demoMode() ? demoDirectory : listPublicCharacters(); +} + +export async function loadPublicPage(characterSlug: string, pageSlug?: string) { + return demoMode() + ? getDemoPage(characterSlug, pageSlug) + : getPublicPage(characterSlug, pageSlug); +} + +export async function loadAdminPage(characterSlug: string, pageSlug: string) { + return demoMode() + ? getDemoAdminPage(characterSlug, pageSlug) + : getAdminPage(characterSlug, pageSlug); +} + +export async function loadAdminNavigation() { + return demoMode() ? demoAdminNavigation : getAdminNavigation(); +}