Files
homaroma/packages/types/index.ts
T
2026-06-14 19:51:19 -04:00

424 lines
9.8 KiB
TypeScript

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[]
}
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>;
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>;