446 lines
12 KiB
TypeScript
446 lines
12 KiB
TypeScript
import { z } from "zod";
|
|
|
|
export interface Scent {
|
|
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
|
|
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 type TranslatableString = string | Record<string, string>;
|
|
|
|
export interface Product {
|
|
id: number
|
|
name: TranslatableString
|
|
description: TranslatableString | null
|
|
details: TranslatableString | 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<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 StoreSettings {
|
|
id: number
|
|
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
|
|
}
|
|
|
|
export interface MenuItem {
|
|
id: number
|
|
label: string
|
|
url: string
|
|
order: number
|
|
translations?: any[]
|
|
}
|
|
|
|
export interface Menu {
|
|
id: number
|
|
name: string
|
|
items: MenuItem[]
|
|
payload?: any
|
|
}
|
|
|
|
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 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(),
|
|
});
|
|
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', 'scent', 'custom']),
|
|
scentId: 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(),
|
|
});
|
|
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: z.string().optional(),
|
|
showPrivacyCheckbox: z.boolean(),
|
|
privacyText: z.string().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']),
|
|
buttonText: z.string().optional(),
|
|
buttonLink: z.string().optional(),
|
|
backgroundColor: z.string().optional(),
|
|
textColor: z.string().optional(),
|
|
});
|
|
export type EditorialTileSectionPayload = z.infer<typeof EditorialTileSectionPayloadSchema>;
|
|
|
|
export const EditorialCarouselSectionPayloadSchema = z.object({
|
|
title: LocalizedStringSchema,
|
|
slides: z.array(z.object({
|
|
category: LocalizedStringSchema,
|
|
title: LocalizedStringSchema,
|
|
description: LocalizedStringSchema,
|
|
imageUrl: z.string().min(1),
|
|
buttonText: z.string().optional(),
|
|
buttonLink: z.string().optional(),
|
|
})).min(1)
|
|
});
|
|
export type EditorialCarouselSectionPayload = z.infer<typeof EditorialCarouselSectionPayloadSchema>;
|
|
|
|
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 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<typeof HeroArticlePayloadSchema>;
|
|
|
|
export const RichTextPayloadSchema = z.object({
|
|
content: z.string().min(1, "Content is required"),
|
|
});
|
|
export type RichTextPayload = z.infer<typeof RichTextPayloadSchema>;
|
|
|
|
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<typeof ScentNotesBreakdownPayloadSchema>;
|
|
|
|
export const QuoteCalloutPayloadSchema = z.object({
|
|
quote: z.string().min(1, "Quote is required"),
|
|
author: z.string().optional(),
|
|
});
|
|
export type QuoteCalloutPayload = z.infer<typeof QuoteCalloutPayloadSchema>;
|
|
|
|
export const ProductCarouselPayloadSchema = z.object({
|
|
title: z.string().optional(),
|
|
description: z.string().optional(),
|
|
collectionId: z.string().optional(),
|
|
limit: z.number().int().positive().optional(),
|
|
showPrice: z.boolean().optional(),
|
|
showAddToCart: z.boolean().optional(),
|
|
products: z.array(z.any()).optional(),
|
|
});
|
|
export type ProductCarouselPayload = z.infer<typeof ProductCarouselPayloadSchema>;
|
|
|
|
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({
|
|
categories: z.array(
|
|
z.object({
|
|
name: z.string().min(1, "Category name is required"),
|
|
icon: z.string().optional(),
|
|
questions: z.array(
|
|
z.object({
|
|
q: z.string().min(1, "Question is required"),
|
|
a: z.string().min(1, "Answer is required"),
|
|
})
|
|
),
|
|
})
|
|
),
|
|
});
|
|
export type AccordionSectionPayload = z.infer<typeof AccordionSectionPayloadSchema>;
|
|
|
|
export const ContactInfoSectionPayloadSchema = z.object({
|
|
address: z.string().min(1, "Address is required"),
|
|
email: z.string().min(1, "Email is required"),
|
|
line: z.string().min(1, "Line ID is required"),
|
|
hours: z.string().min(1, "Business hours are required"),
|
|
});
|
|
export type ContactInfoSectionPayload = z.infer<typeof ContactInfoSectionPayloadSchema>;
|
|
|
|
export const ContactFormSectionPayloadSchema = z.object({
|
|
title: z.string().optional(),
|
|
subtitle: z.string().optional(),
|
|
});
|
|
export type ContactFormSectionPayload = z.infer<typeof ContactFormSectionPayloadSchema>;
|
|
|
|
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'
|
|
] as const;
|
|
export type SectionType = typeof PAGE_SECTION_TYPES[number];
|