import { z } from "zod"; export interface BrandTranslation { id?: number brandId?: number langCode: string name: string } export interface Brand { id: number name: string description?: string | null translations?: BrandTranslation[] _count?: { products?: number } } 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[] fullWidth?: boolean } export interface CategoryUspBenefit { icon?: string title: string description?: string } export interface CategoryFilterConfig { showCategoryNav?: boolean // Toggle Category Pills Bar showFilters?: boolean showToolbar?: boolean showSeries?: boolean showType?: boolean showColor?: boolean showFitment?: boolean showPrice?: boolean showStock?: boolean showAddToCart?: boolean fullWidth?: boolean containerWidth?: 'standard' | 'wide' | 'full' gridColumns?: 2 | 3 | 4 | 5 cardStyle?: 'standard' | 'minimal' | 'swatch' | 'minimal_swatch' cardSize?: 'compact' | 'medium' | 'large' filterPosition?: 'sidebar' | 'top_bar' } 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 = { 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 | 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 | 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 colorCode?: 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 export type NormalizedProductImageInput = z.output export type TranslatableString = string | Record; 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 | null valueTranslations?: Record 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 brandId?: number 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 = { '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 export type FooterFinaleSettings = z.infer export type FooterLayoutSettings = z.infer export type FooterLayoutSettingsPatch = z.infer 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 | 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 headerShowSearch?: boolean | null headerShowWishlist?: boolean | null headerShowCart?: boolean | null headerShowTrackOrder?: boolean | null headerShowLanguage?: boolean | null headerShowAccount?: boolean | null cardPreset?: 'standard' | 'minimal' | 'swatch' | 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 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 translations?: { name?: Record variantName?: Record } | 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; 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; 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; export const DefaultTextColorSchemeSchema = (fallback: TextColorScheme = 'light') => z .union([TextColorSchemeSchema, z.literal(''), z.null()]) .optional() .transform((val) => (val ? (val as TextColorScheme) : fallback)); export const TitleSizeSchema = z.enum(['sm', 'md', 'lg', 'xl']); export type TitleSize = z.infer; export const DefaultTitleSizeSchema = (fallback: TitleSize = 'xl') => z .union([TitleSizeSchema, z.literal(''), z.null()]) .optional() .transform((val) => (val ? (val as TitleSize) : fallback)); export const SectionPaddingLevelSchema = z.enum(['none', 'compact', 'normal', 'spacious']); export type SectionPaddingLevel = z.infer; export const SectionSpacingPayloadSchema = z.object({ paddingTop: SectionPaddingLevelSchema.optional(), paddingBottom: SectionPaddingLevelSchema.optional(), sectionPadding: SectionPaddingLevelSchema.optional(), }); export type SectionSpacingPayload = z.infer; export const HeroSectionPayloadSchema = z.object({ tagline: LocalizedStringSchema.optional(), title: LocalizedStringSchema.optional(), description: LocalizedStringSchema.optional(), imageUrl: z.string().min(1), mobileImageUrl: z.string().optional(), 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'), titleSize: DefaultTitleSizeSchema('xl'), overlayStyle: z.enum(['gradient', 'solid', 'none']).optional().default('gradient'), overlayOpacity: z.number().min(0).max(100).optional(), aspectRatio: z.enum(['21/6', '21/7', '21/9', '16/5', '16/6', '4/1', '5/1', 'auto', 'full_screen']).optional().default('21/7'), mobileAspectRatio: z.enum(['16/9', '4/3', '1/1', '21/9', 'auto', 'full_screen']).optional().default('16/9'), headerSpacing: z.enum(['below_header', 'under_header']).optional(), }); export type HeroSectionPayload = z.infer; export const CategoriesSectionPayloadSchema = z.object({ title: LocalizedStringSchema, subtitle: LocalizedStringSchema.optional(), categoryIds: z.array(z.number()).optional(), }); export type CategoriesSectionPayload = z.infer; 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(), containerWidth: z.enum(['standard', 'wide', 'full']).optional().default('standard'), cardSize: z.enum(['compact', 'medium', 'large']).optional().default('medium'), }); export type ProductsSectionPayload = z.infer; 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'), titleSize: DefaultTitleSizeSchema('xl'), }); export type BannerSectionPayload = z.infer; export const TextSectionPayloadSchema = z.object({ title: LocalizedStringSchema.optional(), content: LocalizedStringSchema, textAlign: z.enum(['left', 'center', 'right']).optional(), }); export type TextSectionPayload = z.infer; 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; 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; 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; export const FeaturedTilesSectionPayloadSchema = z.object({ title: LocalizedStringSchema.optional(), subtitle: LocalizedStringSchema.optional(), columns: z.number().optional().default(2), layoutPreset: z.enum(['editorial', 'spotlight', 'custom']).optional(), headerAlign: z.enum(['left', 'center', 'right', 'none']).optional().default('center'), showHeading: z.boolean().optional().default(true), 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(), containerWidth: z.enum(['standard', 'wide', 'full']).optional().default('standard'), paddingTop: z.enum(['none', 'compact', 'normal', 'spacious']).optional(), paddingBottom: z.enum(['none', 'compact', 'normal', 'spacious']).optional(), sectionPadding: z.enum(['none', 'compact', 'normal', 'spacious']).optional().default('normal'), 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; 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'), titleSize: DefaultTitleSizeSchema('xl'), }); export type HeroVideoSectionPayload = z.infer; 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; 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'), titleSize: DefaultTitleSizeSchema('xl'), }); export type HeroCarouselSlide = z.infer; 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', 'full_screen']).optional().default('21/7'), mobileAspectRatio: z.enum(['16/9', '4/3', '1/1', '21/9', 'auto', 'full_screen']).optional().default('16/9'), showArrows: z.boolean().optional().default(true), showDots: z.boolean().optional().default(true), backgroundColor: z.string().optional(), headerSpacing: z.enum(['below_header', 'under_header']).optional(), }); export type HeroCarouselSectionPayload = z.infer; 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; 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(), titleSize: DefaultTitleSizeSchema('xl'), }); export type HeroArticleSectionPayload = z.infer; export const RichTextSectionPayloadSchema = z.object({ title: LocalizedStringSchema.optional(), content: LocalizedStringSchema, }); export type RichTextSectionPayload = z.infer; 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; 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; 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; 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; 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; 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; 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; 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; 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; 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; export const QuoteCalloutSectionPayloadSchema = z.object({ quote: LocalizedStringSchema, author: LocalizedStringSchema.optional(), role: LocalizedStringSchema.optional(), }); export type QuoteCalloutSectionPayload = z.infer; export const ProductCarouselSectionPayloadSchema = z.object({ title: LocalizedStringSchema.optional(), description: LocalizedStringSchema.optional(), type: z.enum(['featured', 'brand', 'custom']).optional().default('featured'), 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(), cardStyle: z.enum(['standard', 'minimal', 'minimal_swatch', 'swatch']).optional().default('standard'), containerWidth: z.enum(['standard', 'wide', 'full']).optional().default('standard'), cardSize: z.enum(['compact', 'medium', 'large']).optional().default('medium'), layout: z.enum(['carousel', 'grid']).optional().default('carousel'), viewAllText: LocalizedStringSchema.optional(), viewAllLink: z.string().optional(), badgeText: LocalizedStringSchema.optional(), collectionId: z.any().optional(), }).strict(); export type ProductCarouselSectionPayload = z.infer; export const StorySceneSchema = z.object({ id: z.string(), kicker: z.string().optional(), title: z.string().optional(), description: z.string().optional(), metric: z.string().optional(), chips: z.array(z.string()).optional(), theme: z.string().optional(), imageUrl: z.string().optional(), }).passthrough(); export type StoryScene = z.infer; 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', 'story_scroll', 'apple_watch_tour', 'apple_keynote']).optional(), galleryBehavior: z.enum(['carousel', 'stacked']).optional(), purchaseCardSticky: z.boolean().optional(), showStickyPurchaseBarAfterMain: z.boolean().optional(), showBreadcrumbs: z.boolean().optional(), showBrandMeta: z.boolean().optional(), storyHeroImage: z.string().optional(), storyTheme: z.enum(['dark', 'light']).optional(), storyScenes: z.array(StorySceneSchema).optional(), endCtaTitle: z.string().optional(), endCtaButtonText: z.string().optional(), }).strict(); export type ProductLayoutSectionPayload = z.infer; // 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; 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; 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; export const ContactFormSectionPayloadSchema = z.object({ title: LocalizedStringSchema.optional(), subtitle: LocalizedStringSchema.optional(), }); export type ContactFormSectionPayload = z.infer; 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(), titleSize: DefaultTitleSizeSchema('lg'), }); export type HeroV2SectionPayload = z.infer; 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; 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; 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), titleSize: DefaultTitleSizeSchema('lg'), }); export type HeroMinimalSectionPayload = z.infer; 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(), titleSize: DefaultTitleSizeSchema('lg'), }); export type HeroSplitMinimalSectionPayload = z.infer; 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(), titleSize: DefaultTitleSizeSchema('lg'), }); export type HeroVideoMinimalSectionPayload = z.infer; 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; 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; 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; export const CinematicStoryScrollSectionPayloadSchema = z.object({ productId: z.union([z.number(), z.string(), z.null()]).optional(), storyHeroImage: z.string().optional(), storyTheme: z.enum(['dark', 'light']).optional().default('light'), storyScenes: z.array(StorySceneSchema).optional(), endCtaTitle: z.string().optional(), endCtaButtonText: z.string().optional(), endCtaLink: z.string().optional(), paddingTop: z.string().optional(), paddingBottom: z.string().optional(), }).passthrough(); export type CinematicStoryScrollSectionPayload = z.infer; export const VdoFrameScrubberSectionPayloadSchema = z.object({ folderPath: z.string().optional().default('/frame-sequences/chrome-sprocket-428h'), pattern: z.string().optional().default('frame-{index}.webp'), padding: z.number().optional().default(3), totalFrames: z.number().optional().default(432), fps: z.number().optional().default(24), durationSeconds: z.number().optional().default(18), eyebrow: z.string().optional(), title: z.string().optional(), subtitle: z.string().optional(), instruction: z.string().optional(), productName: z.string().optional(), productBadge: z.string().optional(), endCtaTitle: z.string().optional(), endCtaButtonText: z.string().optional(), endCtaLink: z.string().optional(), theme: z.enum(['dark', 'light']).optional().default('dark'), accentColor: z.string().optional().default('#d5ff63'), productId: z.union([z.number(), z.string(), z.null()]).optional(), paddingTop: z.string().optional(), paddingBottom: z.string().optional(), }).passthrough(); export type VdoFrameScrubberSectionPayload = z.infer; export const Interactive360ShowcaseSectionPayloadSchema = z.object({ spriteSheetUrl: z.string().optional().default('/images/3d/sprocket-views.png'), detailImageUrl: z.string().optional().default('/images/3d/sprocket-detail.png'), columns: z.number().optional().default(4), rows: z.number().optional().default(4), eyebrow: z.string().optional(), title: z.string().optional(), subtitle: z.string().optional(), detailKicker: z.string().optional(), detailTitle: z.string().optional(), detailCaption: z.string().optional(), productName: z.string().optional(), productBadge: z.string().optional(), specs: z.array(z.object({ number: z.string().optional(), title: z.string(), subtitle: z.string().optional(), })).optional(), endCtaTitle: z.string().optional(), endCtaButtonText: z.string().optional(), endCtaLink: z.string().optional(), theme: z.enum(['dark', 'light']).optional().default('dark'), accentColor: z.string().optional().default('#d5ff63'), productId: z.union([z.number(), z.string(), z.null()]).optional(), paddingTop: z.string().optional(), paddingBottom: z.string().optional(), }).passthrough(); export type Interactive360ShowcaseSectionPayload = z.infer; export const AppleShowcaseSectionPayloadSchema = z.object({ productId: z.union([z.number(), z.string(), z.null()]).optional(), heroImage: z.string().optional(), storyHeroImage: z.string().optional(), storyTheme: z.enum(['dark', 'light']).optional().default('dark'), endCtaTitle: z.string().optional(), endCtaButtonText: z.string().optional(), endCtaLink: z.string().optional(), paddingTop: z.string().optional(), paddingBottom: z.string().optional(), }).passthrough(); export type AppleShowcaseSectionPayload = z.infer; 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', 'CINEMATIC_STORY_SCROLL', 'APPLE_SHOWCASE', 'INTERACTIVE_360_SHOWCASE', 'VDO_FRAME_SCRUBBER' ] as const; export type SectionType = typeof PAGE_SECTION_TYPES[number]; export const PAGE_SECTION_LOCALIZED_PATHS: Record = { 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', 'viewAllText', 'badgeText'], 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[]'], CINEMATIC_STORY_SCROLL: ['endCtaTitle', 'endCtaButtonText'], APPLE_SHOWCASE: ['endCtaTitle', 'endCtaButtonText'], INTERACTIVE_360_SHOWCASE: ['eyebrow', 'title', 'subtitle', 'detailKicker', 'detailTitle', 'detailCaption', 'productName', 'endCtaTitle', 'endCtaButtonText'], VDO_FRAME_SCRUBBER: ['eyebrow', 'title', 'subtitle', 'instruction', 'productName', 'endCtaTitle', 'endCtaButtonText'], }; export const PAGE_SECTION_PAYLOAD_SCHEMAS: Record = { 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, CINEMATIC_STORY_SCROLL: CinematicStoryScrollSectionPayloadSchema, APPLE_SHOWCASE: AppleShowcaseSectionPayloadSchema, INTERACTIVE_360_SHOWCASE: Interactive360ShowcaseSectionPayloadSchema, VDO_FRAME_SCRUBBER: VdoFrameScrubberSectionPayloadSchema, }; 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; }