Rename ssr to website, website to spa, add spa nginx config

This commit is contained in:
Your Name committed 2026-06-03 09:38:15 +07:00
1 parent fc6bf0218e
commit 8514342c2f
103 files changed
+801 -768

No files matched your search

+227
View File
@@ -0,0 +1,227 @@
import { useState, useEffect } from 'react'
import { useParams, Link } from 'react-router-dom'
import Navbar from '../components/Navbar'
import Footer from '../components/Footer'
import { API_BASE, formatPrice, type Order, type StoreSettings, type Category } from '../types'
import { CheckCircle2, UploadCloud, Loader2, Copy } 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)
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())
])
.then(([orderData, settingsData, catData]) => {
setOrder(orderData)
setSettings(settingsData)
setCategories(catData)
})
.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">
<Navbar categories={categories} />
<div className="flex items-center justify-center pt-40">
<Loader2 className="w-8 h-8 text-cyan-glow animate-spin" />
</div>
</div>
)
}
if (!order || (order as any).error) {
return (
<div className="min-h-screen text-center pt-40">
<Navbar categories={categories} />
<h1 className="text-2xl font-bold mb-4">Order Not Found</h1>
<Link to="/shop" className="text-cyan-glow hover:underline">Return to Shop</Link>
</div>
)
}
return (
<div className="min-h-screen">
<Navbar categories={categories} />
<main className="max-w-3xl mx-auto px-6 pt-32 pb-24 space-y-12 animate-fade-in">
{/* Header */}
<div className="text-center space-y-6">
<div className="w-20 h-20 bg-cyan-glow/10 rounded-full flex items-center justify-center mx-auto border border-cyan-glow/20">
<CheckCircle2 className="w-10 h-10 text-cyan-glow" />
</div>
<div>
<h1 className="text-4xl font-bold">Order Received</h1>
<p className="text-slate-400 mt-2">Thank you! Your order #{order.id.toString().padStart(4, '0')} has been placed.</p>
</div>
</div>
{/* Order Summary */}
<div className="bg-night-900/50 p-8 rounded-2xl border border-white/5 space-y-6">
<h2 className="text-sm font-bold uppercase tracking-widest text-cyan-glow border-b border-white/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-white/5 flex-shrink-0 overflow-hidden">
<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">{item.productName}</p>
<p className="text-slate-400 text-xs mt-0.5">{item.variantSize} x {item.quantity}</p>
</div>
<p className="font-medium text-cyan-glow">{formatPrice(item.priceAtPurchase * item.quantity)}</p>
</div>
))}
</div>
<div className="border-t border-white/10 pt-4 space-y-3 font-medium text-sm text-slate-300">
<div className="flex justify-between">
<span>Subtotal</span>
<span>{formatPrice(order.totalAmount - order.shippingFee)}</span>
</div>
<div className="flex justify-between">
<span>Shipping Fee</span>
<span>{formatPrice(order.shippingFee)}</span>
</div>
<div className="border-t border-white/10 pt-3 mt-3 flex justify-between text-lg font-bold text-white">
<span>Total</span>
<span className="text-cyan-glow">{formatPrice(order.totalAmount)}</span>
</div>
</div>
</div>
{/* Banking Details */}
{settings && order.status === 'PENDING_PAYMENT' && (
<div className="bg-night-900/50 p-8 rounded-2xl border border-white/5 text-center space-y-6 relative overflow-hidden">
<div className="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-transparent via-cyan-glow to-transparent opacity-50" />
<h2 className="text-xs uppercase font-bold tracking-widest text-slate-400">Payment Required</h2>
<div className="text-3xl font-bold text-cyan-glow">
{formatPrice(order.totalAmount)}
</div>
<div className="bg-night-950 p-6 rounded-xl border border-white/5 inline-block text-left w-full max-w-md mx-auto space-y-4">
<div>
<p className="text-xs text-slate-500 uppercase tracking-widest">Bank</p>
<p className="font-bold">{settings.bankName}</p>
</div>
<div className="flex items-center justify-between gap-4">
<div>
<p className="text-xs text-slate-500 uppercase tracking-widest">Account Number</p>
<p className="font-mono text-xl font-bold tracking-widest text-cyan-glow">{settings.bankAccountNumber}</p>
</div>
<button
onClick={copyToClipboard}
className="p-2 glass rounded-lg hover:bg-white/10 transition-colors shrink-0"
title="Copy account number"
>
{copied ? <CheckCircle2 className="w-5 h-5 text-emerald-400" /> : <Copy className="w-5 h-5 text-slate-300" />}
</button>
</div>
<div>
<p className="text-xs text-slate-500 uppercase tracking-widest">Account Name</p>
<p className="font-bold">{settings.bankAccountName}</p>
</div>
</div>
<p className="text-xs text-slate-500">Please transfer the exact amount and upload your slip below.</p>
</div>
)}
{/* Upload Status */}
<div className="bg-night-900/50 p-8 rounded-2xl border border-white/5 space-y-6">
<h2 className="text-sm font-bold uppercase tracking-widest text-cyan-glow border-b border-white/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">
<CheckCircle2 className="w-8 h-8 text-emerald-400" />
</div>
<div className="text-center">
<p className="font-bold text-lg text-emerald-400">Slip Uploaded Successfully</p>
<p className="text-sm text-slate-400 mt-1">Status: {order.status}</p>
<p className="text-xs mt-2 text-slate-500">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-white/10 mt-4 opacity-50 hover:opacity-100 transition-opacity" />
</div>
) : (
<div className="relative border-2 border-dashed border-white/10 rounded-xl p-10 hover:border-cyan-glow/50 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-cyan-glow animate-spin mx-auto" />
) : (
<UploadCloud className="w-12 h-12 text-slate-500 group-hover:text-cyan-glow transition-colors mx-auto" />
)}
<div>
<p className="font-bold">{uploading ? 'Uploading...' : 'Click or drag to upload slip'}</p>
<p className="text-xs text-slate-500 mt-1">Supports JPG, PNG (Max 5MB)</p>
</div>
</div>
</div>
)}
</div>
</main>
<Footer categories={categories} />
</div>
)
}