x
This commit is contained in:
1 parent
b7f337fb79
commit
bf9599008e
29 files changed
+3828
-1541
No files matched your search
@@ -3,7 +3,7 @@ import Image from 'next/image'
|
||||
import { notFound } from 'next/navigation'
|
||||
import type { Metadata } from 'next'
|
||||
import { generateSeoMetadata } from '@/lib/seo'
|
||||
import type { Category, Product, AttributeDefinition, CategorySection, PageSection } from '@homaroma/types'
|
||||
import type { Category, Product, AttributeDefinition, CategorySection, PageSection, CategoryFilterConfig } from '@homaroma/types'
|
||||
import { getCategoryImage, getCategoryMedia, resolveCategoryMediaUrl } from '@homaroma/types'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import ProductCard from '@/components/ProductCard'
|
||||
@@ -14,6 +14,9 @@ import { type ApiResult, fetchJsonResult, getCategoriesResult, getCmsPageResult,
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
import { resolveLocalized } from '@/lib/localization'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
export const revalidate = 0
|
||||
|
||||
function toPageSection(section: CategorySection): PageSection {
|
||||
return {
|
||||
id: section.id,
|
||||
@@ -126,22 +129,65 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
|
||||
const hasArtDirectedHero = Boolean(category.media?.some((item) => item.role === 'hero_desktop' || item.role === 'hero_mobile'))
|
||||
const layoutType = category.layoutType || 'split_showcase'
|
||||
|
||||
// Determine Product Grid Class based on layout archetype
|
||||
const getProductGridClass = () => {
|
||||
// Determine Product Grid Class based on layout archetype, cardSize, and width mode
|
||||
const getProductGridClass = (config?: CategoryFilterConfig, isFullWidth?: boolean) => {
|
||||
// 1. Explicit grid columns on section block
|
||||
const explicitCols = config?.gridColumns
|
||||
if (explicitCols === 2) {
|
||||
return 'grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8'
|
||||
}
|
||||
if (explicitCols === 3) {
|
||||
return 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8'
|
||||
}
|
||||
if (explicitCols === 4) {
|
||||
return 'grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-5 md:gap-6'
|
||||
}
|
||||
if (explicitCols === 5) {
|
||||
return 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 gap-4 md:gap-5'
|
||||
}
|
||||
|
||||
// 2. Card Size / Density setting on section block (large, compact, medium)
|
||||
const cardSize = config?.cardSize ?? category.filterConfig?.cardSize
|
||||
if (cardSize === 'compact') {
|
||||
return isFullWidth
|
||||
? 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-3 md:gap-4'
|
||||
: 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3 md:gap-4'
|
||||
}
|
||||
if (cardSize === 'large') {
|
||||
return 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6 md:gap-8'
|
||||
}
|
||||
|
||||
// 3. Fallback to category-level grid columns
|
||||
const categoryCols = category.filterConfig?.gridColumns
|
||||
if (categoryCols === 2) return 'grid grid-cols-1 md:grid-cols-2 gap-6 md:gap-8'
|
||||
if (categoryCols === 3) return 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8'
|
||||
if (categoryCols === 4) return 'grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-5 md:gap-6'
|
||||
if (categoryCols === 5) return 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 gap-4 md:gap-5'
|
||||
|
||||
switch (layoutType) {
|
||||
case 'editorial_hero':
|
||||
return 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-10'
|
||||
return isFullWidth
|
||||
? 'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-4 gap-6 md:gap-8'
|
||||
: 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8 md:gap-10'
|
||||
case 'dense_catalog':
|
||||
return 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 md:gap-6'
|
||||
return isFullWidth
|
||||
? 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6 gap-4 md:gap-5'
|
||||
: 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4 md:gap-6'
|
||||
case 'split_showcase':
|
||||
default:
|
||||
return 'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 md:gap-8'
|
||||
return isFullWidth
|
||||
? 'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6 md:gap-6'
|
||||
: 'grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 md:gap-8'
|
||||
}
|
||||
}
|
||||
|
||||
const renderHeaderSection = (payload?: Record<string, unknown>) => {
|
||||
const showPills = payload?.showCategoryNav !== false && category.filterConfig?.showCategoryNav !== false
|
||||
const showBreadcrumb = payload?.showBreadcrumbs !== false
|
||||
const isFullWidth = Boolean(category.filterConfig?.fullWidth || category.featuredSeries?.fullWidth)
|
||||
const headerContainerClass = isFullWidth
|
||||
? 'mx-auto w-full max-w-[1920px] px-4 sm:px-6 lg:px-8'
|
||||
: 'mx-auto max-w-7xl px-6'
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -201,7 +247,7 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
|
||||
) : layoutType === 'dense_catalog' ? (
|
||||
/* ── Archetype 3: Dense Catalog Layout ── */
|
||||
<section className="relative overflow-hidden bg-surface-card border-b border-line-primary/10 pt-28 pb-4 md:pt-32 md:pb-5">
|
||||
<div className="mx-auto max-w-7xl px-6 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div className={`${headerContainerClass} flex flex-col md:flex-row md:items-center justify-between gap-4`}>
|
||||
<div className="space-y-1.5">
|
||||
{showBreadcrumb && (
|
||||
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-muted-text">
|
||||
@@ -229,7 +275,7 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
|
||||
) : (
|
||||
/* ── Archetype 1: Split Showcase Layout (Default) ── */
|
||||
<section className="relative overflow-hidden bg-surface-card border-b border-line-primary/10">
|
||||
<div className="relative mx-auto max-w-7xl px-6 pt-32 pb-12 md:pt-32 md:pb-20 flex flex-col-reverse md:flex-row items-center gap-10 md:gap-16">
|
||||
<div className={`relative ${headerContainerClass} pt-32 pb-12 md:pt-32 md:pb-20 flex flex-col-reverse md:flex-row items-center gap-10 md:gap-16`}>
|
||||
<div className="flex-1 text-center md:text-left space-y-4">
|
||||
{showBreadcrumb && (
|
||||
<div className="flex items-center justify-center md:justify-start gap-2 text-xs font-semibold uppercase tracking-widest text-muted-text">
|
||||
@@ -261,7 +307,7 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
|
||||
)}
|
||||
|
||||
{layoutType === 'none' && showBreadcrumb && (
|
||||
<div className="mx-auto max-w-7xl px-6 pt-24 md:pt-28">
|
||||
<div className={`${headerContainerClass} pt-24 md:pt-28`}>
|
||||
<nav aria-label="Breadcrumb" className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-wider text-muted-text">
|
||||
<Link href="/" className="transition hover:text-brand-primary">Home</Link>
|
||||
<span>/</span>
|
||||
@@ -273,7 +319,7 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
|
||||
)}
|
||||
|
||||
{showPills && (
|
||||
<div className="mx-auto max-w-7xl px-6 pt-4">
|
||||
<div className={`${headerContainerClass} pt-4`}>
|
||||
<div className={`flex flex-wrap gap-2.5 ${layoutType === 'editorial_hero' ? 'justify-center' : 'justify-start'}`}>
|
||||
{!categoriesResult.ok && (
|
||||
<span className="rounded-full border border-rose-100 bg-surface-card px-4 py-1.5 text-xs font-bold uppercase tracking-widest text-rose-500">
|
||||
@@ -304,40 +350,79 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
|
||||
)
|
||||
}
|
||||
|
||||
const renderCatalogGrid = () => (
|
||||
<div className="mx-auto max-w-7xl space-y-5 px-6 pb-12">
|
||||
{/* Featured Series Showcase Banner */}
|
||||
{Boolean(category.featuredSeries?.enabled) && (
|
||||
<CategoryFeaturedSeries
|
||||
featured={category.featuredSeries!}
|
||||
categoryName={categoryName}
|
||||
/>
|
||||
)}
|
||||
const renderCatalogGrid = (payload?: Record<string, unknown>) => {
|
||||
const isBannerFullWidth = Boolean(category.featuredSeries?.fullWidth)
|
||||
|
||||
{/* Catalog Container (Filter Sidebar + Toolbar + Grid) */}
|
||||
{products.length === 0 ? (
|
||||
<div className="ui-panel px-6 py-20 text-center">
|
||||
<p className="text-lg font-semibold text-text-main">No products in this category yet.</p>
|
||||
<Link href="/shop" className="mt-4 inline-flex font-semibold text-brand-primary">Back to Shop</Link>
|
||||
// Merge section payload (from Page Builder CATALOG_GRID block) with category.filterConfig
|
||||
const effectiveFilterConfig: CategoryFilterConfig = {
|
||||
...(category.filterConfig || {}),
|
||||
...(payload as any || {}),
|
||||
containerWidth: (payload?.containerWidth as any) || category.filterConfig?.containerWidth,
|
||||
fullWidth: payload?.fullWidth !== undefined
|
||||
? Boolean(payload.fullWidth)
|
||||
: Boolean(category.filterConfig?.fullWidth || category.filterConfig?.containerWidth === 'full'),
|
||||
cardStyle: (payload?.cardStyle as any) || category.filterConfig?.cardStyle || 'standard',
|
||||
cardSize: (payload?.cardSize as any) || category.filterConfig?.cardSize || 'medium',
|
||||
filterPosition: (payload?.filterPosition as any) || category.filterConfig?.filterPosition || 'sidebar',
|
||||
gridColumns: (payload?.gridColumns as any) !== undefined ? (payload?.gridColumns as any) : (payload?.cardSize ? undefined : category.filterConfig?.gridColumns),
|
||||
showPrice: payload?.showPrice !== undefined ? Boolean(payload.showPrice) : (category.filterConfig?.showPrice ?? true),
|
||||
showAddToCart: payload?.showAddToCart !== undefined ? Boolean(payload.showAddToCart) : (category.filterConfig?.showAddToCart ?? true),
|
||||
showFilters: payload?.showFilters !== undefined ? Boolean(payload.showFilters) : (category.filterConfig?.showFilters ?? true),
|
||||
showToolbar: payload?.showToolbar !== undefined ? Boolean(payload.showToolbar) : (category.filterConfig?.showToolbar ?? true),
|
||||
}
|
||||
|
||||
const isCatalogFullWidth = Boolean(effectiveFilterConfig.fullWidth || effectiveFilterConfig.containerWidth === 'full')
|
||||
const catalogContainerClass = isCatalogFullWidth
|
||||
? 'mx-auto w-full max-w-[1920px] px-4 sm:px-6 lg:px-8 pb-12'
|
||||
: effectiveFilterConfig.containerWidth === 'wide'
|
||||
? 'mx-auto w-full max-w-[1600px] px-4 sm:px-6 pb-12'
|
||||
: 'mx-auto max-w-7xl px-6 pb-12'
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Featured Series Showcase Banner */}
|
||||
{Boolean(category.featuredSeries?.enabled) && (
|
||||
<div className={isBannerFullWidth ? 'mx-auto w-full max-w-[1920px] px-4 sm:px-6 lg:px-8' : 'mx-auto max-w-7xl px-6'}>
|
||||
<CategoryFeaturedSeries
|
||||
featured={category.featuredSeries!}
|
||||
categoryName={categoryName}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Catalog Container (Filter Sidebar + Toolbar + Grid) */}
|
||||
<div className={catalogContainerClass}>
|
||||
{products.length === 0 ? (
|
||||
<div className="ui-panel px-6 py-20 text-center">
|
||||
<p className="text-lg font-semibold text-text-main">No products in this category yet.</p>
|
||||
<Link href="/shop" className="mt-4 inline-flex font-semibold text-brand-primary">Back to Shop</Link>
|
||||
</div>
|
||||
) : (
|
||||
<CategoryCatalogContainer
|
||||
products={products}
|
||||
categoryName={categoryName}
|
||||
filterConfig={effectiveFilterConfig}
|
||||
availableFilters={categoryProductsResult.data?.availableFilters || category.availableFilters || []}
|
||||
layoutGridClass={getProductGridClass(effectiveFilterConfig, isCatalogFullWidth)}
|
||||
showFilters={effectiveFilterConfig.showFilters !== false}
|
||||
showToolbar={effectiveFilterConfig.showToolbar !== false}
|
||||
itemsPerPage={typeof payload?.itemsPerPage === 'number' ? payload.itemsPerPage : undefined}
|
||||
lang={lang}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<CategoryCatalogContainer
|
||||
products={products}
|
||||
categoryName={categoryName}
|
||||
filterConfig={category.filterConfig}
|
||||
availableFilters={categoryProductsResult.data?.availableFilters || category.availableFilters || []}
|
||||
layoutGridClass={getProductGridClass()}
|
||||
lang={lang}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const renderUspBar = () => (
|
||||
<div className="mx-auto max-w-7xl px-6 pb-12">
|
||||
<CategoryUspBar benefits={category.uspBenefits} />
|
||||
</div>
|
||||
)
|
||||
const renderUspBar = () => {
|
||||
const isFullWidth = Boolean(category.filterConfig?.fullWidth || category.featuredSeries?.fullWidth)
|
||||
return (
|
||||
<div className={isFullWidth ? 'mx-auto w-full max-w-[1920px] px-4 sm:px-6 lg:px-8 pb-12' : 'mx-auto max-w-7xl px-6 pb-12'}>
|
||||
<CategoryUspBar benefits={category.uspBenefits} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-surface-page text-text-main">
|
||||
@@ -357,7 +442,7 @@ export default async function CategoryPage({ params }: { params: Promise<{ id: s
|
||||
return <div key={sec.id || idx}>{renderHeaderSection(secPayload)}</div>
|
||||
}
|
||||
if (sec.type === 'CATALOG_GRID') {
|
||||
return <div key={sec.id || idx}>{renderCatalogGrid()}</div>
|
||||
return <div key={sec.id || idx}>{renderCatalogGrid(secPayload)}</div>
|
||||
}
|
||||
if (sec.type === 'USP_BAR') {
|
||||
return <div key={sec.id || idx}>{renderUspBar()}</div>
|
||||
|
||||
@@ -5,18 +5,22 @@ import CheckoutClient from '@/components/CheckoutClient'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import { getCategoriesResult, getCmsPageResult, getStoreSettings } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
import { getServerTranslations } from '@/lib/translations'
|
||||
|
||||
// lang is extracted from params
|
||||
export async function generateMetadata({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params;
|
||||
const settings = await getStoreSettings(lang);
|
||||
const [settings, t] = await Promise.all([
|
||||
getStoreSettings(lang),
|
||||
Promise.resolve(getServerTranslations(lang)),
|
||||
]);
|
||||
const brandName = settings?.brandName || '';
|
||||
const description = brandName
|
||||
? `ตรวจสอบรายการสินค้า กรอกข้อมูลจัดส่ง และสร้างคำสั่งซื้อ ${brandName}`
|
||||
: 'ตรวจสอบรายการสินค้า กรอกข้อมูลจัดส่ง และสร้างคำสั่งซื้อ';
|
||||
? (t.checkout?.seoDescription ? t.checkout.seoDescription.replace('{{brand}}', brandName) : `Review items and place order with ${brandName}`)
|
||||
: (t.checkout?.seoDescriptionDefault || 'Review items and place order');
|
||||
|
||||
return generateSeoMetadata({
|
||||
title: 'Checkout',
|
||||
title: t.checkout?.pageHeading || 'Checkout',
|
||||
description,
|
||||
path: '/checkout',
|
||||
lang,
|
||||
@@ -26,6 +30,7 @@ export async function generateMetadata({ params }: { params: Promise<{ lang: str
|
||||
|
||||
export default async function CheckoutPage({ params }: { params: Promise<{ lang: string }> }) {
|
||||
const { lang } = await params
|
||||
const t = getServerTranslations(lang)
|
||||
const [categoriesResult, cmsLayoutResult] = await Promise.all([
|
||||
getCategoriesResult(lang),
|
||||
getCmsPageResult('checkout-layout', lang),
|
||||
@@ -51,16 +56,18 @@ export default async function CheckoutPage({ params }: { params: Promise<{ lang:
|
||||
<div className="mx-auto mb-5 flex h-16 w-16 items-center justify-center rounded-full bg-surface-card text-brand-primary shadow-card">
|
||||
<CreditCard className="h-8 w-8" />
|
||||
</div>
|
||||
<p className="ui-eyebrow">Secure Checkout</p>
|
||||
<h1 className="mt-4 text-4xl font-semibold text-text-main md:text-5xl">ยืนยันคำสั่งซื้อ</h1>
|
||||
<p className="ui-eyebrow">{t.checkout?.title || 'Secure Checkout'}</p>
|
||||
<h1 className="mt-4 text-4xl font-semibold text-text-main md:text-5xl">
|
||||
{t.checkout?.pageHeading || 'Confirm Order'}
|
||||
</h1>
|
||||
<p className="mx-auto mt-4 max-w-2xl text-muted-text">
|
||||
ตรวจสอบรายการสินค้า ใช้คูปองหรือคะแนนสะสม และสร้างคำสั่งซื้อเพื่อไปยังหน้ารายละเอียดออเดอร์
|
||||
{t.checkout?.pageDescription || 'Review your items and place your order.'}
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="mx-auto max-w-7xl px-6 py-14 md:py-20">
|
||||
<CheckoutClient />
|
||||
<CheckoutClient lang={lang} />
|
||||
</section>
|
||||
|
||||
{bottomSections.length > 0 && (
|
||||
|
||||
@@ -181,6 +181,7 @@ export default async function RootLayout({
|
||||
exploreMenu={footerMenus.exploreMenu}
|
||||
supportMenu={footerMenus.supportMenu}
|
||||
settings={settings}
|
||||
lang={lang}
|
||||
/>
|
||||
</ClientProviders>
|
||||
</div>
|
||||
|
||||
@@ -7,28 +7,16 @@ import { CheckCircle2, Clock, PackageCheck, Truck } from 'lucide-react'
|
||||
import { formatPrice } from '@homaroma/types'
|
||||
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import OrderPaymentClient from '@/components/OrderPaymentClient'
|
||||
import { getCategoriesResult, getCmsPageResult, getOrderResult, getStoreSettings } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
import { orderStatusBadgeClass, toneClass } from '@/lib/theme'
|
||||
import { getServerTranslations } from '@/lib/translations'
|
||||
|
||||
// lang is extracted from params
|
||||
function orderLabel(id: number) {
|
||||
return `#${id.toString().padStart(4, '0')}`
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
const labels: Record<string, string> = {
|
||||
PENDING_PAYMENT: 'รอชำระเงิน',
|
||||
PAID: 'ชำระเงินแล้ว',
|
||||
PROCESSING: 'กำลังจัดเตรียมสินค้า',
|
||||
SHIPPED: 'จัดส่งแล้ว',
|
||||
DELIVERED: 'จัดส่งสำเร็จ',
|
||||
CANCELLED: 'ยกเลิกแล้ว',
|
||||
}
|
||||
|
||||
return labels[status] ?? status.replaceAll('_', ' ')
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ id: string, lang: string }> }): Promise<Metadata> {
|
||||
const { id, lang } = await params
|
||||
const [result, settings] = await Promise.all([
|
||||
@@ -38,10 +26,11 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
|
||||
|
||||
if (!result.ok) return { title: 'Order Not Found' }
|
||||
|
||||
const t = getServerTranslations(lang)
|
||||
const brandName = settings?.brandName || ''
|
||||
const description = brandName
|
||||
? `ตรวจสอบสถานะคำสั่งซื้อ ${orderLabel(result.data.id)} จาก ${brandName}`
|
||||
: `ตรวจสอบสถานะคำสั่งซื้อ ${orderLabel(result.data.id)}`
|
||||
? `${t.order.header.orderStatus} ${orderLabel(result.data.id)} - ${brandName}`
|
||||
: `${t.order.header.orderStatus} ${orderLabel(result.data.id)}`
|
||||
|
||||
return generateSeoMetadata({
|
||||
title: `Order ${orderLabel(result.data.id)}`,
|
||||
@@ -54,10 +43,12 @@ export async function generateMetadata({ params }: { params: Promise<{ id: strin
|
||||
|
||||
export default async function OrderStatusPage({ params }: { params: Promise<{ id: string, lang: string }> }) {
|
||||
const { id, lang } = await params
|
||||
const [orderResult, categoriesResult, cmsLayoutResult] = await Promise.all([
|
||||
const t = getServerTranslations(lang)
|
||||
const [orderResult, categoriesResult, cmsLayoutResult, settings] = await Promise.all([
|
||||
getOrderResult(id),
|
||||
getCategoriesResult(lang),
|
||||
getCmsPageResult('order-success-layout', lang),
|
||||
getStoreSettings(lang),
|
||||
])
|
||||
const categories = categoriesResult.ok ? categoriesResult.data : []
|
||||
const cmsSections = cmsLayoutResult.ok ? cmsLayoutResult.data.sections ?? [] : []
|
||||
@@ -77,9 +68,11 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id
|
||||
)}
|
||||
<section className="mx-auto max-w-3xl px-6 py-24 text-center">
|
||||
<div className={`ui-panel p-10 ${toneClass('error')}`}>
|
||||
<h1 className="text-2xl font-semibold text-text-main">ไม่สามารถโหลดคำสั่งซื้อนี้ได้</h1>
|
||||
<h1 className="text-2xl font-semibold text-text-main">{t.order.navigation.loadFailed}</h1>
|
||||
<p className="mt-3 text-sm text-rose-500">{orderResult.error}</p>
|
||||
<Link href="/track" className="mt-6 inline-flex font-semibold text-brand-primary">กลับไปค้นหาคำสั่งซื้อ</Link>
|
||||
<Link href="/track" className="mt-6 inline-flex font-semibold text-brand-primary">
|
||||
{t.order.navigation.backToTrack}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -88,6 +81,11 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id
|
||||
|
||||
const order = orderResult.data
|
||||
const subtotal = order.subtotal ?? Math.max(0, order.totalAmount - order.shippingFee)
|
||||
const statusLabel = t.order?.status?.[order.status as keyof typeof t.order.status] ?? order.status.replaceAll('_', ' ')
|
||||
const formattedDate = new Date(order.createdAt).toLocaleDateString(
|
||||
lang === 'th' ? 'th-TH' : lang === 'ja' ? 'ja-JP' : lang === 'zh' ? 'zh-CN' : 'en-US',
|
||||
{ year: 'numeric', month: 'short', day: 'numeric' },
|
||||
)
|
||||
|
||||
return (
|
||||
<main className="min-h-[70vh] bg-surface-page">
|
||||
@@ -100,38 +98,38 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id
|
||||
<div className="mx-auto mb-5 flex h-20 w-20 items-center justify-center rounded-full bg-surface-card text-brand-primary shadow-card">
|
||||
<CheckCircle2 className="h-10 w-10" />
|
||||
</div>
|
||||
<p className="ui-eyebrow">Order Status</p>
|
||||
<p className="ui-eyebrow">{t.order.header.orderStatus}</p>
|
||||
<h1 className="mt-4 text-4xl font-semibold text-text-main md:text-5xl">
|
||||
{orderLabel(order.id)}
|
||||
</h1>
|
||||
<p className="mt-3 text-muted-text">คำสั่งซื้อของคุณถูกบันทึกแล้ว สามารถตรวจสอบสถานะล่าสุดได้จากหน้านี้</p>
|
||||
<p className="mt-3 text-muted-text">{t.order.header.orderReceived}</p>
|
||||
</div>
|
||||
|
||||
<section className="grid gap-5 md:grid-cols-3">
|
||||
<article className="ui-panel p-6">
|
||||
<Clock className="mb-4 h-6 w-6 text-brand-primary" />
|
||||
<p className="text-xs font-bold uppercase tracking-[0.22em] text-muted-text/70">Created</p>
|
||||
<p className="text-xs font-bold uppercase tracking-[0.22em] text-muted-text/70">{t.order.cards.created}</p>
|
||||
<p className="mt-2 font-semibold text-text-main">
|
||||
{new Date(order.createdAt).toLocaleDateString('th-TH')}
|
||||
{formattedDate}
|
||||
</p>
|
||||
</article>
|
||||
<article className="ui-panel p-6">
|
||||
<PackageCheck className="mb-4 h-6 w-6 text-brand-primary" />
|
||||
<p className="text-xs font-bold uppercase tracking-[0.22em] text-muted-text/70">Status</p>
|
||||
<p className="text-xs font-bold uppercase tracking-[0.22em] text-muted-text/70">{t.order.cards.status}</p>
|
||||
<span className={`mt-3 inline-flex rounded-full px-3 py-1 text-xs font-bold uppercase tracking-[0.14em] ${orderStatusBadgeClass(order.status)}`}>
|
||||
{statusLabel(order.status)}
|
||||
{statusLabel}
|
||||
</span>
|
||||
</article>
|
||||
<article className="ui-panel p-6">
|
||||
<Truck className="mb-4 h-6 w-6 text-brand-primary" />
|
||||
<p className="text-xs font-bold uppercase tracking-[0.22em] text-muted-text/70">Tracking No.</p>
|
||||
<p className="mt-2 font-semibold text-text-main">{order.trackingNo || 'อยู่ระหว่างจัดเตรียมพัสดุ'}</p>
|
||||
<p className="text-xs font-bold uppercase tracking-[0.22em] text-muted-text/70">{t.order.cards.trackingNo}</p>
|
||||
<p className="mt-2 font-semibold text-text-main">{order.trackingNo || t.order.cards.pendingTracking}</p>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section className="ui-panel p-8">
|
||||
<h2 className="border-b border-line-primary/20 pb-4 text-sm font-bold uppercase tracking-[0.22em] text-brand-primary">
|
||||
Order Items
|
||||
{t.order.items.title}
|
||||
</h2>
|
||||
<div className="mt-6 space-y-4">
|
||||
{order.items.map((item) => (
|
||||
@@ -158,41 +156,43 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id
|
||||
|
||||
<div className="mt-6 space-y-3 border-t border-line-primary/20 pt-5 text-sm">
|
||||
<div className="flex justify-between text-muted-text">
|
||||
<span>Subtotal</span>
|
||||
<span>{t.order.items.subtotal}</span>
|
||||
<span>{formatPrice(subtotal)}</span>
|
||||
</div>
|
||||
<div className="flex justify-between text-muted-text">
|
||||
<span>Shipping Fee</span>
|
||||
<span>{t.order.items.shippingFee}</span>
|
||||
<span>{formatPrice(order.shippingFee)}</span>
|
||||
</div>
|
||||
{(order.discountAmount ?? 0) > 0 && (
|
||||
<div className="flex justify-between text-emerald-600">
|
||||
<span>Discount{order.couponCode ? ` (${order.couponCode})` : ''}</span>
|
||||
<span>{t.order.items.discount}{order.couponCode ? ` (${order.couponCode})` : ''}</span>
|
||||
<span>-{formatPrice(order.discountAmount)}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between border-t border-line-primary/20 pt-4 text-lg font-bold text-text-main">
|
||||
<span>Total</span>
|
||||
<span>{t.order.items.total}</span>
|
||||
<span className="text-brand-primary">{formatPrice(order.totalAmount)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{order.status === 'PENDING_PAYMENT' && (
|
||||
<section className={toneClass('warning')}>
|
||||
<h2 className="font-semibold">คำสั่งซื้อนี้ยังรอการชำระเงิน</h2>
|
||||
<p className="mt-2 text-sm leading-relaxed">
|
||||
หน้านี้รองรับการตรวจสอบสถานะคำสั่งซื้อแล้ว ส่วนการชำระเงินและอัปโหลดสลิปบน website จะย้ายมาในรอบถัดไป เพื่อไม่ทำ workaround ที่เสี่ยงกับ payment flow
|
||||
</p>
|
||||
</section>
|
||||
)}
|
||||
{/* Interactive Payment & Slip Upload Section */}
|
||||
<OrderPaymentClient
|
||||
initialOrder={order}
|
||||
bankSettings={settings ? {
|
||||
bankName: settings.bankName,
|
||||
bankAccountName: settings.bankAccountName,
|
||||
bankAccountNumber: settings.bankAccountNumber,
|
||||
} : null}
|
||||
lang={lang}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
<Link href="/track" className="rounded-full bg-surface-card px-6 py-3 font-semibold text-text-main shadow-card transition hover:text-brand-primary">
|
||||
ค้นหาออเดอร์อื่น
|
||||
{t.order.navigation.trackAnother}
|
||||
</Link>
|
||||
<Link href="/shop" className="rounded-full bg-brand-primary px-6 py-3 font-semibold text-surface-card transition hover:bg-brand-primary-hover">
|
||||
กลับไปช้อปต่อ
|
||||
{t.order.navigation.continueShopping}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in new issue
Block a user