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[] } 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 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 } 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 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 Page { id: number slug: string title: string isPublished: boolean sections?: PageSection[] 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 const LocalizedStringSchema = z.union([z.string(), z.record(z.string(), z.string())]); export const HeroSectionPayloadSchema = z.object({ tagline: LocalizedStringSchema, title: LocalizedStringSchema, description: LocalizedStringSchema, imageUrl: z.string().min(1), buttonText: LocalizedStringSchema, buttonLink: z.string(), focalPoint: z.object({ x: z.string(), y: z.string() }).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(), }); 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']), buttonText: LocalizedStringSchema.optional(), buttonLink: z.string().optional(), backgroundColor: z.string().optional(), textColor: z.string().optional(), }); export type EditorialTileSectionPayload = 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 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 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; // 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(), }); 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 PAGE_SECTION_TYPES = [ 'HERO', 'ANNOUNCEMENT_BAR', 'NEWSLETTER', 'EDITORIAL_TILE', 'EDITORIAL_CAROUSEL', 'PRODUCTS', 'CATEGORIES', 'TEXT', 'BANNER', 'HERO_ARTICLE', 'RICH_TEXT', 'SCENT_NOTES_BREAKDOWN', 'QUOTE_CALLOUT', 'PRODUCT_CAROUSEL', 'ACCORDION', 'CONTACT_INFO', 'CONTACT_FORM', 'HERO_V2', 'USP_BAR' ] as const; export type SectionType = typeof PAGE_SECTION_TYPES[number];