564 lines
27 KiB
TypeScript
564 lines
27 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { useParams, Link } from 'react-router-dom'
|
|
import Navbar from '../components/Navbar'
|
|
import Footer from '../components/Footer'
|
|
import DynamicPageRenderer from '../components/DynamicPageRenderer'
|
|
import { API_BASE, formatPrice, type Order, type StoreSettings, type Category, type PageSection } from '../types'
|
|
import { CheckCircle2, UploadCloud, Loader2, Copy, CreditCard, QrCode, Landmark } from 'lucide-react'
|
|
|
|
export default function OrderSuccessPage() {
|
|
const { id } = useParams<{ id: string }>()
|
|
const [order, setOrder] = useState<Order | null>(null)
|
|
const [settings, setSettings] = useState<StoreSettings | null>(null)
|
|
const [categories, setCategories] = useState<Category[]>([])
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
const [uploading, setUploading] = useState(false)
|
|
const [copied, setCopied] = useState(false)
|
|
const [cmsSections, setCmsSections] = useState<PageSection[]>([])
|
|
|
|
const [paymentMethod, setPaymentMethod] = useState<'bank' | 'promptpay' | 'card'>('bank')
|
|
const [qrCodeUrl, setQrCodeUrl] = useState<string | null>(null)
|
|
const [generatingQr, setGeneratingQr] = useState(false)
|
|
const [polling, setPolling] = useState(false)
|
|
|
|
// Card Form State
|
|
const [cardNumber, setCardNumber] = useState('')
|
|
const [cardName, setCardName] = useState('')
|
|
const [cardExpiry, setCardExpiry] = useState('') // MM/YY
|
|
const [cardCvv, setCardCvv] = useState('')
|
|
const [payingCard, setPayingCard] = useState(false)
|
|
const [cardError, setCardError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
let interval: any;
|
|
if (polling && order && order.status === 'PENDING_PAYMENT') {
|
|
interval = setInterval(() => {
|
|
fetch(`${API_BASE}/orders/${order.id}`)
|
|
.then(res => res.json())
|
|
.then(updatedOrder => {
|
|
if (updatedOrder && updatedOrder.status === 'PAID') {
|
|
setOrder(updatedOrder);
|
|
setPolling(false);
|
|
clearInterval(interval);
|
|
}
|
|
})
|
|
.catch(console.error);
|
|
}, 5000);
|
|
}
|
|
return () => clearInterval(interval);
|
|
}, [polling, order]);
|
|
|
|
const handleGeneratePromptPay = async () => {
|
|
if (!order) return
|
|
setGeneratingQr(true)
|
|
try {
|
|
const res = await fetch(`${API_BASE}/payments/charge`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
orderId: order.id,
|
|
amount: order.totalAmount,
|
|
type: 'promptpay'
|
|
})
|
|
})
|
|
const data = await res.json()
|
|
if (res.ok && data.success && data.qrCodeUrl) {
|
|
setQrCodeUrl(data.qrCodeUrl)
|
|
setPolling(true)
|
|
} else {
|
|
alert(data.error || 'Failed to generate PromptPay QR')
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
alert('Failed to connect to payment server')
|
|
} finally {
|
|
setGeneratingQr(false)
|
|
}
|
|
}
|
|
|
|
const handleCardPay = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!order) return
|
|
setPayingCard(true)
|
|
setCardError(null)
|
|
|
|
try {
|
|
const omise = (window as any).Omise
|
|
if (!omise) {
|
|
throw new Error('Omise library is loading, please try again.')
|
|
}
|
|
|
|
omise.setPublicKey(import.meta.env.VITE_OMISE_PUBLIC_KEY)
|
|
|
|
const parts = cardExpiry.split('/')
|
|
if (parts.length !== 2) {
|
|
throw new Error('Expiry date must be in MM/YY format')
|
|
}
|
|
const expMonth = parseInt(parts[0], 10)
|
|
let expYear = parseInt(parts[1], 10)
|
|
if (expYear < 100) expYear += 2000
|
|
|
|
const cardData = {
|
|
name: cardName,
|
|
number: cardNumber.replace(/\s+/g, ''),
|
|
expiration_month: expMonth,
|
|
expiration_year: expYear,
|
|
security_code: cardCvv
|
|
}
|
|
|
|
omise.createToken('card', cardData, async (statusCode: number, response: any) => {
|
|
if (statusCode === 200) {
|
|
try {
|
|
const chargeRes = await fetch(`${API_BASE}/payments/charge`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
orderId: order.id,
|
|
amount: order.totalAmount,
|
|
type: 'card',
|
|
token: response.id
|
|
})
|
|
})
|
|
const chargeData = await chargeRes.json()
|
|
if (chargeRes.ok && chargeData.success) {
|
|
if (chargeData.authorizeUri) {
|
|
window.location.href = chargeData.authorizeUri
|
|
} else {
|
|
window.location.reload()
|
|
}
|
|
} else {
|
|
setCardError(chargeData.error || 'Card charge request failed')
|
|
setPayingCard(false)
|
|
}
|
|
} catch (err: any) {
|
|
setCardError('Failed to verify card charge on server')
|
|
setPayingCard(false)
|
|
}
|
|
} else {
|
|
setCardError(response.message || 'Validation failed for card details')
|
|
setPayingCard(false)
|
|
}
|
|
})
|
|
} catch (err: any) {
|
|
setCardError(err.message || 'An error occurred during payment processing')
|
|
setPayingCard(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
Promise.all([
|
|
fetch(`${API_BASE}/orders/${id}`).then(r => r.json()),
|
|
fetch(`${API_BASE}/settings`).then(r => r.json()),
|
|
fetch(`${API_BASE}/categories`).then(r => r.json()),
|
|
fetch(`${API_BASE}/pages/order-success-layout`).then(r => r.ok ? r.json() : null).catch(() => null)
|
|
])
|
|
.then(([orderData, settingsData, catData, pageData]) => {
|
|
setOrder(orderData)
|
|
setSettings(settingsData)
|
|
setCategories(catData)
|
|
if (pageData && pageData.sections) {
|
|
setCmsSections(pageData.sections)
|
|
}
|
|
})
|
|
.catch(console.error)
|
|
.finally(() => setLoading(false))
|
|
}, [id])
|
|
|
|
const copyToClipboard = () => {
|
|
if (!settings) return
|
|
navigator.clipboard.writeText(settings.bankAccountNumber)
|
|
setCopied(true)
|
|
setTimeout(() => setCopied(false), 2000)
|
|
}
|
|
|
|
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0]
|
|
if (!file || !order) return
|
|
|
|
setUploading(true)
|
|
const formData = new FormData()
|
|
formData.append('file', file)
|
|
|
|
try {
|
|
// 1. Upload file
|
|
const uploadRes = await fetch(`${API_BASE}/upload`, {
|
|
method: 'POST',
|
|
body: formData
|
|
})
|
|
const uploadData = await uploadRes.json()
|
|
|
|
if (uploadData.url) {
|
|
// 2. Update order slip
|
|
const updateRes = await fetch(`${API_BASE}/orders/${order.id}/slip`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ paymentSlipUrl: uploadData.url })
|
|
})
|
|
const updatedOrder = await updateRes.json()
|
|
setOrder(updatedOrder)
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
alert("Failed to upload payment slip.")
|
|
} finally {
|
|
setUploading(false)
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="min-h-screen bg-pop-bg">
|
|
<Navbar categories={categories} />
|
|
<div className="flex items-center justify-center pt-40">
|
|
<Loader2 className="w-8 h-8 text-spa-forest animate-spin" />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!order || (order as any).error) {
|
|
return (
|
|
<div className="min-h-screen bg-pop-bg text-center pt-40">
|
|
<Navbar categories={categories} />
|
|
<h1 className="text-2xl font-bold mb-4 text-pop-black">Order Not Found</h1>
|
|
<Link to="/shop" className="text-spa-forest hover:underline font-bold">Return to Shop</Link>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const announcementSections = cmsSections.filter(s => s.isActive && s.type.toUpperCase() === 'ANNOUNCEMENT_BAR');
|
|
const bottomSections = cmsSections.filter(s => s.isActive && s.type.toUpperCase() !== 'ANNOUNCEMENT_BAR' && s.type.toUpperCase() !== 'HERO' && s.type.toUpperCase() !== 'BANNER');
|
|
|
|
return (
|
|
<div className="min-h-screen bg-pop-bg">
|
|
{announcementSections.length > 0 && (
|
|
<DynamicPageRenderer
|
|
sections={announcementSections}
|
|
categories={categories}
|
|
categoriesLoading={false}
|
|
/>
|
|
)}
|
|
|
|
<Navbar categories={categories} />
|
|
|
|
<main className="max-w-3xl mx-auto px-6 pt-32 pb-24 space-y-12 animate-fade-in text-pop-black">
|
|
|
|
{/* Header */}
|
|
<div className="text-center space-y-6">
|
|
<div className="w-20 h-20 bg-spa-sage/30 rounded-full flex items-center justify-center mx-auto border border-spa-stone/10 shadow-sm">
|
|
<CheckCircle2 className="w-10 h-10 text-spa-forest" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-4xl font-bold text-pop-black">Order Received</h1>
|
|
<p className="text-spa-stone mt-2">Thank you! Your order #{order.id.toString().padStart(4, '0')} has been placed.</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Order Summary */}
|
|
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 space-y-6 shadow-sm">
|
|
<h2 className="text-sm font-bold uppercase tracking-widest text-spa-forest border-b border-spa-stone/10 pb-4">Order Items</h2>
|
|
<div className="space-y-4">
|
|
{order.items.map(item => (
|
|
<div key={item.id} className="flex items-center gap-4 text-sm">
|
|
<div className="w-12 h-12 rounded-lg bg-spa-stone/10 flex-shrink-0 overflow-hidden border border-spa-stone/10">
|
|
<img src={item.productImage || '/images/oil_ozone.png'} alt="Product" className="w-full h-full object-cover" />
|
|
</div>
|
|
<div className="flex-1">
|
|
<p className="font-bold text-pop-black">{item.productName}</p>
|
|
<p className="text-spa-stone/80 text-xs mt-0.5">{item.variantSize} x {item.quantity}</p>
|
|
</div>
|
|
<p className="font-bold text-rose-500">{formatPrice(item.priceAtPurchase * item.quantity)}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<div className="border-t border-spa-stone/10 pt-4 space-y-3 font-medium text-sm text-pop-black">
|
|
<div className="flex justify-between text-spa-stone">
|
|
<span>Subtotal</span>
|
|
<span>{formatPrice(order.totalAmount - order.shippingFee)}</span>
|
|
</div>
|
|
<div className="flex justify-between text-spa-stone">
|
|
<span>Shipping Fee</span>
|
|
<span>{formatPrice(order.shippingFee)}</span>
|
|
</div>
|
|
<div className="border-t border-spa-stone/10 pt-3 mt-3 flex justify-between text-lg font-bold text-pop-black">
|
|
<span>Total</span>
|
|
<span className="text-rose-500">{formatPrice(order.totalAmount)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{order.status === 'PENDING_PAYMENT' ? (
|
|
<div className="space-y-8">
|
|
{/* Payment Method Selector */}
|
|
<div className="bg-white p-4 rounded-2xl border border-spa-stone/10 shadow-sm flex flex-col md:flex-row gap-3">
|
|
<button
|
|
onClick={() => setPaymentMethod('bank')}
|
|
className={`flex-1 py-4 px-6 rounded-xl font-bold flex items-center justify-center gap-3 border transition-all ${
|
|
paymentMethod === 'bank'
|
|
? 'bg-spa-forest text-white border-spa-forest shadow-sm'
|
|
: 'bg-white text-spa-stone hover:bg-spa-light border-spa-stone/10'
|
|
}`}
|
|
>
|
|
<Landmark className="w-5 h-5" />
|
|
<span>โอนเงินธนาคาร</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setPaymentMethod('promptpay')}
|
|
className={`flex-1 py-4 px-6 rounded-xl font-bold flex items-center justify-center gap-3 border transition-all ${
|
|
paymentMethod === 'promptpay'
|
|
? 'bg-spa-forest text-white border-spa-forest shadow-sm'
|
|
: 'bg-white text-spa-stone hover:bg-spa-light border-spa-stone/10'
|
|
}`}
|
|
>
|
|
<QrCode className="w-5 h-5" />
|
|
<span>PromptPay QR</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setPaymentMethod('card')}
|
|
className={`flex-1 py-4 px-6 rounded-xl font-bold flex items-center justify-center gap-3 border transition-all ${
|
|
paymentMethod === 'card'
|
|
? 'bg-spa-forest text-white border-spa-forest shadow-sm'
|
|
: 'bg-white text-spa-stone hover:bg-spa-light border-spa-stone/10'
|
|
}`}
|
|
>
|
|
<CreditCard className="w-5 h-5" />
|
|
<span>บัตรเครดิต/เดบิต</span>
|
|
</button>
|
|
</div>
|
|
|
|
{/* Bank Transfer Layout */}
|
|
{paymentMethod === 'bank' && settings && (
|
|
<div className="space-y-8">
|
|
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 text-center space-y-6 relative overflow-hidden shadow-sm">
|
|
<div className="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-transparent via-spa-forest to-transparent opacity-50" />
|
|
<h2 className="text-xs uppercase font-bold tracking-widest text-spa-stone/80">Payment Required</h2>
|
|
<div className="text-3xl font-bold text-spa-forest">{formatPrice(order.totalAmount)}</div>
|
|
<div className="bg-spa-light p-6 rounded-xl border border-spa-stone/20 inline-block text-left w-full max-w-md mx-auto space-y-4 shadow-inner">
|
|
<div>
|
|
<p className="text-xs text-spa-stone/80 uppercase tracking-widest font-bold">Bank</p>
|
|
<p className="font-bold text-pop-black">{settings.bankName}</p>
|
|
</div>
|
|
<div className="flex items-center justify-between gap-4">
|
|
<div>
|
|
<p className="text-xs text-spa-stone/80 uppercase tracking-widest font-bold">Account Number</p>
|
|
<p className="font-mono text-xl font-bold tracking-widest text-spa-forest">{settings.bankAccountNumber}</p>
|
|
</div>
|
|
<button
|
|
onClick={copyToClipboard}
|
|
className="p-2 bg-white rounded-lg hover:bg-spa-stone/10 border border-spa-stone/20 transition-colors shrink-0 shadow-sm"
|
|
title="Copy account number"
|
|
>
|
|
{copied ? <CheckCircle2 className="w-5 h-5 text-emerald-500" /> : <Copy className="w-5 h-5 text-spa-stone/80" />}
|
|
</button>
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-spa-stone/80 uppercase tracking-widest font-bold">Account Name</p>
|
|
<p className="font-bold text-pop-black">{settings.bankAccountName}</p>
|
|
</div>
|
|
</div>
|
|
<p className="text-xs text-spa-stone font-medium">Please transfer the exact amount and upload your slip below.</p>
|
|
</div>
|
|
|
|
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 space-y-6 shadow-sm">
|
|
<h2 className="text-sm font-bold uppercase tracking-widest text-spa-forest border-b border-spa-stone/10 pb-4">Payment Slip Verification</h2>
|
|
{order.paymentSlipUrl ? (
|
|
<div className="flex flex-col items-center justify-center space-y-4 py-4">
|
|
<div className="w-16 h-16 bg-emerald-500/10 rounded-full flex items-center justify-center border border-emerald-500/20">
|
|
<CheckCircle2 className="w-8 h-8 text-emerald-500" />
|
|
</div>
|
|
<div className="text-center">
|
|
<p className="font-bold text-lg text-emerald-600">Slip Uploaded Successfully</p>
|
|
<p className="text-sm text-spa-stone mt-1 font-medium">Status: {order.status}</p>
|
|
<p className="text-xs mt-2 text-spa-stone/80">We will verify your payment and process your order shortly.</p>
|
|
</div>
|
|
<img src={order.paymentSlipUrl} alt="Slip" className="w-48 rounded-xl border border-spa-stone/20 mt-4 opacity-80 hover:opacity-100 transition-opacity shadow-sm" />
|
|
</div>
|
|
) : (
|
|
<div className="relative border-2 border-dashed border-spa-stone/30 bg-spa-light rounded-xl p-10 hover:border-spa-forest transition-colors group cursor-pointer overflow-hidden">
|
|
<input
|
|
type="file"
|
|
accept="image/*"
|
|
onChange={handleFileUpload}
|
|
disabled={uploading}
|
|
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-not-allowed"
|
|
/>
|
|
<div className="text-center space-y-4">
|
|
{uploading ? (
|
|
<Loader2 className="w-12 h-12 text-spa-forest animate-spin mx-auto" />
|
|
) : (
|
|
<UploadCloud className="w-12 h-12 text-spa-stone/60 group-hover:text-spa-forest transition-colors mx-auto" />
|
|
)}
|
|
<div>
|
|
<p className="font-bold text-pop-black">{uploading ? 'Uploading...' : 'Click or drag to upload slip'}</p>
|
|
<p className="text-xs text-spa-stone font-medium mt-1">Supports JPG, PNG (Max 5MB)</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* PromptPay Layout */}
|
|
{paymentMethod === 'promptpay' && (
|
|
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 text-center space-y-6 relative overflow-hidden shadow-sm">
|
|
<div className="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-transparent via-spa-forest to-transparent opacity-50" />
|
|
<h2 className="text-xs uppercase font-bold tracking-widest text-spa-stone/80">PromptPay QR Code</h2>
|
|
<div className="text-3xl font-bold text-spa-forest">{formatPrice(order.totalAmount)}</div>
|
|
|
|
{!qrCodeUrl ? (
|
|
<div className="py-8">
|
|
<button
|
|
onClick={handleGeneratePromptPay}
|
|
disabled={generatingQr}
|
|
className="px-8 py-4 bg-spa-forest text-white font-bold rounded-xl tracking-widest uppercase hover:bg-spa-stone transition-all duration-300 disabled:opacity-50 flex items-center justify-center gap-3 mx-auto"
|
|
>
|
|
{generatingQr ? <Loader2 className="w-5 h-5 animate-spin" /> : <QrCode className="w-5 h-5" />}
|
|
<span>สร้างคิวอาร์โค้ด PromptPay</span>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6 max-w-sm mx-auto">
|
|
<div className="bg-white p-4 rounded-xl border border-spa-stone/20 inline-block shadow-sm">
|
|
<img src={qrCodeUrl} alt="PromptPay QR Code" className="w-64 h-64 object-contain mx-auto" />
|
|
</div>
|
|
|
|
<div className="space-y-2 text-xs text-spa-stone font-medium">
|
|
<p className="text-sm font-bold text-pop-black">สแกนจ่ายเงินเพื่อเสร็จสิ้นออเดอร์</p>
|
|
<p>1. กดบันทึกรูปภาพคิวอาร์โค้ดลงในมือถือของคุณ</p>
|
|
<p>2. เปิดแอป Mobile Banking และเลือกสแกนภาพจากอัลบั้ม</p>
|
|
<p className="text-[10px] text-rose-500 uppercase font-bold tracking-wider animate-pulse mt-4">
|
|
{polling ? "🔄 กำลังตรวจสอบยอดชำระเงินโดยอัตโนมัติ..." : "รอการจ่ายเงิน..."}
|
|
</p>
|
|
</div>
|
|
|
|
<a
|
|
href={qrCodeUrl}
|
|
download={`promptpay-order-${order.id}.png`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="inline-flex items-center gap-2 text-xs text-spa-forest hover:underline font-bold"
|
|
>
|
|
<UploadCloud className="w-4 h-4 transform rotate-180" />
|
|
<span>ดาวน์โหลดรูปภาพ QR Code</span>
|
|
</a>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Credit Card Layout */}
|
|
{paymentMethod === 'card' && (
|
|
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 space-y-6 relative overflow-hidden shadow-sm">
|
|
<div className="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-transparent via-spa-forest to-transparent opacity-50" />
|
|
<div className="text-center space-y-2">
|
|
<h2 className="text-xs uppercase font-bold tracking-widest text-spa-stone/80">Credit / Debit Card</h2>
|
|
<div className="text-3xl font-bold text-spa-forest">{formatPrice(order.totalAmount)}</div>
|
|
</div>
|
|
|
|
<form onSubmit={handleCardPay} className="max-w-md mx-auto space-y-5">
|
|
{cardError && (
|
|
<div className="p-4 bg-rose-500/10 border border-rose-500/20 text-rose-500 font-medium rounded-lg text-sm text-center">
|
|
{cardError}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">ชื่อบนบัตร (Cardholder Name)</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={cardName}
|
|
onChange={e => setCardName(e.target.value)}
|
|
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3 text-pop-black focus:border-spa-forest outline-none transition-all placeholder:text-spa-stone/30"
|
|
placeholder="Jane Doe"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">หมายเลขบัตร (Card Number)</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
maxLength={19}
|
|
value={cardNumber}
|
|
onChange={e => {
|
|
const val = e.target.value.replace(/\D/g, '').replace(/(.{4})/g, '$1 ').trim();
|
|
setCardNumber(val);
|
|
}}
|
|
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3 text-pop-black focus:border-spa-forest outline-none transition-all placeholder:text-spa-stone/30"
|
|
placeholder="4242 4242 4242 4242"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">วันหมดอายุ (Expiry Date)</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
maxLength={5}
|
|
value={cardExpiry}
|
|
onChange={e => {
|
|
const val = e.target.value.replace(/\D/g, '').replace(/(\d{2})(\d{1,2})/, '$1/$2').trim();
|
|
setCardExpiry(val);
|
|
}}
|
|
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3 text-pop-black focus:border-spa-forest outline-none transition-all placeholder:text-spa-stone/30"
|
|
placeholder="MM/YY"
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">รหัส CVV / CVC</label>
|
|
<input
|
|
type="password"
|
|
required
|
|
maxLength={4}
|
|
value={cardCvv}
|
|
onChange={e => setCardCvv(e.target.value.replace(/\D/g, ''))}
|
|
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3 text-pop-black focus:border-spa-forest outline-none transition-all placeholder:text-spa-stone/30"
|
|
placeholder="123"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="submit"
|
|
disabled={payingCard}
|
|
className="w-full py-4 bg-spa-forest text-white font-bold rounded-lg tracking-widest uppercase hover:bg-spa-stone transition-all duration-300 disabled:opacity-50 flex items-center justify-center gap-2 mt-4"
|
|
>
|
|
{payingCard ? <Loader2 className="w-5 h-5 animate-spin" /> : <CreditCard className="w-5 h-5" />}
|
|
<span>ชำระเงิน {formatPrice(order.totalAmount)}</span>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
)}
|
|
</div>
|
|
) : (
|
|
<div className="bg-white p-8 rounded-2xl border border-spa-stone/10 text-center space-y-6 shadow-sm">
|
|
<div className="w-16 h-16 bg-emerald-500/10 rounded-full flex items-center justify-center border border-emerald-500/20 mx-auto">
|
|
<CheckCircle2 className="w-8 h-8 text-emerald-500" />
|
|
</div>
|
|
<div className="text-center space-y-2">
|
|
<p className="font-bold text-2xl text-emerald-600">ชำระเงินเสร็จสิ้น</p>
|
|
<p className="text-sm text-spa-stone font-medium">ทางแบรนด์ได้รับยอดชำระของคุณเรียบร้อย ออเดอร์กำลังอยู่ระหว่างจัดเตรียมสินค้า</p>
|
|
<p className="text-xs text-spa-stone/60">ออเดอร์สถานะปัจจุบัน: {order.status}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</main>
|
|
|
|
{bottomSections.length > 0 && (
|
|
<DynamicPageRenderer
|
|
sections={bottomSections}
|
|
categories={categories}
|
|
categoriesLoading={false}
|
|
/>
|
|
)}
|
|
|
|
<Footer categories={categories} />
|
|
</div>
|
|
)
|
|
}
|