Full website
This commit is contained in:
1 parent
807c43ece5
commit
ea9f33f60e
40 files changed
+4503
-1202
No files matched your search
@@ -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)
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -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 ?? [] }
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user