1293 lines
44 KiB
TypeScript
1293 lines
44 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
|
|
_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 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(),
|
|
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(),
|
|
});
|
|
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(['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),
|
|
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<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(),
|
|
});
|
|
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(),
|
|
});
|
|
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];
|