Full website

This commit is contained in:
Your Name committed 2026-06-17 07:42:59 -04:00
1 parent 807c43ece5
commit ea9f33f60e
40 files changed
+4503 -1202

No files matched your search

+21
View File
@@ -0,0 +1,21 @@
import type { PageSection } from '@homaroma/types'
const announcementTypes = new Set(['ANNOUNCEMENT_BAR'])
const heroTypes = new Set(['HERO', 'BANNER'])
function sectionType(section: PageSection) {
return section.type.toUpperCase()
}
export function splitCmsLayoutSections(sections: PageSection[]) {
const activeSections = sections.filter((section) => section.isActive)
return {
announcementSections: activeSections.filter((section) => announcementTypes.has(sectionType(section))),
heroSections: activeSections.filter((section) => heroTypes.has(sectionType(section))),
bottomSections: activeSections.filter((section) => {
const type = sectionType(section)
return !announcementTypes.has(type) && !heroTypes.has(type)
}),
}
}
+123
View File
@@ -0,0 +1,123 @@
import type { Article, Category, Order, Page, Product } from '@homaroma/types'
import { API_BASE } from './api'
const defaultRevalidate = 60
type ProductListResponse = {
data?: Product[]
}
export type Coupon = {
id: number
code: string
type: 'PERCENTAGE' | 'FIXED_AMOUNT' | 'FREE_SHIPPING' | string
value: number
minPurchase: number
usageLimit: number | null
usedCount: number
expiresAt?: string | null
}
export type ApiResult<T> =
| { ok: true; data: T }
| { ok: false; error: string; status?: number }
export async function fetchJson<T>(path: string, init?: RequestInit & { next?: { revalidate?: number } }): Promise<T | null> {
try {
const res = await fetch(`${API_BASE}${path}`, {
...init,
next: init?.next ?? { revalidate: defaultRevalidate },
})
if (!res.ok) {
console.error(`[website-api] ${path} returned ${res.status}`)
return null
}
return await res.json() as T
} catch (error) {
console.error(`[website-api] Failed to fetch ${path}`, error)
return null
}
}
export async function fetchJsonResult<T>(path: string, init?: RequestInit & { next?: { revalidate?: number } }): Promise<ApiResult<T>> {
try {
const res = await fetch(`${API_BASE}${path}`, {
...init,
next: init?.next ?? { revalidate: defaultRevalidate },
})
if (!res.ok) {
return { ok: false, error: `${path} returned ${res.status}`, status: res.status }
}
return { ok: true, data: await res.json() as T }
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : `Failed to fetch ${path}`,
}
}
}
export async function getCategories(lang = 'th'): Promise<Category[]> {
return await fetchJson<Category[]>(`/categories?lang=${lang}`, { next: { revalidate: 3600 } }) ?? []
}
export async function getCategoriesResult(lang = 'th'): Promise<ApiResult<Category[]>> {
return await fetchJsonResult<Category[]>(`/categories?lang=${lang}`, { next: { revalidate: 3600 } })
}
export async function getCmsPage(slug: string, lang = 'th'): Promise<Page | null> {
return await fetchJson<Page>(`/pages/${slug}?lang=${lang}`, { next: { revalidate: defaultRevalidate } })
}
export async function getCmsPageResult(slug: string, lang = 'th'): Promise<ApiResult<Page>> {
return await fetchJsonResult<Page>(`/pages/${slug}?lang=${lang}`, { next: { revalidate: defaultRevalidate } })
}
export async function getArticlesResult(): Promise<ApiResult<Article[]>> {
return await fetchJsonResult<Article[]>('/articles?publishedOnly=true', { next: { revalidate: defaultRevalidate } })
}
export async function getActiveCouponsResult(): Promise<ApiResult<Coupon[]>> {
const response = await fetchJsonResult<Coupon[]>('/coupons/active', { next: { revalidate: defaultRevalidate } })
if (!response.ok) return response
return {
ok: true,
data: response.data.filter((coupon) => coupon.usageLimit === null || coupon.usedCount < coupon.usageLimit),
}
}
export async function getOrderResult(id: string | number): Promise<ApiResult<Order>> {
return await fetchJsonResult<Order>(`/orders/${id}`, { next: { revalidate: defaultRevalidate } })
}
export async function getProduct(id: string | number, lang = 'th'): Promise<Product | null> {
return await fetchJson<Product>(`/products/${id}?lang=${lang}`, { next: { revalidate: defaultRevalidate } })
}
export async function getProductResult(id: string | number, lang = 'th'): Promise<ApiResult<Product>> {
return await fetchJsonResult<Product>(`/products/${id}?lang=${lang}`, { next: { revalidate: defaultRevalidate } })
}
export async function getProducts(query = '', lang = 'th'): Promise<Product[]> {
const separator = query ? '&' : ''
const response = await fetchJson<ProductListResponse>(`/products?${query}${separator}lang=${lang}`, {
next: { revalidate: defaultRevalidate },
})
return response?.data ?? []
}
export async function getProductsResult(query = '', lang = 'th'): Promise<ApiResult<Product[]>> {
const separator = query ? '&' : ''
const response = await fetchJsonResult<ProductListResponse>(`/products?${query}${separator}lang=${lang}`, {
next: { revalidate: defaultRevalidate },
})
if (!response.ok) return response
return { ok: true, data: response.data.data ?? [] }
}
+16
View File
@@ -0,0 +1,16 @@
import type { TranslatableString } from '@homaroma/types'
export function resolveLocalized(value: TranslatableString | null | undefined, lang = 'th'): string {
if (!value) return ''
if (typeof value === 'string') return value
return value[lang] ?? value[lang.slice(0, 2)] ?? value.th ?? value.en ?? Object.values(value)[0] ?? ''
}
export function toRecord(value: unknown): Record<string, unknown> {
return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {}
}
export function localizedFromRecord(payload: Record<string, unknown>, key: string, lang = 'th'): string {
return resolveLocalized(payload[key] as TranslatableString | null | undefined, lang)
}