386 lines
8.6 KiB
TypeScript
386 lines
8.6 KiB
TypeScript
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<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
|
|
}
|
|
|
|
export interface MenuItem {
|
|
id: number
|
|
label: string
|
|
url: string
|
|
order: number
|
|
}
|
|
|
|
export interface Menu {
|
|
id: number
|
|
name: string
|
|
items: MenuItem[]
|
|
}
|
|
|
|
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<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(),
|
|
collectionId: z.string().optional(),
|
|
limit: z.number().int().positive().optional(),
|
|
showPrice: z.boolean().optional(),
|
|
showAddToCart: z.boolean().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>;
|