feat(pages): add reusable page block editor and renderer baseline
This commit is contained in:
@@ -6,6 +6,8 @@ import {
|
||||
createCustomerInputSchema,
|
||||
createNavigationMenuInputSchema,
|
||||
createPageInputSchema,
|
||||
parsePageBlocks,
|
||||
serializePageBlocks,
|
||||
updateCommissionStatusInputSchema,
|
||||
updateNavigationItemInputSchema,
|
||||
} from "./index"
|
||||
@@ -64,4 +66,23 @@ describe("domain schemas", () => {
|
||||
expect(menu.success).toBe(true)
|
||||
expect(navUpdate.success).toBe(false)
|
||||
})
|
||||
|
||||
it("parses and serializes page blocks with legacy fallback", () => {
|
||||
const legacy = parsePageBlocks("Legacy body")
|
||||
expect(legacy[0]?.type).toBe("rich_text")
|
||||
|
||||
const serialized = serializePageBlocks([
|
||||
{
|
||||
id: "hero-1",
|
||||
type: "hero",
|
||||
heading: "Hello",
|
||||
subheading: null,
|
||||
ctaLabel: null,
|
||||
ctaHref: null,
|
||||
},
|
||||
])
|
||||
|
||||
const parsed = parsePageBlocks(serialized)
|
||||
expect(parsed[0]?.type).toBe("hero")
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,6 +3,98 @@ import { z } from "zod"
|
||||
export const pageStatusSchema = z.enum(["draft", "published"])
|
||||
export const pageLocaleSchema = z.enum(["de", "en", "es", "fr"])
|
||||
|
||||
const pageBlockBaseSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export const heroPageBlockSchema = pageBlockBaseSchema.extend({
|
||||
type: z.literal("hero"),
|
||||
heading: z.string().min(1).max(180),
|
||||
subheading: z.string().max(500).nullable().optional(),
|
||||
ctaLabel: z.string().max(80).nullable().optional(),
|
||||
ctaHref: z.string().max(500).nullable().optional(),
|
||||
})
|
||||
|
||||
export const richTextPageBlockSchema = pageBlockBaseSchema.extend({
|
||||
type: z.literal("rich_text"),
|
||||
body: z.string().min(1),
|
||||
})
|
||||
|
||||
export const galleryPageBlockSchema = pageBlockBaseSchema.extend({
|
||||
type: z.literal("gallery"),
|
||||
title: z.string().max(180).nullable().optional(),
|
||||
imageIds: z.array(z.string().uuid()).default([]),
|
||||
})
|
||||
|
||||
export const ctaPageBlockSchema = pageBlockBaseSchema.extend({
|
||||
type: z.literal("cta"),
|
||||
label: z.string().min(1).max(80),
|
||||
href: z.string().max(500),
|
||||
variant: z.enum(["primary", "secondary"]).default("primary"),
|
||||
})
|
||||
|
||||
export const formPageBlockSchema = pageBlockBaseSchema.extend({
|
||||
type: z.literal("form"),
|
||||
formKey: z.string().min(1).max(120),
|
||||
title: z.string().max(180).nullable().optional(),
|
||||
description: z.string().max(500).nullable().optional(),
|
||||
})
|
||||
|
||||
export const priceCardsPageBlockSchema = pageBlockBaseSchema.extend({
|
||||
type: z.literal("price_cards"),
|
||||
title: z.string().max(180).nullable().optional(),
|
||||
cards: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1).max(180),
|
||||
price: z.string().max(80).nullable().optional(),
|
||||
description: z.string().max(500).nullable().optional(),
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
})
|
||||
|
||||
export const pageBlockSchema = z.discriminatedUnion("type", [
|
||||
heroPageBlockSchema,
|
||||
richTextPageBlockSchema,
|
||||
galleryPageBlockSchema,
|
||||
ctaPageBlockSchema,
|
||||
formPageBlockSchema,
|
||||
priceCardsPageBlockSchema,
|
||||
])
|
||||
|
||||
export const pageBlocksSchema = z.array(pageBlockSchema)
|
||||
|
||||
function isJsonLike(value: string): boolean {
|
||||
const trimmed = value.trim()
|
||||
return trimmed.startsWith("{") || trimmed.startsWith("[")
|
||||
}
|
||||
|
||||
export function parsePageBlocks(content: string): z.infer<typeof pageBlocksSchema> {
|
||||
if (!isJsonLike(content)) {
|
||||
return [
|
||||
{
|
||||
id: "legacy-rich-text",
|
||||
type: "rich_text",
|
||||
body: content,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const parsed = JSON.parse(content)
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error("Page block payload must be an array.")
|
||||
}
|
||||
|
||||
return pageBlocksSchema.parse(parsed)
|
||||
}
|
||||
|
||||
export function serializePageBlocks(blocks: z.infer<typeof pageBlocksSchema>): string {
|
||||
return JSON.stringify(pageBlocksSchema.parse(blocks))
|
||||
}
|
||||
|
||||
export const createPageInputSchema = z.object({
|
||||
title: z.string().min(1).max(180),
|
||||
slug: z.string().min(1).max(180),
|
||||
@@ -67,3 +159,5 @@ export type UpsertPageTranslationInput = z.infer<typeof upsertPageTranslationInp
|
||||
export type CreateNavigationMenuInput = z.infer<typeof createNavigationMenuInputSchema>
|
||||
export type CreateNavigationItemInput = z.infer<typeof createNavigationItemInputSchema>
|
||||
export type UpdateNavigationItemInput = z.infer<typeof updateNavigationItemInputSchema>
|
||||
export type PageBlock = z.infer<typeof pageBlockSchema>
|
||||
export type PageBlocks = z.infer<typeof pageBlocksSchema>
|
||||
|
||||
Reference in New Issue
Block a user