feat(templates): add six reusable guide structures
This commit is contained in:
@@ -0,0 +1,131 @@
|
|||||||
|
import { resolveBlock } from "@/lib/blocks";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AppliedTemplate,
|
||||||
|
TemplateBlueprint,
|
||||||
|
TemplateDataSourceBlueprint,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export type IdFactory = () => string;
|
||||||
|
|
||||||
|
function cloneAndReplace(
|
||||||
|
value: unknown,
|
||||||
|
sourceIds: ReadonlyMap<string, string>,
|
||||||
|
nestedIds: Map<string, string>,
|
||||||
|
idFactory: IdFactory,
|
||||||
|
): unknown {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
if (value.startsWith("@source:")) {
|
||||||
|
const key = value.slice("@source:".length);
|
||||||
|
const sourceId = sourceIds.get(key);
|
||||||
|
if (!sourceId) throw new Error(`Template references missing data source: ${key}`);
|
||||||
|
return sourceId;
|
||||||
|
}
|
||||||
|
if (value.startsWith("@id:")) {
|
||||||
|
const key = value.slice("@id:".length);
|
||||||
|
const existing = nestedIds.get(key);
|
||||||
|
if (existing) return existing;
|
||||||
|
const generated = idFactory();
|
||||||
|
nestedIds.set(key, generated);
|
||||||
|
return generated;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) =>
|
||||||
|
cloneAndReplace(item, sourceIds, nestedIds, idFactory),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (value && typeof value === "object") {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(value).map(([key, item]) => [
|
||||||
|
key,
|
||||||
|
cloneAndReplace(item, sourceIds, nestedIds, idFactory),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cloneDataSource(
|
||||||
|
blueprint: TemplateDataSourceBlueprint,
|
||||||
|
id: string,
|
||||||
|
idFactory: IdFactory,
|
||||||
|
): AppliedTemplate["dataSources"][number] {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
name: blueprint.name,
|
||||||
|
description: blueprint.description ?? null,
|
||||||
|
version: 1,
|
||||||
|
fields: structuredClone(blueprint.fields),
|
||||||
|
rows: (blueprint.rows ?? []).map((row) => ({
|
||||||
|
id: row.id ?? idFactory(),
|
||||||
|
values: structuredClone(row.values),
|
||||||
|
hidden: row.hidden,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyTemplate(
|
||||||
|
template: TemplateBlueprint,
|
||||||
|
characterId: string,
|
||||||
|
idFactory: IdFactory = () => crypto.randomUUID(),
|
||||||
|
): AppliedTemplate {
|
||||||
|
const sourceIds = new Map(
|
||||||
|
template.dataSources.map((source) => [source.key, idFactory()]),
|
||||||
|
);
|
||||||
|
const nestedIds = new Map<string, string>();
|
||||||
|
const dataSources = template.dataSources.map((source) =>
|
||||||
|
cloneDataSource(source, sourceIds.get(source.key)!, idFactory),
|
||||||
|
);
|
||||||
|
|
||||||
|
const pages = template.pages.map((page, pageIndex) => {
|
||||||
|
const pageId = idFactory();
|
||||||
|
const blocks = page.blocks.map((block, blockIndex) => {
|
||||||
|
const serialized = {
|
||||||
|
id: idFactory(),
|
||||||
|
type: block.type,
|
||||||
|
schemaVersion: block.schemaVersion ?? 1,
|
||||||
|
config: cloneAndReplace(
|
||||||
|
block.config,
|
||||||
|
sourceIds,
|
||||||
|
nestedIds,
|
||||||
|
idFactory,
|
||||||
|
) as Record<string, unknown>,
|
||||||
|
responsiveLayout: cloneAndReplace(
|
||||||
|
block.responsiveLayout ?? {},
|
||||||
|
sourceIds,
|
||||||
|
nestedIds,
|
||||||
|
idFactory,
|
||||||
|
) as Record<string, unknown>,
|
||||||
|
sortOrder: blockIndex,
|
||||||
|
};
|
||||||
|
const resolution = resolveBlock(serialized);
|
||||||
|
if (resolution.kind !== "ready") {
|
||||||
|
throw new Error(
|
||||||
|
`Built-in template ${template.slug} contains an invalid ${block.type} block.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return resolution.block;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: pageId,
|
||||||
|
characterId,
|
||||||
|
slug: page.slug,
|
||||||
|
title: page.title,
|
||||||
|
publicNote: page.publicNote ?? null,
|
||||||
|
navOrder: pageIndex,
|
||||||
|
visible: false as const,
|
||||||
|
version: 1 as const,
|
||||||
|
blocks,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
templateSlug: template.slug,
|
||||||
|
characterId,
|
||||||
|
pages,
|
||||||
|
dataSources,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
import type { TemplateBlueprint } from "./types";
|
||||||
|
|
||||||
|
const comparisonFields = [
|
||||||
|
{ key: "name", label: "ชื่อ", type: "text" as const },
|
||||||
|
{ key: "base", label: "ค่าพื้นฐาน", type: "number" as const, displayPrecision: 0 },
|
||||||
|
{ key: "bonus", label: "โบนัส", type: "number" as const, displayPrecision: 0 },
|
||||||
|
{ key: "cost", label: "ต้นทุน", type: "number" as const, displayPrecision: 0 },
|
||||||
|
{
|
||||||
|
key: "score",
|
||||||
|
label: "คะแนนเปรียบเทียบ",
|
||||||
|
type: "number" as const,
|
||||||
|
formula: "(base + bonus) / cost",
|
||||||
|
displayPrecision: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const builtInTemplates: readonly TemplateBlueprint[] = [
|
||||||
|
{
|
||||||
|
slug: "classic-character-guide",
|
||||||
|
name: "คู่มือตัวละครแบบคลาสสิก",
|
||||||
|
description: "โครงสร้างหลายหน้าสำหรับข้อมูล บิลด์ ทีม และคำอธิบายเชิงลึก",
|
||||||
|
category: "character-guide",
|
||||||
|
builtIn: true,
|
||||||
|
dataSources: [
|
||||||
|
{
|
||||||
|
key: "weapons",
|
||||||
|
name: "อันดับอาวุธ",
|
||||||
|
fields: comparisonFields,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "teams",
|
||||||
|
name: "ทีมแนะนำ",
|
||||||
|
fields: [
|
||||||
|
{ key: "team", label: "ทีม", type: "text" },
|
||||||
|
{ key: "member_1", label: "สมาชิก 1", type: "text" },
|
||||||
|
{ key: "member_2", label: "สมาชิก 2", type: "text" },
|
||||||
|
{ key: "member_3", label: "สมาชิก 3", type: "text" },
|
||||||
|
{ key: "member_4", label: "สมาชิก 4", type: "text" },
|
||||||
|
{ key: "notes", label: "หมายเหตุ", type: "text" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
slug: "info",
|
||||||
|
title: "ข้อมูล",
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "profile",
|
||||||
|
config: {
|
||||||
|
eyebrow: "ภาพรวมตัวละคร",
|
||||||
|
title: "ชื่อตัวละคร",
|
||||||
|
summary: "เพิ่มบทบาท จุดเด่น และแนวทางใช้งาน",
|
||||||
|
badges: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "section",
|
||||||
|
config: { title: "จุดเด่น", anchor: "highlights" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "rich-text",
|
||||||
|
config: { content: [{ type: "paragraph", text: "เริ่มเขียนคำแนะนำที่นี่" }] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "build",
|
||||||
|
title: "บิลด์",
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "ranking",
|
||||||
|
config: {
|
||||||
|
title: "อันดับอาวุธ",
|
||||||
|
dataSourceId: "@source:weapons",
|
||||||
|
labelField: "name",
|
||||||
|
valueField: "score",
|
||||||
|
detailField: "notes",
|
||||||
|
higherIsBetter: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "teams",
|
||||||
|
title: "ทีม",
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "team-composition",
|
||||||
|
config: {
|
||||||
|
title: "ทีมแนะนำ",
|
||||||
|
dataSourceId: "@source:teams",
|
||||||
|
teamField: "team",
|
||||||
|
memberFields: ["member_1", "member_2", "member_3", "member_4"],
|
||||||
|
notesField: "notes",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "combo-rotation-guide",
|
||||||
|
name: "คู่มือคอมโบและโรเทชัน",
|
||||||
|
description: "ขั้นตอนคอมโบตามลำดับ พร้อมผู้ใช้สกิล เวลา และคำอธิบายเสริม",
|
||||||
|
category: "rotation",
|
||||||
|
builtIn: true,
|
||||||
|
dataSources: [],
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
slug: "combo",
|
||||||
|
title: "คอมโบ",
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "callout",
|
||||||
|
config: {
|
||||||
|
title: "ก่อนเริ่ม",
|
||||||
|
body: "ระบุเงื่อนไขและทรัพยากรที่ต้องเตรียม",
|
||||||
|
tone: "tip",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "rotation",
|
||||||
|
config: {
|
||||||
|
title: "ลำดับการเล่น",
|
||||||
|
steps: [
|
||||||
|
{
|
||||||
|
id: "@id:rotation-step-1",
|
||||||
|
actor: "ตัวละคร A",
|
||||||
|
action: "เริ่มต้นด้วยสกิล",
|
||||||
|
detail: "อธิบายเหตุผลและจังหวะสลับตัว",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "combined-one-page-build",
|
||||||
|
name: "บิลด์รวมหน้าเดียว",
|
||||||
|
description: "สรุปโปรไฟล์ ค่าสำคัญ อันดับอุปกรณ์ และทีมในหน้าเดียว",
|
||||||
|
category: "one-page",
|
||||||
|
builtIn: true,
|
||||||
|
dataSources: [
|
||||||
|
{ key: "builds", name: "ตัวเลือกบิลด์", fields: comparisonFields },
|
||||||
|
],
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
slug: "guide",
|
||||||
|
title: "คู่มือ",
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "profile",
|
||||||
|
config: { title: "ชื่อตัวละคร", summary: "สรุปแนวทางการเล่น", badges: [] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "stat-cards",
|
||||||
|
config: {
|
||||||
|
title: "เป้าหมายค่าสถานะ",
|
||||||
|
items: [
|
||||||
|
{ id: "@id:stat-1", label: "ค่าสำคัญ", value: "—" },
|
||||||
|
{ id: "@id:stat-2", label: "ค่ารอง", value: "—" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "comparison-chart",
|
||||||
|
responsiveLayout: { width: "wide" },
|
||||||
|
config: {
|
||||||
|
title: "เปรียบเทียบบิลด์",
|
||||||
|
dataSourceId: "@source:builds",
|
||||||
|
categoryField: "name",
|
||||||
|
series: [{ field: "score", label: "คะแนน", color: "chart-1" }],
|
||||||
|
presentation: "horizontal-bar",
|
||||||
|
normalization: "raw",
|
||||||
|
sort: "descending",
|
||||||
|
showValues: true,
|
||||||
|
showLegend: false,
|
||||||
|
tableFallback: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "multi-role-reaction-build",
|
||||||
|
name: "บิลด์หลายบทบาทและปฏิกิริยา",
|
||||||
|
description: "แยกแนวทางตามบทบาทหรือปฏิกิริยาด้วยแท็บที่อ่านง่าย",
|
||||||
|
category: "multi-role",
|
||||||
|
builtIn: true,
|
||||||
|
dataSources: [],
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
slug: "roles",
|
||||||
|
title: "บทบาท",
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "profile",
|
||||||
|
config: { title: "ชื่อตัวละคร", summary: "เลือกแนวทางตามทีม", badges: [] },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "tabs",
|
||||||
|
config: {
|
||||||
|
title: "แนวทางบิลด์",
|
||||||
|
tabs: [
|
||||||
|
{ id: "@id:role-1", label: "บทบาทหลัก", content: "อธิบายบิลด์สำหรับบทบาทหลัก" },
|
||||||
|
{ id: "@id:role-2", label: "บทบาทรอง", content: "อธิบายบิลด์สำหรับบทบาทรอง" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "accordion",
|
||||||
|
config: {
|
||||||
|
title: "รายละเอียดปฏิกิริยา",
|
||||||
|
items: [
|
||||||
|
{ id: "@id:reaction-1", title: "ปฏิกิริยาที่ 1", content: "เพิ่มเงื่อนไขและข้อควรระวัง" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "categorized-team-database",
|
||||||
|
name: "ฐานข้อมูลทีมแบบหมวดหมู่",
|
||||||
|
description: "ตารางทีมที่กรองตามหมวด พร้อมรายละเอียดสมาชิกและผลรวม",
|
||||||
|
category: "teams",
|
||||||
|
builtIn: true,
|
||||||
|
dataSources: [
|
||||||
|
{
|
||||||
|
key: "team-database",
|
||||||
|
name: "ฐานข้อมูลทีม",
|
||||||
|
fields: [
|
||||||
|
{ key: "category", label: "หมวด", type: "badge" },
|
||||||
|
{ key: "team", label: "ชื่อทีม", type: "text" },
|
||||||
|
{ key: "members", label: "สมาชิก", type: "text" },
|
||||||
|
{ key: "score", label: "คะแนน", type: "number", displayPrecision: 1 },
|
||||||
|
{ key: "notes", label: "หมายเหตุ", type: "text" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
slug: "teams",
|
||||||
|
title: "ฐานข้อมูลทีม",
|
||||||
|
blocks: [
|
||||||
|
{ type: "section", config: { title: "ค้นหาทีมที่เหมาะกับคุณ", anchor: "teams" } },
|
||||||
|
{
|
||||||
|
type: "table",
|
||||||
|
responsiveLayout: { width: "wide" },
|
||||||
|
config: {
|
||||||
|
dataSourceId: "@source:team-database",
|
||||||
|
columns: [
|
||||||
|
{ field: "category", label: "หมวด", align: "start" },
|
||||||
|
{ field: "team", label: "ทีม", align: "start" },
|
||||||
|
{ field: "members", label: "สมาชิก", align: "start" },
|
||||||
|
{ field: "score", label: "คะแนน", align: "end" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: "calculation-comparison-guide",
|
||||||
|
name: "คู่มือเปรียบเทียบด้วยการคำนวณ",
|
||||||
|
description: "ข้อมูลดิบ สูตรเลขคณิตแบบจำกัด กราฟ และตารางสำรองที่เข้าถึงได้",
|
||||||
|
category: "calculation",
|
||||||
|
builtIn: true,
|
||||||
|
dataSources: [
|
||||||
|
{ key: "comparison", name: "ข้อมูลเปรียบเทียบ", fields: comparisonFields },
|
||||||
|
],
|
||||||
|
pages: [
|
||||||
|
{
|
||||||
|
slug: "comparison",
|
||||||
|
title: "เปรียบเทียบ",
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: "callout",
|
||||||
|
config: { body: "ผลลัพธ์คำนวณจากค่าที่ผู้เขียนกำหนด", tone: "info" },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "comparison-chart",
|
||||||
|
responsiveLayout: { width: "wide" },
|
||||||
|
config: {
|
||||||
|
title: "ผลการเปรียบเทียบ",
|
||||||
|
dataSourceId: "@source:comparison",
|
||||||
|
categoryField: "name",
|
||||||
|
series: [
|
||||||
|
{ field: "base", label: "ค่าพื้นฐาน", color: "chart-2" },
|
||||||
|
{ field: "score", label: "คะแนน", color: "chart-1" },
|
||||||
|
],
|
||||||
|
presentation: "grouped-bar",
|
||||||
|
normalization: "raw",
|
||||||
|
sort: "descending",
|
||||||
|
showValues: true,
|
||||||
|
showLegend: true,
|
||||||
|
tableFallback: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
export { applyTemplate } from "./apply";
|
||||||
|
export type { IdFactory } from "./apply";
|
||||||
|
export { builtInTemplates } from "./built-in";
|
||||||
|
export type {
|
||||||
|
AppliedTemplate,
|
||||||
|
TemplateBlockBlueprint,
|
||||||
|
TemplateBlueprint,
|
||||||
|
TemplateDataSourceBlueprint,
|
||||||
|
TemplatePageBlueprint,
|
||||||
|
} from "./types";
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { applyTemplate, builtInTemplates } from ".";
|
||||||
|
|
||||||
|
const characterId = "a23ff62d-e74a-44fd-a8b1-c3aa62bedf13";
|
||||||
|
|
||||||
|
describe("built-in templates", () => {
|
||||||
|
it("contains the six required reusable structures", () => {
|
||||||
|
expect(builtInTemplates.map((template) => template.slug)).toEqual([
|
||||||
|
"classic-character-guide",
|
||||||
|
"combo-rotation-guide",
|
||||||
|
"combined-one-page-build",
|
||||||
|
"multi-role-reaction-build",
|
||||||
|
"categorized-team-database",
|
||||||
|
"calculation-comparison-guide",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies every template as hidden, independently validated content", () => {
|
||||||
|
for (const template of builtInTemplates) {
|
||||||
|
const applied = applyTemplate(template, characterId);
|
||||||
|
expect(applied.pages.length).toBeGreaterThan(0);
|
||||||
|
expect(applied.pages.every((page) => page.visible === false)).toBe(true);
|
||||||
|
expect(applied.pages.every((page) => page.version === 1)).toBe(true);
|
||||||
|
expect(
|
||||||
|
applied.pages.flatMap((page) => page.blocks).every((block) => block.schemaVersion === 1),
|
||||||
|
).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clones pages, blocks, nested items, and data sources independently", () => {
|
||||||
|
const template = builtInTemplates[0];
|
||||||
|
const first = applyTemplate(template, characterId);
|
||||||
|
const second = applyTemplate(template, characterId);
|
||||||
|
|
||||||
|
expect(first.pages[0].id).not.toBe(second.pages[0].id);
|
||||||
|
expect(first.pages[0].blocks[0].id).not.toBe(second.pages[0].blocks[0].id);
|
||||||
|
expect(first.dataSources[0].id).not.toBe(second.dataSources[0].id);
|
||||||
|
expect(
|
||||||
|
(first.pages[1].blocks[0].config as { dataSourceId: string }).dataSourceId,
|
||||||
|
).toBe(first.dataSources[0].id);
|
||||||
|
expect(
|
||||||
|
(second.pages[1].blocks[0].config as { dataSourceId: string }).dataSourceId,
|
||||||
|
).toBe(second.dataSources[0].id);
|
||||||
|
|
||||||
|
first.pages[0].title = "แก้เฉพาะสำเนาแรก";
|
||||||
|
expect(second.pages[0].title).toBe("ข้อมูล");
|
||||||
|
expect(template.pages[0].title).toBe("ข้อมูล");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies later template edits only to future applications", () => {
|
||||||
|
const before = applyTemplate(builtInTemplates[1], characterId);
|
||||||
|
const edited = structuredClone(builtInTemplates[1]);
|
||||||
|
edited.pages[0].title = "โรเทชันฉบับใหม่";
|
||||||
|
const after = applyTemplate(edited, characterId);
|
||||||
|
|
||||||
|
expect(before.pages[0].title).toBe("คอมโบ");
|
||||||
|
expect(after.pages[0].title).toBe("โรเทชันฉบับใหม่");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import type { DataSourceField, DataSourceRow } from "@/lib/content/types";
|
||||||
|
|
||||||
|
export interface TemplateDataSourceBlueprint {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
fields: DataSourceField[];
|
||||||
|
rows?: Array<Omit<DataSourceRow, "id"> & { id?: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateBlockBlueprint {
|
||||||
|
type: string;
|
||||||
|
schemaVersion?: number;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
responsiveLayout?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplatePageBlueprint {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
publicNote?: string;
|
||||||
|
blocks: TemplateBlockBlueprint[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TemplateBlueprint {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
builtIn: boolean;
|
||||||
|
dataSources: TemplateDataSourceBlueprint[];
|
||||||
|
pages: TemplatePageBlueprint[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppliedTemplate {
|
||||||
|
templateSlug: string;
|
||||||
|
characterId: string;
|
||||||
|
pages: Array<{
|
||||||
|
id: string;
|
||||||
|
characterId: string;
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
publicNote: string | null;
|
||||||
|
navOrder: number;
|
||||||
|
visible: false;
|
||||||
|
version: 1;
|
||||||
|
blocks: Array<{
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
schemaVersion: number;
|
||||||
|
config: Record<string, unknown>;
|
||||||
|
responsiveLayout: Record<string, unknown>;
|
||||||
|
sortOrder: number;
|
||||||
|
}>;
|
||||||
|
}>;
|
||||||
|
dataSources: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
version: 1;
|
||||||
|
fields: DataSourceField[];
|
||||||
|
rows: DataSourceRow[];
|
||||||
|
}>;
|
||||||
|
}
|
||||||
+2
-1
@@ -13,7 +13,8 @@
|
|||||||
"test:coverage": "vitest run --coverage",
|
"test:coverage": "vitest run --coverage",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "bun scripts/migrate.ts",
|
"db:migrate": "bun scripts/migrate.ts",
|
||||||
"db:studio": "drizzle-kit studio"
|
"db:studio": "drizzle-kit studio",
|
||||||
|
"db:seed": "bun scripts/seed-templates.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"better-auth": "^1.7.2",
|
"better-auth": "^1.7.2",
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { closeDb, getDb } from "@/db";
|
||||||
|
import { templates } from "@/db/schema";
|
||||||
|
import { builtInTemplates } from "@/lib/templates";
|
||||||
|
|
||||||
|
const db = getDb();
|
||||||
|
|
||||||
|
for (const template of builtInTemplates) {
|
||||||
|
await db
|
||||||
|
.insert(templates)
|
||||||
|
.values({
|
||||||
|
slug: template.slug,
|
||||||
|
name: template.name,
|
||||||
|
description: template.description,
|
||||||
|
category: template.category,
|
||||||
|
structure: template as unknown as Record<string, unknown>,
|
||||||
|
builtIn: true,
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: templates.slug,
|
||||||
|
set: {
|
||||||
|
name: template.name,
|
||||||
|
description: template.description,
|
||||||
|
category: template.category,
|
||||||
|
structure: template as unknown as Record<string, unknown>,
|
||||||
|
builtIn: true,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await closeDb();
|
||||||
Reference in New Issue
Block a user