export interface Scent { id: number name: string description?: string | null } export interface Category { id: number name: string imageUrl?: string | null _count?: { products: number } createdAt?: string updatedAt?: string } export interface Variant { id: number size: string price: number | 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 order: number } export interface Product { id: number name: string description: string | null details: string | null price: number | null categoryId: number category: Category scent: Scent variants: Variant[] images: ProductImage[] } export const API_BASE = (typeof import.meta !== 'undefined' && (import.meta as any).env?.VITE_API_URL) || ((globalThis as any).process?.env?.VITE_API_URL) || 'http://localhost:3000/api' 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 shippingFee: number bankName: string bankAccountName: string bankAccountNumber: string pointsEarnRate: number } export interface OrderItem { id: number orderId: number productId: number variantId?: number | null quantity: number priceAtPurchase: number productName: string variantSize?: 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 variantSize?: 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 interface HeroSectionPayload { tagline: string title: string description: string imageUrl: string buttonText: string buttonLink: string } export interface CategoriesSectionPayload { title: string subtitle?: string categoryIds?: number[] } export interface ProductsSectionPayload { title: string subtitle?: string type: 'featured' | 'scent' | 'custom' scentId?: number productIds?: number[] limit?: number layout?: 'grid' | 'carousel' enableQuickShop?: boolean } export interface BannerSectionPayload { imageUrl: string linkUrl?: string title?: string description?: string buttonText?: string } export interface TextSectionPayload { title?: string content: string textAlign?: 'left' | 'center' | 'right' } export interface AnnouncementBarSectionPayload { slides: string[] backgroundColor?: string textColor?: string speed?: number } export interface NewsletterSectionPayload { title: string description: string buttonText: string placeholderText?: string showPrivacyCheckbox: boolean privacyText?: string; } export interface EditorialTileSectionPayload { title: string description: string imageUrl: string imagePosition: 'left' | 'right' buttonText?: string buttonLink?: string backgroundColor?: string textColor?: string } export interface EditorialCarouselSectionPayload { title: string slides: { category: string title: string description: string imageUrl: string buttonText?: string buttonLink?: string }[] } 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 } import { z } from "zod"; export const HeroArticlePayloadSchema = z.object({ title: z.string().min(1, "Title is required"), subtitle: z.string().optional(), author: z.string().optional(), publishDate: z.string().optional(), // Or z.date() if preferred, but string works for ISO dates backgroundImageUrl: z.string().url("Must be a valid URL").optional(), overlayColor: z.string().optional(), }); export type HeroArticlePayload = z.infer; export const RichTextPayloadSchema = z.object({ content: z.string().min(1, "Content is required"), }); export type RichTextPayload = z.infer; export const ScentNotesBreakdownPayloadSchema = z.object({ title: z.string().optional(), topNotes: z.array(z.string()), middleNotes: z.array(z.string()), baseNotes: z.array(z.string()), }); export type ScentNotesBreakdownPayload = z.infer; export const QuoteCalloutPayloadSchema = z.object({ quote: z.string().min(1, "Quote is required"), author: z.string().optional(), }); export type QuoteCalloutPayload = z.infer; export const ProductCarouselPayloadSchema = z.object({ title: z.string().optional(), collectionId: z.string().optional(), limit: z.number().int().positive().optional(), showPrice: z.boolean().optional(), showAddToCart: z.boolean().optional(), }); export type ProductCarouselPayload = z.infer; 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;