x
This commit is contained in:
1 parent
c365add07f
commit
c449de2482
28 files changed
+694
-300
No files matched your search
@@ -4,6 +4,7 @@ import { generateSeoMetadata } from '@/lib/seo'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import { getActiveCouponsResult, getCategoriesResult, getCmsPageResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
import { toneClass } from '@/lib/theme'
|
||||
|
||||
// lang is extracted from params
|
||||
function formatDiscount(type: string, value: number) {
|
||||
@@ -62,7 +63,7 @@ export default async function CouponsPage({ params }: { params: Promise<{ lang:
|
||||
|
||||
<div className="mx-auto max-w-6xl px-6 pb-24">
|
||||
{!couponsResult.ok ? (
|
||||
<div className="ui-panel border border-rose-100 px-6 py-20 text-center">
|
||||
<div className={`ui-panel px-6 py-20 text-center ${toneClass('error')}`}>
|
||||
<Ticket className="mx-auto mb-4 h-16 w-16 text-rose-200" />
|
||||
<h2 className="mb-2 text-xl font-semibold text-text-main">ไม่สามารถโหลดคูปองได้</h2>
|
||||
<p className="text-sm text-rose-500">{couponsResult.error}</p>
|
||||
@@ -92,7 +93,7 @@ export default async function CouponsPage({ params }: { params: Promise<{ lang:
|
||||
<Ticket className="h-6 w-6" />
|
||||
</div>
|
||||
{coupon.expiresAt && (
|
||||
<span className="rounded-md bg-amber-50 px-2 py-1 text-[10px] font-bold uppercase tracking-wider text-amber-600">
|
||||
<span className={`rounded-md px-2 py-1 text-[10px] font-bold uppercase tracking-wider ${toneClass('warning')}`}>
|
||||
หมดเขต {new Date(coupon.expiresAt).toLocaleDateString('th-TH')}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { formatPrice } from '@homaroma/types'
|
||||
import DynamicPageRenderer from '@/components/dynamic/DynamicPageRenderer'
|
||||
import { getCategoriesResult, getCmsPageResult, getOrderResult } from '@/lib/cms'
|
||||
import { splitCmsLayoutSections } from '@/lib/cms-sections'
|
||||
import { orderStatusBadgeClass, toneClass } from '@/lib/theme'
|
||||
|
||||
// lang is extracted from params
|
||||
function orderLabel(id: number) {
|
||||
@@ -28,14 +29,6 @@ function statusLabel(status: string) {
|
||||
return labels[status] ?? status.replaceAll('_', ' ')
|
||||
}
|
||||
|
||||
function statusTone(status: string) {
|
||||
if (status === 'PENDING_PAYMENT') return 'bg-brand-warm/15 text-amber-700 border-amber-100'
|
||||
if (status === 'PAID' || status === 'PROCESSING') return 'bg-brand-success/20 text-emerald-700 border-emerald-100'
|
||||
if (status === 'SHIPPED' || status === 'DELIVERED') return 'bg-brand-cool/40 text-sky-700 border-sky-100'
|
||||
if (status === 'CANCELLED') return 'bg-rose-50 text-rose-700 border-rose-100'
|
||||
return 'bg-surface-page text-muted-text border-line-primary/20'
|
||||
}
|
||||
|
||||
export async function generateMetadata({ params }: { params: Promise<{ id: string, lang: string }> }): Promise<Metadata> {
|
||||
const { id, lang } = await params
|
||||
const result = await getOrderResult(id)
|
||||
@@ -74,7 +67,7 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id
|
||||
<DynamicPageRenderer sections={announcementSections} categories={categories} lang={lang} />
|
||||
)}
|
||||
<section className="mx-auto max-w-3xl px-6 py-24 text-center">
|
||||
<div className="ui-panel border border-rose-100 p-10">
|
||||
<div className={`ui-panel p-10 ${toneClass('error')}`}>
|
||||
<h1 className="text-2xl font-semibold text-text-main">ไม่สามารถโหลดคำสั่งซื้อนี้ได้</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>
|
||||
@@ -116,7 +109,7 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id
|
||||
<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>
|
||||
<span className={`mt-3 inline-flex rounded-full border px-3 py-1 text-xs font-bold uppercase tracking-[0.14em] ${statusTone(order.status)}`}>
|
||||
<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)}
|
||||
</span>
|
||||
</article>
|
||||
@@ -177,7 +170,7 @@ export default async function OrderStatusPage({ params }: { params: Promise<{ id
|
||||
</section>
|
||||
|
||||
{order.status === 'PENDING_PAYMENT' && (
|
||||
<section className="rounded-3xl border border-amber-100 bg-brand-warm/15 p-6 text-amber-800">
|
||||
<section className={toneClass('warning')}>
|
||||
<h2 className="font-semibold">คำสั่งซื้อนี้ยังรอการชำระเงิน</h2>
|
||||
<p className="mt-2 text-sm leading-relaxed">
|
||||
หน้านี้รองรับการตรวจสอบสถานะคำสั่งซื้อแล้ว ส่วนการชำระเงินและอัปโหลดสลิปบน website จะย้ายมาในรอบถัดไป เพื่อไม่ทำ workaround ที่เสี่ยงกับ payment flow
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-body: var(--font-body-active, var(--font-body-locale, ui-sans-serif)), system-ui, sans-serif;
|
||||
--font-display: var(--font-display-active, var(--font-display-locale, var(--font-body-locale, ui-sans-serif))), system-ui, sans-serif;
|
||||
--font-sans: var(--font-body);
|
||||
|
||||
--color-brand-primary: #FF6B6B;
|
||||
--color-brand-primary-hover: #FF4500;
|
||||
--color-brand-accent: #FFF200;
|
||||
@@ -47,8 +43,8 @@
|
||||
@layer base {
|
||||
:root,
|
||||
html {
|
||||
--font-body-locale: var(--font-sarabun, "Sarabun"), "Noto Sans Thai", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-display-locale: var(--font-body-locale);
|
||||
--font-body-locale: "Sarabun", "Noto Sans Thai", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-display-locale: "Sarabun", "Noto Sans Thai", ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
html[lang="en"] {
|
||||
@@ -76,8 +72,12 @@
|
||||
--font-display-locale: var(--font-body-locale);
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: var(--font-body-locale);
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-surface-page text-text-main font-sans antialiased;
|
||||
@apply bg-surface-page text-text-main antialiased;
|
||||
background-image: radial-gradient(circle at top right, rgba(255,107,107,0.15), transparent 40%),
|
||||
radial-gradient(circle at bottom left, rgba(255,160,122,0.15), transparent 40%);
|
||||
min-height: 100vh;
|
||||
@@ -85,6 +85,7 @@
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
@apply text-text-main font-semibold tracking-tight;
|
||||
font-family: var(--font-display-locale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -205,6 +206,188 @@
|
||||
@apply italic text-brand-accent;
|
||||
}
|
||||
|
||||
.ui-hero-shell {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 90vh;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
background: var(--color-text-main);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ui-hero-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: color-mix(in srgb, var(--color-text-main) 40%, transparent);
|
||||
}
|
||||
|
||||
.ui-hero-overlay-strong {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
color-mix(in srgb, var(--color-text-main) 90%, transparent),
|
||||
color-mix(in srgb, var(--color-text-main) 10%, transparent),
|
||||
color-mix(in srgb, var(--color-text-main) 30%, transparent)
|
||||
);
|
||||
}
|
||||
|
||||
.ui-hero-title {
|
||||
color: #fff;
|
||||
font-family: var(--font-display-locale);
|
||||
font-size: 3rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.25;
|
||||
letter-spacing: 0.025em;
|
||||
filter: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));
|
||||
}
|
||||
|
||||
.ui-hero-copy {
|
||||
margin-inline: auto;
|
||||
max-width: 42rem;
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 2rem;
|
||||
color: rgb(255 255 255 / 0.9);
|
||||
font-size: 1.125rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.75rem;
|
||||
filter: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));
|
||||
}
|
||||
|
||||
.ui-hero-button {
|
||||
border-radius: 9999px;
|
||||
background: var(--color-surface-card);
|
||||
padding: 1rem 2rem;
|
||||
color: var(--color-text-main);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
transition: background-color 300ms ease, box-shadow 300ms ease;
|
||||
}
|
||||
|
||||
.ui-hero-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem 2rem;
|
||||
color: #fff;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
|
||||
.ui-banner-shell {
|
||||
position: relative;
|
||||
margin-inline: auto;
|
||||
min-height: 360px;
|
||||
max-width: 80rem;
|
||||
overflow: hidden;
|
||||
border-radius: 1.5rem;
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
.ui-banner-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
color-mix(in srgb, var(--color-text-main) 75%, transparent),
|
||||
color-mix(in srgb, var(--color-text-main) 35%, transparent),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.ui-banner-content {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
max-width: 36rem;
|
||||
padding: 2rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ui-banner-content > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.ui-section-dark {
|
||||
background: var(--color-text-main);
|
||||
padding: 5rem 1.5rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ui-section-dark-card {
|
||||
overflow: hidden;
|
||||
border-radius: 1.5rem;
|
||||
border: 1px solid color-mix(in srgb, var(--color-line-primary) 10%, transparent);
|
||||
background: color-mix(in srgb, var(--color-surface-card) 60%, transparent);
|
||||
}
|
||||
|
||||
.ui-section-dark-card-title {
|
||||
color: #fff;
|
||||
font-size: 2.25rem;
|
||||
font-weight: 600;
|
||||
line-height: 2.5rem;
|
||||
}
|
||||
|
||||
.ui-section-dark-card-copy {
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 4;
|
||||
color: rgb(255 255 255 / 0.7);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.625;
|
||||
}
|
||||
|
||||
.ui-section-dark-link {
|
||||
display: inline-flex;
|
||||
color: var(--color-brand-accent);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
transition: color 150ms ease;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.ui-hero-shell {
|
||||
aspect-ratio: 21 / 9;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ui-hero-title {
|
||||
font-size: 4.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.ui-hero-copy {
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
}
|
||||
|
||||
.ui-banner-content {
|
||||
padding: 3.5rem;
|
||||
}
|
||||
|
||||
.ui-section-dark-card-title {
|
||||
font-size: 3rem;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.ui-hero-title {
|
||||
font-size: 6rem;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-panel {
|
||||
@apply rounded-3xl bg-white shadow-card;
|
||||
}
|
||||
@@ -265,6 +448,64 @@
|
||||
@apply rounded-full border px-3 py-1 text-xs font-bold;
|
||||
}
|
||||
|
||||
.ui-status-badge-pending {
|
||||
border-color: rgb(254 243 199);
|
||||
background: rgb(255 251 235);
|
||||
color: rgb(180 83 9);
|
||||
}
|
||||
|
||||
.ui-status-badge-active {
|
||||
border-color: rgb(209 250 229);
|
||||
background: rgb(236 253 245);
|
||||
color: rgb(4 120 87);
|
||||
}
|
||||
|
||||
.ui-status-badge-shipped {
|
||||
border-color: rgb(224 242 254);
|
||||
background: rgb(240 249 255);
|
||||
color: rgb(3 105 161);
|
||||
}
|
||||
|
||||
.ui-status-badge-cancelled {
|
||||
border-color: rgb(255 228 230);
|
||||
background: rgb(255 241 242);
|
||||
color: rgb(190 18 60);
|
||||
}
|
||||
|
||||
.ui-status-badge-neutral {
|
||||
border-color: color-mix(in srgb, var(--color-line-primary) 20%, transparent);
|
||||
background: var(--color-surface-page);
|
||||
color: color-mix(in srgb, var(--color-text-main) 80%, transparent);
|
||||
}
|
||||
|
||||
.ui-tone-warning {
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgb(254 243 199);
|
||||
background: rgb(255 251 235);
|
||||
color: rgb(146 64 14);
|
||||
}
|
||||
|
||||
.ui-tone-success {
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgb(209 250 229);
|
||||
background: rgb(236 253 245);
|
||||
color: rgb(4 120 87);
|
||||
}
|
||||
|
||||
.ui-tone-error {
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgb(255 228 230);
|
||||
background: rgb(255 241 242);
|
||||
color: rgb(190 18 60);
|
||||
}
|
||||
|
||||
.ui-tone-info {
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgb(224 242 254);
|
||||
background: rgb(240 249 255);
|
||||
color: rgb(3 105 161);
|
||||
}
|
||||
|
||||
.ui-avatar-shell {
|
||||
@apply flex h-20 w-20 items-center justify-center overflow-hidden rounded-full border border-brand-primary/20 bg-surface-page;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useAuthStore, type Customer } from '../store/authStore'
|
||||
import { useCartStore } from '../store/cartStore'
|
||||
import { useLang } from '../hooks/useLang'
|
||||
import { getPathWithLocale } from '../lib/locales'
|
||||
import { toneClass } from '../lib/theme'
|
||||
|
||||
type StoreSettings = {
|
||||
shippingFee?: number
|
||||
@@ -571,10 +572,10 @@ export default function CheckoutClient() {
|
||||
</section>
|
||||
|
||||
{submitError && (
|
||||
<div className="flex gap-3 rounded-2xl border border-rose-100 bg-rose-50 p-4 text-sm text-rose-700">
|
||||
<AlertCircle className="h-5 w-5 shrink-0" />
|
||||
<p>{submitError}</p>
|
||||
</div>
|
||||
<div className={`flex gap-3 p-4 text-sm ${toneClass('error')}`}>
|
||||
<AlertCircle className="h-5 w-5 shrink-0" />
|
||||
<p>{submitError}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
@@ -641,15 +642,15 @@ export default function CheckoutClient() {
|
||||
type="button"
|
||||
onClick={applyCoupon}
|
||||
disabled={couponLoading || !couponCode.trim()}
|
||||
className="rounded-2xl bg-text-main px-4 py-3 text-xs font-bold uppercase tracking-widest text-white transition hover:bg-brand-primary disabled:opacity-50"
|
||||
>
|
||||
{couponLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : 'ใช้'}
|
||||
</button>
|
||||
className="rounded-2xl bg-text-main px-4 py-3 text-xs font-bold uppercase tracking-widest text-white transition hover:bg-brand-primary disabled:opacity-50"
|
||||
>
|
||||
{couponLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : 'ใช้'}
|
||||
</button>
|
||||
</div>
|
||||
{couponError && <p className="text-xs font-semibold text-rose-500">{couponError}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between rounded-2xl border border-emerald-100 bg-emerald-50 p-4">
|
||||
<div className={`flex items-center justify-between p-4 ${toneClass('success')}`}>
|
||||
<span className="flex items-center gap-2 text-sm font-bold text-emerald-700">
|
||||
<Ticket className="h-4 w-4" />
|
||||
{appliedCoupon}
|
||||
@@ -742,7 +743,7 @@ export default function CheckoutClient() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-amber-100 bg-amber-50 p-4 text-sm text-amber-800">
|
||||
<div className={`p-4 text-sm ${toneClass('warning')}`}>
|
||||
<div className="flex gap-2">
|
||||
<CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0" />
|
||||
<p>
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { formatPrice, type CustomerAddress, type Order } from '@homaroma/types'
|
||||
|
||||
import { API_BASE } from '../lib/api'
|
||||
import { orderStatusBadgeClass, toneClass } from '../lib/theme'
|
||||
import { useAuthStore, type Customer } from '../store/authStore'
|
||||
|
||||
type ProfileTab = 'orders' | 'addresses' | 'settings'
|
||||
@@ -74,14 +75,6 @@ function statusLabel(status: string) {
|
||||
return labels[status] ?? status.replaceAll('_', ' ')
|
||||
}
|
||||
|
||||
function statusClass(status: string) {
|
||||
if (status === 'PENDING_PAYMENT') return 'border-amber-100 bg-amber-50 text-amber-700'
|
||||
if (status === 'PAID' || status === 'PROCESSING') return 'border-emerald-100 bg-emerald-50 text-emerald-700'
|
||||
if (status === 'SHIPPED' || status === 'DELIVERED') return 'border-sky-100 bg-sky-50 text-sky-700'
|
||||
if (status === 'CANCELLED') return 'border-rose-100 bg-rose-50 text-rose-700'
|
||||
return 'border-line-primary/20 bg-surface-page text-text-main/80'
|
||||
}
|
||||
|
||||
function formatAddress(address: CustomerAddress) {
|
||||
return `${address.addressLine} ต.${address.subdistrict} อ.${address.district} จ.${address.province} ${address.zipcode}`
|
||||
}
|
||||
@@ -294,7 +287,7 @@ export default function ProfileClient() {
|
||||
ล็อกอินเพื่อตรวจสอบประวัติคำสั่งซื้อ สะสมคะแนนสมาชิก และบันทึกสมุดที่อยู่จัดส่งสินค้า
|
||||
</p>
|
||||
{error && (
|
||||
<p className="mt-5 rounded-2xl border border-amber-100 bg-amber-50 p-4 text-sm text-amber-700">
|
||||
<p className={`mt-5 p-4 text-sm ${toneClass('warning')}`}>
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
@@ -323,7 +316,7 @@ export default function ProfileClient() {
|
||||
return (
|
||||
<section className="space-y-8">
|
||||
{error && (
|
||||
<div className="rounded-3xl border border-rose-100 bg-rose-50 p-5 text-sm font-medium text-rose-600">
|
||||
<div className={`p-5 text-sm font-medium ${toneClass('error')}`}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
@@ -398,7 +391,7 @@ export default function ProfileClient() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`ui-status-badge ${statusClass(order.status)}`}>
|
||||
<span className={orderStatusBadgeClass(order.status)}>
|
||||
{statusLabel(order.status)}
|
||||
</span>
|
||||
<Link href={`/order/${order.id}`} className="rounded-full border border-line-primary/20 p-2 text-muted-text transition hover:text-brand-primary">
|
||||
@@ -534,10 +527,10 @@ export default function ProfileClient() {
|
||||
|
||||
<form onSubmit={handleSettingsSubmit} className="ui-panel max-w-xl space-y-5 p-6">
|
||||
{settingsSuccess && (
|
||||
<div className="flex items-center justify-center gap-2 rounded-2xl border border-emerald-100 bg-emerald-50 p-4 text-sm font-bold text-emerald-700">
|
||||
<Check className="h-4 w-4" />
|
||||
บันทึกข้อมูลติดต่อสำเร็จแล้ว
|
||||
</div>
|
||||
<div className={`flex items-center justify-center gap-2 p-4 text-sm font-bold ${toneClass('success')}`}>
|
||||
<Check className="h-4 w-4" />
|
||||
บันทึกข้อมูลติดต่อสำเร็จแล้ว
|
||||
</div>
|
||||
)}
|
||||
<TextInput label="อีเมล" value={settingsForm.email} onChange={(email) => setSettingsForm((current) => ({ ...current, email }))} type="email" />
|
||||
<TextInput label="เบอร์โทรศัพท์" value={settingsForm.phone} onChange={(phone) => setSettingsForm((current) => ({ ...current, phone }))} type="tel" />
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ChevronRight, Loader2, Package, Search } from 'lucide-react'
|
||||
import { API_BASE } from '../lib/api'
|
||||
import { useLang } from '../hooks/useLang'
|
||||
import { getPathWithLocale } from '../lib/locales'
|
||||
import { toneClass } from '../lib/theme'
|
||||
|
||||
type TrackResponse =
|
||||
| { success: true; orderId: number }
|
||||
@@ -105,7 +106,7 @@ export default function TrackOrderClient() {
|
||||
|
||||
<form className="space-y-5" onSubmit={handleSubmit}>
|
||||
{error && (
|
||||
<div className="rounded-2xl border border-rose-100 bg-rose-50 p-4 text-sm font-medium text-rose-600">
|
||||
<div className={`p-4 text-sm font-medium ${toneClass('error')}`}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -79,8 +79,8 @@ function AnnouncementBar({ payload, lang }: { payload: Record<string, unknown>,
|
||||
if (!slides.length) return null
|
||||
|
||||
return (
|
||||
<section
|
||||
className={`announcement-bar absolute top-0 left-0 z-[60] w-full overflow-hidden px-4 py-3 text-center text-sm font-semibold tracking-wide ${backgroundColor ? '' : 'bg-text-main'} ${textColor ? '' : 'text-white'}`}
|
||||
<section
|
||||
className={`announcement-bar absolute left-0 top-0 z-[60] w-full overflow-hidden px-4 py-3 text-center text-sm font-semibold tracking-wide ${backgroundColor ? '' : 'bg-text-main'} ${textColor ? '' : 'text-white'}`}
|
||||
style={{ backgroundColor, color: textColor }}
|
||||
>
|
||||
<Swiper
|
||||
@@ -112,7 +112,7 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
|
||||
const buttonLink = typeof payload.buttonLink === 'string' ? payload.buttonLink : '/shop'
|
||||
|
||||
return (
|
||||
<section className="relative flex min-h-[90vh] w-full items-center justify-center overflow-hidden bg-text-main md:aspect-[21/9] md:min-h-0">
|
||||
<section className="ui-hero-shell">
|
||||
<Image
|
||||
src={imageUrl}
|
||||
alt={title || 'HomAroma'}
|
||||
@@ -120,8 +120,8 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
|
||||
unoptimized
|
||||
className="absolute inset-0 h-full w-full scale-[1.02] object-cover"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-text-main/40" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-text-main/90 via-text-main/10 to-text-main/30" />
|
||||
<div className="ui-hero-overlay" />
|
||||
<div className="ui-hero-overlay-strong" />
|
||||
<div className="relative z-10 mx-auto mt-16 w-full max-w-4xl space-y-6 px-8 text-center md:mt-0">
|
||||
<div className="space-y-6">
|
||||
{tagline && (
|
||||
@@ -129,24 +129,24 @@ function HeroSection({ payload, lang }: { payload: Record<string, unknown>, lang
|
||||
{tagline}
|
||||
</p>
|
||||
)}
|
||||
<h1 className="font-display text-5xl font-light leading-tight tracking-wide text-white drop-shadow-md md:text-7xl lg:text-8xl">
|
||||
<h1 className="ui-hero-title">
|
||||
{renderAccentTitle(title || 'HomAroma')}
|
||||
</h1>
|
||||
{description && (
|
||||
<p className="mx-auto max-w-2xl pb-8 pt-4 text-lg font-light text-white/90 drop-shadow-md md:text-xl">
|
||||
<p className="ui-hero-copy">
|
||||
{description}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-col items-center justify-center gap-6 sm:flex-row">
|
||||
<Link
|
||||
href={buttonLink}
|
||||
className="rounded-full bg-surface-card px-8 py-4 text-sm font-bold uppercase tracking-widest text-text-main transition-all duration-300 hover:bg-surface-page hover:shadow-xl"
|
||||
className="ui-hero-button"
|
||||
>
|
||||
{buttonText || 'Explore Collection'}
|
||||
</Link>
|
||||
<a
|
||||
href="#categories"
|
||||
className="group flex items-center gap-2 px-8 py-4 text-sm font-medium uppercase tracking-widest text-white transition-colors hover:text-brand-accent"
|
||||
className="ui-hero-link"
|
||||
>
|
||||
{heroLearnMoreText(resolvedLang)}
|
||||
<span className="transition-transform group-hover:translate-x-1">→</span>
|
||||
@@ -285,10 +285,10 @@ function BannerSection({ payload, lang }: { payload: Record<string, unknown>, la
|
||||
const linkUrl = typeof payload.linkUrl === 'string' ? payload.linkUrl : ''
|
||||
|
||||
const content = (
|
||||
<div className="relative mx-auto min-h-[360px] max-w-7xl overflow-hidden rounded-3xl shadow-card">
|
||||
<div className="ui-banner-shell">
|
||||
<Image src={imageUrl} alt={title || 'Banner'} fill unoptimized className="absolute inset-0 h-full w-full object-cover" />
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-text-main/75 via-text-main/35 to-transparent" />
|
||||
<div className="relative z-10 max-w-xl space-y-4 p-8 text-white md:p-14">
|
||||
<div className="ui-banner-overlay" />
|
||||
<div className="ui-banner-content">
|
||||
{title && <h2 className="text-3xl font-semibold md:text-5xl">{title}</h2>}
|
||||
{description && <p className="text-white/90">{description}</p>}
|
||||
{buttonText && <span className="inline-flex rounded-full bg-surface-card px-6 py-3 font-semibold text-text-main">{buttonText}</span>}
|
||||
@@ -329,15 +329,15 @@ function NewsletterSection({ payload, lang }: { payload: Record<string, unknown>
|
||||
const placeholder = typeof payload.placeholderText === 'string' ? payload.placeholderText : 'you@example.com'
|
||||
|
||||
return (
|
||||
<section className="bg-text-main px-6 py-20 text-white">
|
||||
<section className="ui-section-dark">
|
||||
<div className="mx-auto grid max-w-5xl gap-8 md:grid-cols-[1fr_auto] md:items-end">
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-3xl font-semibold text-white md:text-4xl">{title}</h2>
|
||||
{description && <p className="max-w-2xl text-white/75">{description}</p>}
|
||||
</div>
|
||||
<form className="flex flex-col gap-3 sm:flex-row">
|
||||
<input className="min-w-0 rounded-full px-5 py-3 text-text-main" placeholder={placeholder} type="email" />
|
||||
<button className="rounded-full bg-brand-primary px-6 py-3 font-semibold text-white" type="button">
|
||||
<input className="ui-form-shell min-w-0 rounded-full" placeholder={placeholder} type="email" />
|
||||
<button className="ui-button-brand rounded-full" type="button">
|
||||
{buttonText}
|
||||
</button>
|
||||
</form>
|
||||
@@ -381,7 +381,7 @@ function EditorialTileSection({ payload, lang }: { payload: Record<string, unkno
|
||||
<div className="h-px w-14 bg-brand-primary" />
|
||||
{description && <p className="whitespace-pre-line text-base leading-relaxed text-muted-text md:text-lg" style={{ color: textColor }}>{description}</p>}
|
||||
{buttonText && buttonLink && (
|
||||
<Link href={buttonLink} className="inline-flex rounded-full border border-current px-8 py-3 text-xs font-bold uppercase tracking-[0.22em] transition hover:bg-brand-primary hover:text-white">
|
||||
<Link href={buttonLink} className="ui-section-link inline-flex rounded-full border border-current px-8 py-3 text-xs font-bold uppercase tracking-[0.22em] transition hover:bg-brand-primary hover:text-white">
|
||||
{buttonText}
|
||||
</Link>
|
||||
)}
|
||||
@@ -399,7 +399,7 @@ function EditorialCarouselSection({ payload, lang }: { payload: Record<string, u
|
||||
if (!slides.length) return null
|
||||
|
||||
return (
|
||||
<section className="bg-text-main px-6 py-20 text-white">
|
||||
<section className="ui-section-dark">
|
||||
<div className="mx-auto max-w-7xl space-y-12">
|
||||
{title && (
|
||||
<div className="mx-auto max-w-3xl space-y-4 text-center">
|
||||
@@ -417,7 +417,7 @@ function EditorialCarouselSection({ payload, lang }: { payload: Record<string, u
|
||||
const imageUrl = typeof slide.imageUrl === 'string' ? slide.imageUrl : ''
|
||||
|
||||
return (
|
||||
<article key={index} className="group overflow-hidden rounded-3xl border border-line-primary/10 bg-surface-card/60">
|
||||
<article key={index} className="ui-section-dark-card group">
|
||||
{imageUrl && (
|
||||
<div className="relative aspect-[4/3] overflow-hidden">
|
||||
<Image
|
||||
@@ -431,10 +431,10 @@ function EditorialCarouselSection({ payload, lang }: { payload: Record<string, u
|
||||
)}
|
||||
<div className="space-y-4 p-6">
|
||||
{category && <p className="text-[10px] font-bold uppercase tracking-[0.24em] text-brand-accent">{category}</p>}
|
||||
{slideTitle && <h3 className="text-2xl font-semibold text-white">{renderAccentTitle(slideTitle)}</h3>}
|
||||
{description && <p className="line-clamp-4 text-sm leading-relaxed text-white/70">{description}</p>}
|
||||
{slideTitle && <h3 className="ui-section-dark-card-title">{renderAccentTitle(slideTitle)}</h3>}
|
||||
{description && <p className="ui-section-dark-card-copy">{description}</p>}
|
||||
{buttonText && buttonLink && (
|
||||
<Link href={buttonLink} className="inline-flex text-xs font-bold uppercase tracking-[0.22em] text-brand-accent transition hover:text-white">
|
||||
<Link href={buttonLink} className="ui-section-dark-link">
|
||||
{buttonText} →
|
||||
</Link>
|
||||
)}
|
||||
@@ -475,16 +475,16 @@ function HeroArticleSection({ payload, lang }: { payload: Record<string, unknown
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="relative flex min-h-[60vh] items-center justify-center overflow-hidden bg-text-main px-6 py-24 text-center text-white md:min-h-[76vh]">
|
||||
<section className="ui-hero-shell min-h-[60vh] px-6 py-24 text-center md:min-h-[76vh]">
|
||||
{imageUrl && (
|
||||
<>
|
||||
<Image src={imageUrl} alt={title} fill unoptimized className="absolute inset-0 h-full w-full object-cover" />
|
||||
<div className="absolute inset-0 bg-text-main/55" />
|
||||
<div className="ui-hero-overlay" />
|
||||
</>
|
||||
)}
|
||||
<div className="relative z-10 mx-auto max-w-4xl space-y-6">
|
||||
{category && <p className="text-xs font-bold uppercase tracking-[0.28em] text-brand-accent">{category}</p>}
|
||||
<h1 className="text-4xl font-semibold leading-tight md:text-6xl">{title}</h1>
|
||||
<h1 className="ui-hero-title">{title}</h1>
|
||||
{subtitle && <p className="mx-auto max-w-2xl text-lg text-white/85 md:text-xl">{subtitle}</p>}
|
||||
{(author || date) && (
|
||||
<p className="text-xs font-bold uppercase tracking-[0.22em] text-white/70">
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export type ThemeTone = 'warning' | 'success' | 'error' | 'info'
|
||||
|
||||
export function toneClass(tone: ThemeTone) {
|
||||
return `ui-tone-${tone}`
|
||||
}
|
||||
|
||||
export function orderStatusBadgeClass(status: string) {
|
||||
if (status === 'PENDING_PAYMENT') return 'ui-status-badge ui-status-badge-pending'
|
||||
if (status === 'PAID' || status === 'PROCESSING') return 'ui-status-badge ui-status-badge-active'
|
||||
if (status === 'SHIPPED' || status === 'DELIVERED') return 'ui-status-badge ui-status-badge-shipped'
|
||||
if (status === 'CANCELLED') return 'ui-status-badge ui-status-badge-cancelled'
|
||||
return 'ui-status-badge ui-status-badge-neutral'
|
||||
}
|
||||
Reference in new issue
Block a user