import { z } from "zod"; export interface Brand { id: number name: string description?: string | null } export interface Category { id: number name: TranslatableString imageUrl?: string | null _count?: { products: number } translations?: { langCode: string; name: string }[] createdAt?: string updatedAt?: string } export interface Variant { id: number sku?: string | null barcode?: string | null price: number | null stock?: number option1: string option2: string option3: string 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 order: number } 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 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 = { '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 createdAt?: string updatedAt?: string } 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 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 translations?: StoreSettingsTranslation[] } 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 } 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 const LocalizedStringSchema = z.union([z.string(), z.record(z.string(), z.string())]); 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: z.enum([ 'top-left', 'top-center', 'top-right', 'center-left', 'center', 'center-right', 'bottom-left', 'bottom-center', 'bottom-right' ]).optional().default('center'), mobileButtonPosition: z.enum([ 'top-left', 'top-center', 'top-right', 'center-left', 'center', 'center-right', 'bottom-left', 'bottom-center', 'bottom-right' ]).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(), }); 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(), }); 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(['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), 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(['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(), }); 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(), }); 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']).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; 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; 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']), 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; 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; // 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(), }); 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), }); 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(), }); 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(), }); 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 PAGE_SECTION_TYPES = [ 'HERO', 'HERO_V2', 'HERO_VIDEO', 'HERO_CAROUSEL', '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 = { 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'], 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 = { HERO: HeroSectionPayloadSchema, HERO_V2: HeroV2SectionPayloadSchema, HERO_VIDEO: HeroVideoSectionPayloadSchema, HERO_CAROUSEL: HeroCarouselSectionPayloadSchema, 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', '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];