This commit is contained in:
Your Name committed 2026-10-08 07:12:58 +07:00
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>
+15 -8
View File
@@ -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 && (
+1
View File
@@ -181,6 +181,7 @@ export default async function RootLayout({
exploreMenu={footerMenus.exploreMenu}
supportMenu={footerMenus.supportMenu}
settings={settings}
lang={lang}
/>
</ClientProviders>
</div>
+42 -42
View File
@@ -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>