Files
homaroma/packages/types/index.ts
T
2026-10-06 15:35:04 +07:00

1387 lines
47 KiB
TypeScript

import { z } from "zod";
export interface Brand {
id: number
name: string
description?: string | null
}
export const CATEGORY_MEDIA_ROLES = [
'thumbnail',
'hero_desktop',
'hero_mobile',
'card_portrait',
'social_share',
] as const
export type CategoryMediaRole = typeof CATEGORY_MEDIA_ROLES[number]
export interface CategoryMedia {
id?: number
categoryId?: number
role: CategoryMediaRole
url: string
width?: number | null
height?: number | null
focalX?: number | null
focalY?: number | null
sortOrder?: number
}
export interface CategoryFeaturedSeries {
enabled: boolean
badge?: string
title?: string
subtitle?: string
description?: string
ctaText?: string
ctaUrl?: string
imageUrl?: string
actionImageUrl?: string
features?: string[]
}
export interface CategoryUspBenefit {
icon?: string
title: string
description?: string
}
export interface CategoryFilterConfig {
showCategoryNav?: boolean // Toggle Category Pills Bar
showSeries?: boolean
showType?: boolean
showColor?: boolean
showFitment?: boolean
showPrice?: boolean
showStock?: boolean
}
export interface Category {
id: number
name: TranslatableString
imageUrl?: string | null
media?: CategoryMedia[]
layoutType?: 'split_showcase' | 'editorial_hero' | 'dense_catalog' | 'none' | string | null
featuredSeries?: CategoryFeaturedSeries | null
uspBenefits?: CategoryUspBenefit[] | null
filterConfig?: CategoryFilterConfig | null
sections?: CategorySection[]
availableFilters?: AttributeDefinition[]
_count?: { products: number }
translations?: { langCode: string; name: string }[]
createdAt?: string
updatedAt?: string
}
const CATEGORY_MEDIA_FALLBACKS: Record<CategoryMediaRole, CategoryMediaRole[]> = {
thumbnail: ['thumbnail'],
hero_desktop: ['hero_desktop', 'thumbnail'],
hero_mobile: ['hero_mobile', 'hero_desktop', 'thumbnail'],
card_portrait: ['card_portrait', 'thumbnail'],
social_share: ['social_share', 'hero_desktop', 'thumbnail'],
}
export function getCategoryMedia(
category: Pick<Category, 'media'> | null | undefined,
role: CategoryMediaRole,
): CategoryMedia | undefined {
const media = Array.isArray(category?.media) ? category.media : []
for (const candidateRole of CATEGORY_MEDIA_FALLBACKS[role]) {
const candidate = media.find((item) => item.role === candidateRole && item.url?.trim())
if (candidate) return candidate
}
return undefined
}
export function resolveCategoryMediaUrl(
category: Pick<Category, 'media' | 'imageUrl'> | null | undefined,
role: CategoryMediaRole,
fallbackUrl = '',
): string {
return getCategoryMedia(category, role)?.url || category?.imageUrl || fallbackUrl
}
export interface Variant {
id: number
sku?: string | null
barcode?: string | null
price: number | null
stock?: number
option1: string
option2: string
option3: string
imageUrl?: string | null
bomItems?: BomItem[]
}
export interface RawMaterial {
id: number
name: string
unit: string
pricePerUnit: number
}
export interface BomItem {
id?: number
rawMaterialId: number
rawMaterial?: RawMaterial
quantity: number
}
export interface ProductImage {
id: number
url: string
mobileUrl?: string | null
order: number
}
const OptionalProductImageMobileUrlSchema = z
.union([z.string(), z.null()])
.optional()
.transform((value) => {
if (value === undefined) return undefined
if (value === null) return null
const normalized = value.trim()
return normalized.length > 0 ? normalized : null
})
.refine((value) => value === undefined || value === null || !/^data:video\//i.test(value), 'Product image URL must reference an image')
.refine((value) => value === undefined || value === null || !/\.(?:mp4|webm|mov|ogv|ogg)(?:[?#]|$)/i.test(value), 'Product image URL must reference an image')
const ProductImageUrlSchema = z.string()
.trim()
.min(1, 'Product image URL is required')
.refine((value) => !/^data:video\//i.test(value), 'Product image URL must reference an image')
.refine((value) => !/\.(?:mp4|webm|mov|ogv|ogg)(?:[?#]|$)/i.test(value), 'Product image URL must reference an image')
export const ProductImageObjectInputSchema = z.object({
id: z.number().int().positive().optional(),
url: ProductImageUrlSchema,
mobileUrl: OptionalProductImageMobileUrlSchema,
order: z.number().int().optional(),
})
export const ProductImageInputSchema = z.union([
ProductImageUrlSchema.transform((url) => ({
url,
})),
ProductImageObjectInputSchema,
])
export type ProductImageInput = z.input<typeof ProductImageInputSchema>
export type NormalizedProductImageInput = z.output<typeof ProductImageInputSchema>
export type TranslatableString = string | Record<string, string>;
export interface AttributeDefinitionTranslation {
id: number
attributeDefinitionId: number
langCode: string
label: string
description?: string | null
}
export interface AttributeDefinition {
id: number
code: string
label: TranslatableString
description?: TranslatableString | null
type: 'TEXT' | 'NUMBER' | 'BOOLEAN' | 'SELECT' | 'JSON'
unit?: string | null
options?: TranslatableString[] | null
isRequired: boolean
isFilterable: boolean
sortOrder: number
categoryId?: number | null
category?: Category | null
translations?: AttributeDefinitionTranslation[]
}
export interface ProductAttributeTranslation {
id: number
productAttributeId: number
langCode: string
valueText: string
}
export interface ProductAttribute {
id: number
productId: number
attributeDefinitionId: number
attributeDefinition: AttributeDefinition
valueText?: string | null
valueNumber?: number | null
valueBoolean?: boolean | null
valueJson?: any | null
value?: string | number | boolean | Record<string, any> | null
valueTranslations?: Record<string, string>
translations?: ProductAttributeTranslation[]
}
export interface Product {
id: number
name: TranslatableString
titleGraphicUrl?: string | null
description: TranslatableString | null
details: TranslatableString | null
price: number | null
categoryId: number
category: Category
brand: Brand
options?: Array<{ name: TranslatableString; values: TranslatableString[] }> | null
variants: Variant[]
images: ProductImage[]
attributes?: ProductAttribute[]
isDeleted?: boolean
deletedAt?: string | null
}
const DEFAULT_DEV_API_PORT = 3051
function resolveApiBase() {
const viteEnv = (typeof import.meta !== 'undefined' ? (import.meta as any).env : undefined)
const envValue = viteEnv?.VITE_API_URL || (globalThis as any).process?.env?.VITE_API_URL
if (envValue) {
const isDev = Boolean(viteEnv?.DEV)
const isLocalhostUrl = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?\/api\/?$/.test(envValue)
const win = (globalThis as any).window
if (isDev && typeof win !== 'undefined' && isLocalhostUrl) {
return `${win.location.protocol}//${win.location.hostname}:${DEFAULT_DEV_API_PORT}/api`
}
return envValue
}
const win = (globalThis as any).window
if (typeof win !== 'undefined') {
return `${win.location.protocol}//${win.location.hostname}:${DEFAULT_DEV_API_PORT}/api`
}
return 'http://localhost:3000/api'
}
export const API_BASE = resolveApiBase()
export const CATEGORY_IMAGES: Record<string, string> = {
'Oil Ozone': '/images/oil_ozone.png',
'Aroma Oil': '/images/oil_ozone.png',
'Reed Diffuser': '/images/reed_diffuser.png',
'Room Spray': '/images/reed_diffuser.png',
'Car Diffuser': '/images/oil_ozone.png',
}
export function getCategoryImage(name: string): string {
return CATEGORY_IMAGES[name] ?? '/images/oil_ozone.png'
}
export function formatPrice(price: number | null | undefined): string {
if (price == null) return '—'
return `฿${price.toLocaleString()}`
}
export interface Hero {
id: number
tagline: string
title: string
description: string
imageUrl: string
buttonText: string
buttonLink: string
isActive: boolean
createdAt: string
updatedAt: string
}
export interface StoreSettingsTranslation {
id: number
storeSettingsId: number
langCode: string
footerDescription?: string | null
newsletterTitle?: string | null
newsletterDescription?: string | null
newsletterPlaceholder?: string | null
newsletterButtonText?: string | null
defaultSeoTitle?: string | null
defaultSeoDescription?: string | null
brandPrompt?: string | null
createdAt?: string
updatedAt?: string
}
export const FooterColumnSchema = z.enum([
'brand',
'explore',
'collections',
'support',
])
export const FooterFinaleSettingsSchema = z.object({
enabled: z.boolean(),
mode: z.enum(['static', 'curtain']),
mobileHeight: z.enum(['compact', 'medium', 'large']),
desktopHeight: z.enum(['compact', 'half', 'screen']),
logoSize: z.enum(['sm', 'md', 'lg', 'xl']),
}).strict()
export const FooterLayoutSettingsSchema = z.object({
columns: z.array(FooterColumnSchema).min(1).max(4).superRefine((columns, context) => {
if (new Set(columns).size !== columns.length) {
context.addIssue({
code: 'custom',
message: 'Footer columns must be unique',
})
}
}),
mobileColumns: z.union([z.literal(1), z.literal(2)]),
desktopColumns: z.union([z.literal(2), z.literal(3), z.literal(4)]),
containerWidth: z.enum(['narrow', 'standard', 'wide', 'full']),
verticalPadding: z.enum(['sm', 'md', 'lg', 'xl']),
columnGap: z.enum(['sm', 'md', 'lg', 'xl']),
headingSize: z.enum(['sm', 'md', 'lg']),
linkSize: z.enum(['sm', 'md', 'lg']),
brandLogoSize: z.enum(['sm', 'md', 'lg', 'xl']),
socialSize: z.enum(['sm', 'md', 'lg']),
finale: FooterFinaleSettingsSchema,
}).strict()
export const FooterLayoutSettingsPatchSchema = FooterLayoutSettingsSchema
.partial()
.extend({ finale: FooterFinaleSettingsSchema.partial().strict().optional() })
.strict()
export type FooterColumn = z.infer<typeof FooterColumnSchema>
export type FooterFinaleSettings = z.infer<typeof FooterFinaleSettingsSchema>
export type FooterLayoutSettings = z.infer<typeof FooterLayoutSettingsSchema>
export type FooterLayoutSettingsPatch = z.infer<typeof FooterLayoutSettingsPatchSchema>
export const DEFAULT_FOOTER_LAYOUT_SETTINGS: FooterLayoutSettings = {
columns: ['brand', 'explore', 'collections', 'support'],
mobileColumns: 1,
desktopColumns: 4,
containerWidth: 'standard',
verticalPadding: 'lg',
columnGap: 'lg',
headingSize: 'sm',
linkSize: 'md',
brandLogoSize: 'md',
socialSize: 'md',
finale: {
enabled: true,
mode: 'curtain',
mobileHeight: 'medium',
desktopHeight: 'screen',
logoSize: 'xl',
},
}
export interface StoreSettings {
id: number
brandName: string
logoUrl?: string | null
themePreset?: string | null
themeTokens?: Record<string, string> | null
shippingFee: number
bankName: string
bankAccountName: string
bankAccountNumber: string
pointsEarnRate: number
footerDescription?: string | null
facebookUrl?: string | null
instagramUrl?: string | null
tiktokUrl?: string | null
newsletterTitle?: string | null
newsletterDescription?: string | null
newsletterPlaceholder?: string | null
newsletterButtonText?: string | null
footerSupportHeading?: any | null
footerCopyrightText?: any | null
footerLayout?: FooterLayoutSettings | null
scrollRevealEnabled?: boolean | null
scrollRevealStyle?: string | null
scrollRevealDuration?: number | null
headerStyle?: 'single_line' | 'two_tier' | string | null
headerBehavior?: 'always_sticky' | 'hide_on_scroll' | string | null
mobileHeaderStyle?: 'logo_center' | 'logo_left' | 'search_bar_below' | string | null
mobileHeaderBehavior?: 'always_sticky' | 'hide_on_scroll' | string | null
defaultSeoTitle?: string | null
defaultSeoDescription?: string | null
defaultOgImage?: string | null
brandPrompt?: string | null
translations?: StoreSettingsTranslation[]
}
export interface BrandKnowledge {
id: number
brandName: string
tagline?: string | null
story?: string | null
targetAudience?: string | null
toneOfVoice?: string | null
visualArtPrompt?: string | null
productUsps?: string | null
marketingGuidelines?: string | null
customRules?: string | null
masterPromptOverride?: string | null
createdAt?: string
updatedAt?: string
}
export interface FooterBrand {
brandName: string
logoUrl?: string | null
description: Record<string, string>
socialLinks: {
facebookUrl?: string | null
instagramUrl?: string | null
tiktokUrl?: string | null
}
}
export interface FooterBrandResponse {
version: string
brand: FooterBrand
}
export interface MenuItem {
id: number
label: string
url: string
order: number
columnImage?: string | null
hoverImage?: string | null
translations?: any[]
}
export interface Menu {
id: number
name: string
items: MenuItem[]
payload?: any
rootImage?: string | null
}
export interface OrderItem {
id: number
orderId: number
productId: number
variantId?: number | null
quantity: number
priceAtPurchase: number
productName: string
variantName?: string | null
productImage?: string | null
}
export interface Order {
id: number
customerName: string
customerEmail: string
customerPhone: string
shippingAddress: string
subtotal?: number
promoLevel1Name?: string | null
promoLevel1Disc?: number
promoLevel2Name?: string | null
promoLevel2Disc?: number
couponCode?: string | null
discountAmount?: number
pointsUsed?: number
pointsDiscount?: number
pointsEarned?: number
totalAmount: number
shippingFee: number
status: string
channel?: string
paymentMethod?: string | null
receivedAmount?: number | null
changeAmount?: number | null
cashierId?: number | null
paymentSlipUrl?: string | null
trackingNo?: string | null
customerId?: number | null
customer?: any | null
items: OrderItem[]
createdAt: string
updatedAt: string
}
export interface Article {
id: number
slug: string
title: string
coverImage: string | null
content: string
isPublished: boolean
createdAt: string
updatedAt: string
}
export interface CartItem {
cartItemId: string
productId: number
variantId?: number | null
quantity: number
price: number
productName: string
variantName?: string | null
productImage?: string | null
}
export interface PageTranslation {
id: number
pageId: number
langCode: string
title: string
seoTitle?: string | null
seoDescription?: string | null
seoKeywords?: string | null
createdAt?: string
updatedAt?: string
}
export interface Page {
id: number
slug: string
title: string
seoTitle?: string | null
seoDescription?: string | null
seoKeywords?: string | null
ogImageUrl?: string | null
isPublished: boolean
sections?: PageSection[]
translations?: PageTranslation[]
createdAt?: string
updatedAt?: string
}
export interface PageSection {
id: number
pageId: number
type: string // e.g. 'HERO' | 'CATEGORIES' | 'PRODUCTS' | 'BANNER' | 'TEXT'
order: number
payload: any // Specific payload depending on type
isActive: boolean
createdAt?: string
updatedAt?: string
}
export interface ProductDetailSection {
id: number
productId: number
type: string
order: number
payload: any
isActive: boolean
isLocked: boolean
createdAt?: string
updatedAt?: string
}
export interface CategorySection {
id: number
categoryId: number
type: string
order: number
payload: any
isActive: boolean
isLocked: boolean
createdAt?: string
updatedAt?: string
}
export const LocalizedStringSchema = z.union([z.string(), z.record(z.string(), z.string())]);
export const ContentPositionSchema = z.enum([
'top-left', 'top-center', 'top-right',
'center-left', 'center', 'center-right',
'bottom-left', 'bottom-center', 'bottom-right'
]);
export type ContentPosition = z.infer<typeof ContentPositionSchema>;
export const OptionalContentPositionSchema = z
.union([ContentPositionSchema, z.literal(''), z.null()])
.optional()
.transform((val) => (val ? (val as ContentPosition) : undefined));
export const DesktopContentPositionSchema = (fallback: ContentPosition) =>
z
.union([ContentPositionSchema, z.literal(''), z.null()])
.optional()
.transform((val) => (val ? (val as ContentPosition) : fallback));
export const ContentWidthSchema = z.enum(['full', 'standard', 'compact']);
export type ContentWidth = z.infer<typeof ContentWidthSchema>;
export const DefaultContentWidthSchema = (fallback: ContentWidth = 'standard') =>
z
.union([ContentWidthSchema, z.literal(''), z.null()])
.optional()
.transform((val) => (val ? (val as ContentWidth) : fallback));
export const TextColorSchemeSchema = z.enum(['light', 'dark']);
export type TextColorScheme = z.infer<typeof TextColorSchemeSchema>;
export const DefaultTextColorSchemeSchema = (fallback: TextColorScheme = 'light') =>
z
.union([TextColorSchemeSchema, z.literal(''), z.null()])
.optional()
.transform((val) => (val ? (val as TextColorScheme) : fallback));
export const HeroSectionPayloadSchema = z.object({
tagline: LocalizedStringSchema.optional(),
title: LocalizedStringSchema.optional(),
description: LocalizedStringSchema.optional(),
imageUrl: z.string().min(1),
buttonText: LocalizedStringSchema.optional(),
buttonLink: z.string().optional(),
focalPoint: z.object({ x: z.string(), y: z.string() }).optional(),
buttonPosition: DesktopContentPositionSchema('center'),
mobileButtonPosition: OptionalContentPositionSchema,
contentWidth: DefaultContentWidthSchema('standard'),
textColorScheme: DefaultTextColorSchemeSchema('light'),
overlayStyle: z.enum(['gradient', 'solid', 'none']).optional().default('gradient'),
overlayOpacity: z.number().min(0).max(100).optional(),
});
export type HeroSectionPayload = z.infer<typeof HeroSectionPayloadSchema>;
export const CategoriesSectionPayloadSchema = z.object({
title: LocalizedStringSchema,
subtitle: LocalizedStringSchema.optional(),
categoryIds: z.array(z.number()).optional(),
});
export type CategoriesSectionPayload = z.infer<typeof CategoriesSectionPayloadSchema>;
export const ProductsSectionPayloadSchema = z.object({
title: LocalizedStringSchema,
subtitle: LocalizedStringSchema.optional(),
type: z.enum(['featured', 'brand', 'custom']),
categoryId: z.number().optional(),
brandId: z.number().optional(),
productIds: z.array(z.number()).optional(),
limit: z.number().optional(),
layout: z.enum(['grid', 'carousel']).optional(),
enableQuickShop: z.boolean().optional(),
});
export type ProductsSectionPayload = z.infer<typeof ProductsSectionPayloadSchema>;
export const BannerSectionPayloadSchema = z.object({
imageUrl: z.string().min(1),
linkUrl: z.string().optional(),
title: LocalizedStringSchema.optional(),
description: LocalizedStringSchema.optional(),
buttonText: LocalizedStringSchema.optional(),
focalPoint: z.object({ x: z.string(), y: z.string() }).optional(),
buttonPosition: DesktopContentPositionSchema('center-left'),
mobileButtonPosition: OptionalContentPositionSchema,
contentWidth: DefaultContentWidthSchema('standard'),
textColorScheme: DefaultTextColorSchemeSchema('light'),
});
export type BannerSectionPayload = z.infer<typeof BannerSectionPayloadSchema>;
export const TextSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
content: LocalizedStringSchema,
textAlign: z.enum(['left', 'center', 'right']).optional(),
});
export type TextSectionPayload = z.infer<typeof TextSectionPayloadSchema>;
export const AnnouncementBarSectionPayloadSchema = z.object({
slides: z.array(LocalizedStringSchema).min(1),
backgroundColor: z.string().optional(),
textColor: z.string().optional(),
speed: z.number().optional(),
});
export type AnnouncementBarSectionPayload = z.infer<typeof AnnouncementBarSectionPayloadSchema>;
export const NewsletterSectionPayloadSchema = z.object({
title: LocalizedStringSchema,
description: LocalizedStringSchema,
buttonText: LocalizedStringSchema,
placeholderText: LocalizedStringSchema.optional(),
showPrivacyCheckbox: z.boolean(),
privacyText: LocalizedStringSchema.optional(),
});
export type NewsletterSectionPayload = z.infer<typeof NewsletterSectionPayloadSchema>;
export const EditorialTileSectionPayloadSchema = z.object({
title: LocalizedStringSchema,
description: LocalizedStringSchema,
imageUrl: z.string().min(1),
imagePosition: z.enum(['left', 'right']),
imageAspectRatio: z.enum(['landscape', 'wide', 'square', 'portrait', 'tall', 'auto']).optional(),
buttonText: LocalizedStringSchema.optional(),
buttonLink: z.string().optional(),
backgroundColor: z.string().optional(),
textColor: z.string().optional(),
});
export type EditorialTileSectionPayload = z.infer<typeof EditorialTileSectionPayloadSchema>;
export const FeaturedTilesSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
subtitle: LocalizedStringSchema.optional(),
columns: z.number().optional().default(2),
layoutPreset: z.enum(['editorial', 'spotlight', 'custom']).optional(),
textAlign: z.enum(['left', 'center', 'right']).optional(),
ctaStyle: z.enum(['arrow_link', 'pill_button', 'solid_button', 'none']).optional(),
imageAspectRatio: z.enum(['landscape', 'wide', 'square', 'portrait', 'tall', 'auto']).optional(),
imageRounded: z.enum(['none', 'sm', 'md', 'xl', '2xl', '3xl', 'full']).optional(),
backgroundColor: z.string().optional(),
textColor: z.string().optional(),
tiles: z.array(z.object({
title: LocalizedStringSchema,
description: LocalizedStringSchema.optional(),
imageUrl: z.string().min(1),
imageAspectRatio: z.enum(['landscape', 'wide', 'square', 'portrait', 'tall', 'auto']).optional(),
buttonText: LocalizedStringSchema.optional(),
buttonLink: z.string().optional(),
})).min(1)
});
export type FeaturedTilesSectionPayload = z.infer<typeof FeaturedTilesSectionPayloadSchema>;
export const HeroVideoSectionPayloadSchema = z.object({
tagline: LocalizedStringSchema.optional(),
title: LocalizedStringSchema.optional(),
description: LocalizedStringSchema.optional(),
videoUrl: z.string().min(1),
posterImageUrl: z.string().optional(),
buttonText: LocalizedStringSchema.optional(),
buttonLink: z.string().optional(),
secondaryButtonText: LocalizedStringSchema.optional(),
secondaryButtonLink: z.string().optional(),
overlayOpacity: z.number().optional().default(40),
height: z.enum(['full', 'large', 'medium']).optional().default('large'),
muted: z.boolean().optional().default(true),
autoplay: z.boolean().optional().default(true),
loop: z.boolean().optional().default(true),
backgroundColor: z.string().optional(),
textColor: z.string().optional(),
focalPoint: z.object({ x: z.string(), y: z.string() }).optional(),
buttonPosition: DesktopContentPositionSchema('center'),
mobileButtonPosition: OptionalContentPositionSchema,
contentWidth: DefaultContentWidthSchema('standard'),
textColorScheme: DefaultTextColorSchemeSchema('light'),
});
export type HeroVideoSectionPayload = z.infer<typeof HeroVideoSectionPayloadSchema>;
export const EditorialCarouselSectionPayloadSchema = z.object({
title: LocalizedStringSchema,
backgroundColor: z.string().optional(),
textColor: z.string().optional(),
layout: z.enum(['standard', 'split_screen']).optional(),
imagePosition: z.enum(['left', 'right']).optional(),
slides: z.array(z.object({
category: LocalizedStringSchema,
title: LocalizedStringSchema,
description: LocalizedStringSchema,
imageUrl: z.string().min(1),
buttonText: LocalizedStringSchema.optional(),
buttonLink: z.string().optional(),
})).min(1)
});
export type EditorialCarouselSectionPayload = z.infer<typeof EditorialCarouselSectionPayloadSchema>;
export const HeroCarouselSlideSchema = z.object({
imageUrl: z.string().min(1),
mobileImageUrl: z.string().optional(),
linkUrl: z.string().optional(),
alt: LocalizedStringSchema.optional(),
title: LocalizedStringSchema.optional(),
subtitle: LocalizedStringSchema.optional(),
buttonText: LocalizedStringSchema.optional(),
focalPoint: z.object({ x: z.string(), y: z.string() }).optional(),
buttonPosition: DesktopContentPositionSchema('bottom-left'),
mobileButtonPosition: OptionalContentPositionSchema,
contentWidth: DefaultContentWidthSchema('standard'),
textColorScheme: DefaultTextColorSchemeSchema('light'),
});
export type HeroCarouselSlide = z.infer<typeof HeroCarouselSlideSchema>;
export const HeroCarouselSectionPayloadSchema = z.object({
slides: z.array(HeroCarouselSlideSchema).min(1),
autoplay: z.boolean().optional().default(true),
interval: z.number().optional().default(4500),
aspectRatio: z.enum(['21/6', '21/7', '21/9', '16/5', '16/6', '4/1', '5/1', 'auto']).optional().default('21/7'),
mobileAspectRatio: z.enum(['16/9', '4/3', '1/1', '21/9', 'auto']).optional().default('16/9'),
showArrows: z.boolean().optional().default(true),
showDots: z.boolean().optional().default(true),
backgroundColor: z.string().optional(),
});
export type HeroCarouselSectionPayload = z.infer<typeof HeroCarouselSectionPayloadSchema>;
export const DividerSectionPayloadSchema = z.object({
width: z.enum(['full', 'container', 'narrow']),
style: z.enum(['solid', 'dashed', 'dotted']),
thickness: z.number().int().min(1).max(4),
spacing: z.enum(['none', 'sm', 'md', 'lg', 'xl']),
color: z.string().regex(
/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i,
'Color must be a hex value in #RGB, #RGBA, #RRGGBB, or #RRGGBBAA format',
).optional(),
});
export type DividerSectionPayload = z.infer<typeof DividerSectionPayloadSchema>;
export interface CustomerAddress {
id: number
customerId: number
recipientName: string
phone: string
addressLine: string
subdistrict: string
district: string
province: string
zipcode: string
isDefault: boolean
createdAt?: string
updatedAt?: string
}
export const HeroArticleSectionPayloadSchema = z.object({
title: LocalizedStringSchema,
subtitle: LocalizedStringSchema.optional(),
author: LocalizedStringSchema.optional(),
category: LocalizedStringSchema.optional(),
publishDate: z.string().optional(),
date: z.string().optional(),
imageUrl: z.string().optional(),
backgroundImageUrl: z.string().optional(),
overlayColor: z.string().optional(),
});
export type HeroArticleSectionPayload = z.infer<typeof HeroArticleSectionPayloadSchema>;
export const RichTextSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
content: LocalizedStringSchema,
});
export type RichTextSectionPayload = z.infer<typeof RichTextSectionPayloadSchema>;
export const ArticleMediaSectionPayloadSchema = z.object({
heading: LocalizedStringSchema.optional(),
body: LocalizedStringSchema,
imageUrl: z.string().min(1).optional(),
imageAlt: LocalizedStringSchema.optional(),
imageCaption: LocalizedStringSchema.optional(),
imagePosition: z.enum(['left', 'right', 'top']).optional().default('right'),
width: z.enum(['narrow', 'standard', 'wide']).optional().default('standard'),
background: z.enum(['default', 'muted']).optional().default('default'),
}).strict().superRefine((payload, ctx) => {
if (payload.imageUrl && !payload.imageAlt) {
ctx.addIssue({
code: 'custom',
path: ['imageAlt'],
message: 'imageAlt is required when imageUrl is provided',
});
}
});
export type ArticleMediaSectionPayload = z.infer<typeof ArticleMediaSectionPayloadSchema>;
export const ArticleHeaderSectionPayloadSchema = z.object({
title: LocalizedStringSchema,
subtitle: LocalizedStringSchema.optional(),
category: LocalizedStringSchema.optional(),
author: LocalizedStringSchema.optional(),
publishDate: z.string().optional(),
imageUrl: z.string().min(1).optional(),
imageAlt: LocalizedStringSchema.optional(),
}).strict().superRefine((payload, ctx) => {
if (payload.imageUrl && !payload.imageAlt) {
ctx.addIssue({ code: 'custom', path: ['imageAlt'], message: 'imageAlt is required when imageUrl is provided' });
}
});
export type ArticleHeaderSectionPayload = z.infer<typeof ArticleHeaderSectionPayloadSchema>;
export const ArticleGallerySectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
layout: z.enum(['grid', 'featured']).optional().default('grid'),
images: z.array(z.object({
imageUrl: z.string().min(1),
alt: LocalizedStringSchema,
caption: LocalizedStringSchema.optional(),
}).strict()).min(2).max(8),
}).strict();
export type ArticleGallerySectionPayload = z.infer<typeof ArticleGallerySectionPayloadSchema>;
export const ArticleFactsSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
items: z.array(z.object({
title: LocalizedStringSchema,
description: LocalizedStringSchema.optional(),
}).strict()).min(1).max(8),
}).strict();
export type ArticleFactsSectionPayload = z.infer<typeof ArticleFactsSectionPayloadSchema>;
export const ArticleVideoSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
videoUrl: z.string().url().startsWith('https://'),
caption: LocalizedStringSchema.optional(),
posterUrl: z.string().min(1).optional(),
aspectRatio: z.enum(['16:9', '4:3', '1:1']).optional().default('16:9'),
}).strict();
export type ArticleVideoSectionPayload = z.infer<typeof ArticleVideoSectionPayloadSchema>;
export const ArticleReferencesSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
items: z.array(z.object({
title: LocalizedStringSchema,
url: z.string().url().startsWith('https://').optional(),
note: LocalizedStringSchema.optional(),
}).strict()).min(1).max(20),
}).strict();
export type ArticleReferencesSectionPayload = z.infer<typeof ArticleReferencesSectionPayloadSchema>;
export const ArticleTableSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
columns: z.array(LocalizedStringSchema).min(2).max(6),
rows: z.array(z.object({
label: LocalizedStringSchema,
cells: z.array(LocalizedStringSchema).min(2).max(6),
}).strict()).min(1).max(20),
note: LocalizedStringSchema.optional(),
}).strict().superRefine((payload, ctx) => {
payload.rows.forEach((row, index) => {
if (row.cells.length !== payload.columns.length) {
ctx.addIssue({
code: 'custom',
path: ['rows', index, 'cells'],
message: `Row must contain exactly ${payload.columns.length} cells`,
});
}
});
});
export type ArticleTableSectionPayload = z.infer<typeof ArticleTableSectionPayloadSchema>;
export const ArticleDownloadSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
description: LocalizedStringSchema.optional(),
files: z.array(z.object({
title: LocalizedStringSchema,
url: z.string().url().startsWith('https://'),
fileType: z.string().optional(),
fileSize: z.string().optional(),
}).strict()).min(1).max(10),
}).strict();
export type ArticleDownloadSectionPayload = z.infer<typeof ArticleDownloadSectionPayloadSchema>;
const ArticleLinkSchema = z.string().refine(
(value) => value.startsWith('/') || value.startsWith('https://'),
'Link must be a website-relative path or an HTTPS URL',
);
export const ArticleCtaSectionPayloadSchema = z.object({
title: LocalizedStringSchema,
description: LocalizedStringSchema.optional(),
primaryText: LocalizedStringSchema,
primaryLink: ArticleLinkSchema,
secondaryText: LocalizedStringSchema.optional(),
secondaryLink: ArticleLinkSchema.optional(),
alignment: z.enum(['left', 'center']).optional().default('center'),
}).strict().superRefine((payload, ctx) => {
if (payload.secondaryText && !payload.secondaryLink) {
ctx.addIssue({ code: 'custom', path: ['secondaryLink'], message: 'secondaryLink is required when secondaryText is provided' });
}
});
export type ArticleCtaSectionPayload = z.infer<typeof ArticleCtaSectionPayloadSchema>;
export const ScentNotesBreakdownSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
description: LocalizedStringSchema.optional(),
topNotes: z.union([LocalizedStringSchema, z.array(LocalizedStringSchema)]).optional(),
heartNotes: z.union([LocalizedStringSchema, z.array(LocalizedStringSchema)]).optional(),
middleNotes: z.union([LocalizedStringSchema, z.array(LocalizedStringSchema)]).optional(),
baseNotes: z.union([LocalizedStringSchema, z.array(LocalizedStringSchema)]).optional(),
});
export type ScentNotesBreakdownSectionPayload = z.infer<typeof ScentNotesBreakdownSectionPayloadSchema>;
export const QuoteCalloutSectionPayloadSchema = z.object({
quote: LocalizedStringSchema,
author: LocalizedStringSchema.optional(),
role: LocalizedStringSchema.optional(),
});
export type QuoteCalloutSectionPayload = z.infer<typeof QuoteCalloutSectionPayloadSchema>;
export const ProductCarouselSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
description: LocalizedStringSchema.optional(),
type: z.enum(['featured', 'brand', 'custom']),
categoryId: z.number().optional(),
brandId: z.number().optional(),
productIds: z.array(z.number()).optional(),
limit: z.number().positive().optional(),
showPrice: z.boolean().optional(),
showAddToCart: z.boolean().optional(),
products: z.array(z.any()).optional(),
}).strict();
export type ProductCarouselSectionPayload = z.infer<typeof ProductCarouselSectionPayloadSchema>;
export const ProductLayoutSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
subtitle: LocalizedStringSchema.optional(),
showHeading: z.boolean().optional(),
showViewAllLink: z.boolean().optional(),
layoutMode: z.enum(['standard', 'sticky_gallery']).optional(),
galleryBehavior: z.enum(['carousel', 'stacked']).optional(),
purchaseCardSticky: z.boolean().optional(),
showStickyPurchaseBarAfterMain: z.boolean().optional(),
showBreadcrumbs: z.boolean().optional(),
showBrandMeta: z.boolean().optional(),
}).strict();
export type ProductLayoutSectionPayload = z.infer<typeof ProductLayoutSectionPayloadSchema>;
// Aliases for backwards compatibility in ScentGuideSectionSchema
export const HeroArticlePayloadSchema = HeroArticleSectionPayloadSchema;
export type HeroArticlePayload = HeroArticleSectionPayload;
export const ScentNotesBreakdownPayloadSchema = ScentNotesBreakdownSectionPayloadSchema;
export type ScentNotesBreakdownPayload = ScentNotesBreakdownSectionPayload;
export const QuoteCalloutPayloadSchema = QuoteCalloutSectionPayloadSchema;
export type QuoteCalloutPayload = QuoteCalloutSectionPayload;
export const ProductCarouselPayloadSchema = ProductCarouselSectionPayloadSchema;
export type ProductCarouselPayload = ProductCarouselSectionPayload;
export const RichTextPayloadSchema = RichTextSectionPayloadSchema;
export type RichTextPayload = RichTextSectionPayload;
export const ScentGuideSectionSchema = z.discriminatedUnion("type", [
z.object({
id: z.string().or(z.number()),
type: z.literal("hero_article"),
order: z.number(),
payload: HeroArticlePayloadSchema,
}),
z.object({
id: z.string().or(z.number()),
type: z.literal("rich_text"),
order: z.number(),
payload: RichTextPayloadSchema,
}),
z.object({
id: z.string().or(z.number()),
type: z.literal("scent_notes_breakdown"),
order: z.number(),
payload: ScentNotesBreakdownPayloadSchema,
}),
z.object({
id: z.string().or(z.number()),
type: z.literal("quote_callout"),
order: z.number(),
payload: QuoteCalloutPayloadSchema,
}),
z.object({
id: z.string().or(z.number()),
type: z.literal("product_carousel"),
order: z.number(),
payload: ProductCarouselPayloadSchema,
}),
]);
export const ScentGuidePageSchema = z.object({
pageId: z.string().or(z.number()),
title: z.string(),
slug: z.string(),
status: z.string().optional(),
metadata: z.object({
seoTitle: z.string().optional(),
seoDescription: z.string().optional(),
}).optional(),
sections: z.array(ScentGuideSectionSchema),
});
export type ScentGuidePage = z.infer<typeof ScentGuidePageSchema>;
export const AccordionSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
backgroundColor: z.string().optional(),
textColor: z.string().optional(),
iconColor: z.string().optional(),
categories: z.array(
z.object({
id: z.string().optional(),
name: LocalizedStringSchema,
icon: z.string().optional(),
questions: z.array(
z.object({
id: z.string().optional(),
q: LocalizedStringSchema,
a: LocalizedStringSchema,
})
),
})
).optional(),
});
export type AccordionSectionPayload = z.infer<typeof AccordionSectionPayloadSchema>;
export const ContactInfoSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
address: LocalizedStringSchema,
email: z.string().min(1),
line: z.string().min(1),
hours: LocalizedStringSchema,
});
export type ContactInfoSectionPayload = z.infer<typeof ContactInfoSectionPayloadSchema>;
export const ContactFormSectionPayloadSchema = z.object({
title: LocalizedStringSchema.optional(),
subtitle: LocalizedStringSchema.optional(),
});
export type ContactFormSectionPayload = z.infer<typeof ContactFormSectionPayloadSchema>;
export const HeroV2SectionPayloadSchema = z.object({
tagline: LocalizedStringSchema.optional(),
title: LocalizedStringSchema,
description: LocalizedStringSchema.optional(),
imageUrl: z.string().min(1),
contentAlignment: z.enum(['left', 'center', 'right']).optional(),
showOrnament: z.boolean().optional(),
primaryButtonText: LocalizedStringSchema.optional(),
primaryButtonLink: z.string().optional(),
secondaryButtonText: LocalizedStringSchema.optional(),
secondaryButtonLink: z.string().optional(),
focalPoint: z.object({ x: z.string(), y: z.string() }).optional(),
});
export type HeroV2SectionPayload = z.infer<typeof HeroV2SectionPayloadSchema>;
export const UspBarSectionPayloadSchema = z.object({
backgroundColor: z.string().optional(),
items: z.array(z.object({
icon: z.string(),
title: LocalizedStringSchema,
subtitle: LocalizedStringSchema.optional(),
})).max(4).optional(),
});
export type UspBarSectionPayload = z.infer<typeof UspBarSectionPayloadSchema>;
export const CustomSectionPayloadSchema = z.object({
componentId: z.string().min(1),
props: z.record(z.string(), z.unknown()).optional().default({}),
}).strict();
export type CustomSectionPayload = z.infer<typeof CustomSectionPayloadSchema>;
export const HeroMinimalSectionPayloadSchema = z.object({
imageUrl: z.string().min(1),
tagline: LocalizedStringSchema.optional(),
title: LocalizedStringSchema,
focalPoint: z.object({ x: z.string(), y: z.string() }).optional(),
contentAlignment: z.enum(['left', 'center', 'right']).optional().default('center'),
overlayOpacity: z.number().optional().default(20),
});
export type HeroMinimalSectionPayload = z.infer<typeof HeroMinimalSectionPayloadSchema>;
export const HeroSplitMinimalSectionPayloadSchema = z.object({
imageUrl: z.string().min(1),
title: LocalizedStringSchema,
subtitle: LocalizedStringSchema.optional(),
imagePosition: z.enum(['left', 'right']).optional().default('right'),
focalPoint: z.object({ x: z.string(), y: z.string() }).optional(),
backgroundColor: z.string().optional(),
});
export type HeroSplitMinimalSectionPayload = z.infer<typeof HeroSplitMinimalSectionPayloadSchema>;
export const HeroVideoMinimalSectionPayloadSchema = z.object({
videoUrl: z.string().min(1),
posterImageUrl: z.string().optional(),
tagline: LocalizedStringSchema.optional(),
title: LocalizedStringSchema.optional(),
overlayOpacity: z.number().optional().default(25),
height: z.enum(['full', 'large', 'medium']).optional().default('large'),
focalPoint: z.object({ x: z.string(), y: z.string() }).optional(),
});
export type HeroVideoMinimalSectionPayload = z.infer<typeof HeroVideoMinimalSectionPayloadSchema>;
export const BannerMinimalSectionPayloadSchema = z.object({
title: LocalizedStringSchema,
subtitle: LocalizedStringSchema.optional(),
imageUrl: z.string().optional(),
linkUrl: z.string().optional(),
backgroundColor: z.string().optional(),
textColor: z.string().optional(),
focalPoint: z.object({ x: z.string(), y: z.string() }).optional(),
});
export type BannerMinimalSectionPayload = z.infer<typeof BannerMinimalSectionPayloadSchema>;
export const AnnouncementBarMinimalSectionPayloadSchema = z.object({
slides: z.array(LocalizedStringSchema).min(1),
backgroundColor: z.string().optional(),
textColor: z.string().optional(),
speed: z.number().optional().default(4000),
});
export type AnnouncementBarMinimalSectionPayload = z.infer<typeof AnnouncementBarMinimalSectionPayloadSchema>;
export const UspMinimalSectionPayloadSchema = z.object({
items: z.array(LocalizedStringSchema).min(1),
separator: z.enum(['dot', 'slash', 'line']).optional().default('dot'),
backgroundColor: z.string().optional(),
textColor: z.string().optional(),
});
export type UspMinimalSectionPayload = z.infer<typeof UspMinimalSectionPayloadSchema>;
export const PAGE_SECTION_TYPES = [
'HERO', 'HERO_V2', 'HERO_VIDEO', 'HERO_CAROUSEL', 'DIVIDER', 'ANNOUNCEMENT_BAR', 'NEWSLETTER', 'EDITORIAL_TILE', 'FEATURED_TILES',
'EDITORIAL_CAROUSEL', 'PRODUCTS', 'CATEGORIES', 'TEXT', 'BANNER',
'HERO_ARTICLE', 'RICH_TEXT', 'ARTICLE_HEADER', 'ARTICLE_MEDIA', 'ARTICLE_GALLERY',
'ARTICLE_FACTS', 'ARTICLE_VIDEO', 'ARTICLE_REFERENCES', 'ARTICLE_TABLE', 'ARTICLE_DOWNLOAD',
'ARTICLE_CTA', 'SCENT_NOTES_BREAKDOWN', 'QUOTE_CALLOUT',
'PRODUCT_CAROUSEL', 'ACCORDION', 'CONTACT_INFO', 'CONTACT_FORM',
'USP_BAR', 'PRODUCT_MAIN', 'PRODUCT_DETAILS', 'PRODUCT_SPECS',
'PRODUCT_REVIEWS', 'RELATED_PRODUCTS', 'CUSTOM',
'HERO_MINIMAL', 'HERO_SPLIT_MINIMAL', 'HERO_VIDEO_MINIMAL', 'BANNER_MINIMAL', 'ANNOUNCEMENT_BAR_MINIMAL', 'USP_MINIMAL'
] as const;
export type SectionType = typeof PAGE_SECTION_TYPES[number];
export const PAGE_SECTION_LOCALIZED_PATHS: Record<SectionType, readonly string[]> = {
HERO: ['tagline', 'title', 'description', 'buttonText'],
HERO_V2: ['tagline', 'title', 'description', 'primaryButtonText', 'secondaryButtonText'],
HERO_VIDEO: ['tagline', 'title', 'description', 'buttonText', 'secondaryButtonText'],
HERO_CAROUSEL: ['slides[].title', 'slides[].subtitle', 'slides[].buttonText', 'slides[].alt'],
DIVIDER: [],
ANNOUNCEMENT_BAR: ['slides[]'],
NEWSLETTER: ['title', 'description', 'buttonText', 'placeholderText', 'privacyText'],
EDITORIAL_TILE: ['title', 'description', 'buttonText'],
FEATURED_TILES: ['title', 'subtitle', 'tiles[].title', 'tiles[].description', 'tiles[].buttonText'],
EDITORIAL_CAROUSEL: ['title', 'slides[].category', 'slides[].title', 'slides[].description', 'slides[].buttonText'],
PRODUCTS: ['title', 'subtitle'],
CATEGORIES: ['title', 'subtitle'],
TEXT: ['title', 'content'],
BANNER: ['title', 'description', 'buttonText'],
HERO_ARTICLE: ['title', 'subtitle', 'author', 'category'],
RICH_TEXT: ['title', 'content'],
ARTICLE_HEADER: ['title', 'subtitle', 'category', 'author', 'imageAlt'],
ARTICLE_MEDIA: ['heading', 'body', 'imageAlt', 'imageCaption'],
ARTICLE_GALLERY: ['title', 'images[].alt', 'images[].caption'],
ARTICLE_FACTS: ['title', 'items[].title', 'items[].description'],
ARTICLE_VIDEO: ['title', 'caption'],
ARTICLE_REFERENCES: ['title', 'items[].title', 'items[].note'],
ARTICLE_TABLE: ['title', 'columns[]', 'rows[].label', 'rows[].cells[]', 'note'],
ARTICLE_DOWNLOAD: ['title', 'description', 'files[].title'],
ARTICLE_CTA: ['title', 'description', 'primaryText', 'secondaryText'],
SCENT_NOTES_BREAKDOWN: ['title', 'description', 'topNotes', 'heartNotes', 'middleNotes', 'baseNotes'],
QUOTE_CALLOUT: ['quote', 'author', 'role'],
PRODUCT_CAROUSEL: ['title', 'description'],
ACCORDION: ['title', 'categories[].name', 'categories[].questions[].q', 'categories[].questions[].a'],
CONTACT_INFO: ['title', 'address', 'hours'],
CONTACT_FORM: ['title', 'subtitle'],
USP_BAR: ['items[].title', 'items[].subtitle'],
PRODUCT_MAIN: ['title', 'subtitle'],
PRODUCT_DETAILS: ['title', 'subtitle'],
PRODUCT_SPECS: ['title', 'subtitle'],
PRODUCT_REVIEWS: ['title', 'subtitle'],
RELATED_PRODUCTS: ['title', 'subtitle'],
CUSTOM: [],
HERO_MINIMAL: ['tagline', 'title'],
HERO_SPLIT_MINIMAL: ['title', 'subtitle'],
HERO_VIDEO_MINIMAL: ['tagline', 'title'],
BANNER_MINIMAL: ['title', 'subtitle'],
ANNOUNCEMENT_BAR_MINIMAL: ['slides[]'],
USP_MINIMAL: ['items[]'],
};
export const PAGE_SECTION_PAYLOAD_SCHEMAS: Record<SectionType, z.ZodType> = {
HERO: HeroSectionPayloadSchema,
HERO_V2: HeroV2SectionPayloadSchema,
HERO_VIDEO: HeroVideoSectionPayloadSchema,
HERO_CAROUSEL: HeroCarouselSectionPayloadSchema,
DIVIDER: DividerSectionPayloadSchema,
ANNOUNCEMENT_BAR: AnnouncementBarSectionPayloadSchema,
NEWSLETTER: NewsletterSectionPayloadSchema,
EDITORIAL_TILE: EditorialTileSectionPayloadSchema,
FEATURED_TILES: FeaturedTilesSectionPayloadSchema,
EDITORIAL_CAROUSEL: EditorialCarouselSectionPayloadSchema,
PRODUCTS: ProductsSectionPayloadSchema,
CATEGORIES: CategoriesSectionPayloadSchema,
TEXT: TextSectionPayloadSchema,
BANNER: BannerSectionPayloadSchema,
HERO_ARTICLE: HeroArticleSectionPayloadSchema,
RICH_TEXT: RichTextSectionPayloadSchema,
ARTICLE_HEADER: ArticleHeaderSectionPayloadSchema,
ARTICLE_MEDIA: ArticleMediaSectionPayloadSchema,
ARTICLE_GALLERY: ArticleGallerySectionPayloadSchema,
ARTICLE_FACTS: ArticleFactsSectionPayloadSchema,
ARTICLE_VIDEO: ArticleVideoSectionPayloadSchema,
ARTICLE_REFERENCES: ArticleReferencesSectionPayloadSchema,
ARTICLE_TABLE: ArticleTableSectionPayloadSchema,
ARTICLE_DOWNLOAD: ArticleDownloadSectionPayloadSchema,
ARTICLE_CTA: ArticleCtaSectionPayloadSchema,
SCENT_NOTES_BREAKDOWN: ScentNotesBreakdownSectionPayloadSchema,
QUOTE_CALLOUT: QuoteCalloutSectionPayloadSchema,
PRODUCT_CAROUSEL: ProductCarouselSectionPayloadSchema,
ACCORDION: AccordionSectionPayloadSchema,
CONTACT_INFO: ContactInfoSectionPayloadSchema,
CONTACT_FORM: ContactFormSectionPayloadSchema,
USP_BAR: UspBarSectionPayloadSchema,
PRODUCT_MAIN: ProductLayoutSectionPayloadSchema,
PRODUCT_DETAILS: ProductLayoutSectionPayloadSchema,
PRODUCT_SPECS: ProductLayoutSectionPayloadSchema,
PRODUCT_REVIEWS: ProductLayoutSectionPayloadSchema,
RELATED_PRODUCTS: ProductLayoutSectionPayloadSchema,
CUSTOM: CustomSectionPayloadSchema,
HERO_MINIMAL: HeroMinimalSectionPayloadSchema,
HERO_SPLIT_MINIMAL: HeroSplitMinimalSectionPayloadSchema,
HERO_VIDEO_MINIMAL: HeroVideoMinimalSectionPayloadSchema,
BANNER_MINIMAL: BannerMinimalSectionPayloadSchema,
ANNOUNCEMENT_BAR_MINIMAL: AnnouncementBarMinimalSectionPayloadSchema,
USP_MINIMAL: UspMinimalSectionPayloadSchema,
};
export const PRODUCT_DETAIL_SECTION_TYPES = [
'PRODUCT_MAIN',
'HERO',
'HERO_V2',
'HERO_VIDEO',
'HERO_CAROUSEL',
'DIVIDER',
'PRODUCT_DETAILS',
'PRODUCT_SPECS',
'PRODUCT_REVIEWS',
'RELATED_PRODUCTS',
'RICH_TEXT',
'TEXT',
'BANNER',
'PRODUCT_CAROUSEL',
'EDITORIAL_TILE',
'FEATURED_TILES',
'QUOTE_CALLOUT',
'CUSTOM',
] as const;
export type ProductDetailSectionType = typeof PRODUCT_DETAIL_SECTION_TYPES[number];
export const CATEGORY_SECTION_TYPES = [
'CATEGORY_HEADER',
'CATALOG_GRID',
'USP_BAR',
'HERO',
'HERO_V2',
'HERO_VIDEO',
'HERO_CAROUSEL',
'DIVIDER',
'RICH_TEXT',
'TEXT',
'BANNER',
'PRODUCT_CAROUSEL',
'EDITORIAL_TILE',
'FEATURED_TILES',
'QUOTE_CALLOUT',
'CUSTOM',
] as const;
export type CategorySectionType = typeof CATEGORY_SECTION_TYPES[number];
export interface LlmPrompt {
id: number;
title: string;
category: 'navigation' | 'catalog' | 'theme' | 'copywriting' | 'cms' | 'translation' | 'custom';
description: string | null;
tags: string[];
content: string;
isSystem: boolean;
createdAt: string;
updatedAt: string;
}