788 lines
37 KiB
TypeScript
788 lines
37 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { useTranslation } from 'react-i18next'
|
|
import Navbar from '../components/Navbar'
|
|
import Footer from '../components/Footer'
|
|
import { API_BASE, formatPrice, type Category, type Order, type CustomerAddress } from '../types'
|
|
import { useAuthStore } from '../store/authStore'
|
|
import {
|
|
User,
|
|
MapPin,
|
|
Package,
|
|
Settings,
|
|
Plus,
|
|
Edit3,
|
|
Trash2,
|
|
Check,
|
|
Loader2,
|
|
ExternalLink,
|
|
Phone,
|
|
Mail,
|
|
Award
|
|
} from 'lucide-react'
|
|
import { ThaiAddressInput } from '../components/ui/ThaiAddressInput'
|
|
|
|
export default function ProfilePage() {
|
|
const { token, customer, setAuth } = useAuthStore()
|
|
const { i18n } = useTranslation()
|
|
const [categories, setCategories] = useState<Category[]>([])
|
|
|
|
// Tab State
|
|
const [activeTab, setActiveTab] = useState<'orders' | 'addresses' | 'settings'>('orders')
|
|
|
|
// Data States
|
|
const [dbCustomer, setDbCustomer] = useState<any>(null)
|
|
const [orders, setOrders] = useState<Order[]>([])
|
|
const [addresses, setAddresses] = useState<CustomerAddress[]>([])
|
|
|
|
// Loading States
|
|
const [loadingOrders, setLoadingOrders] = useState(false)
|
|
const [loadingAddresses, setLoadingAddresses] = useState(false)
|
|
|
|
// Address Form States
|
|
const [showAddressForm, setShowAddressForm] = useState(false)
|
|
const [editingAddress, setEditingAddress] = useState<CustomerAddress | null>(null)
|
|
|
|
const [recipientName, setRecipientName] = useState('')
|
|
const [phone, setPhone] = useState('')
|
|
const [addressLine, setAddressLine] = useState('')
|
|
const [subdistrict, setSubdistrict] = useState('')
|
|
const [district, setDistrict] = useState('')
|
|
const [province, setProvince] = useState('')
|
|
const [zipcode, setZipcode] = useState('')
|
|
const [isDefault, setIsDefault] = useState(false)
|
|
const [submittingAddress, setSubmittingAddress] = useState(false)
|
|
|
|
// Settings Form States
|
|
const [emailInput, setEmailInput] = useState('')
|
|
const [phoneInput, setPhoneInput] = useState('')
|
|
const [submittingSettings, setSubmittingSettings] = useState(false)
|
|
const [settingsSuccess, setSettingsSuccess] = useState(false)
|
|
|
|
// Fetch Categories
|
|
useEffect(() => {
|
|
fetch(`${API_BASE}/categories?lang=${i18n.language}`)
|
|
.then(res => res.json())
|
|
.then(setCategories)
|
|
.catch(console.error)
|
|
}, [i18n.language])
|
|
|
|
// Fetch Customer Profile details & Points dynamically
|
|
const fetchProfile = () => {
|
|
if (!customer) return
|
|
fetch(`${API_BASE}/profile/${customer.id}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data && !data.error) {
|
|
setDbCustomer(data)
|
|
setEmailInput(data.email || '')
|
|
setPhoneInput(data.phone || '')
|
|
}
|
|
})
|
|
.catch(console.error)
|
|
}
|
|
|
|
// Fetch Orders
|
|
const fetchOrders = () => {
|
|
if (!customer) return
|
|
setLoadingOrders(true)
|
|
fetch(`${API_BASE}/orders/customer/${customer.id}`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (Array.isArray(data)) {
|
|
setOrders(data)
|
|
}
|
|
})
|
|
.catch(console.error)
|
|
.finally(() => setLoadingOrders(false))
|
|
}
|
|
|
|
// Fetch Addresses
|
|
const fetchAddresses = () => {
|
|
if (!customer) return
|
|
setLoadingAddresses(true)
|
|
fetch(`${API_BASE}/profile/${customer.id}/addresses`)
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (Array.isArray(data)) {
|
|
setAddresses(data)
|
|
}
|
|
})
|
|
.catch(console.error)
|
|
.finally(() => setLoadingAddresses(false))
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (customer) {
|
|
fetchProfile()
|
|
fetchOrders()
|
|
fetchAddresses()
|
|
}
|
|
}, [customer])
|
|
|
|
// Reset Address Form
|
|
const resetAddressForm = () => {
|
|
setRecipientName('')
|
|
setPhone('')
|
|
setAddressLine('')
|
|
setSubdistrict('')
|
|
setDistrict('')
|
|
setProvince('')
|
|
setZipcode('')
|
|
setIsDefault(false)
|
|
setEditingAddress(null)
|
|
setShowAddressForm(false)
|
|
}
|
|
|
|
// Handle Edit Address Click
|
|
const startEditAddress = (addr: CustomerAddress) => {
|
|
setEditingAddress(addr)
|
|
setRecipientName(addr.recipientName)
|
|
setPhone(addr.phone)
|
|
setAddressLine(addr.addressLine)
|
|
setSubdistrict(addr.subdistrict)
|
|
setDistrict(addr.district)
|
|
setProvince(addr.province)
|
|
setZipcode(addr.zipcode)
|
|
setIsDefault(addr.isDefault)
|
|
setShowAddressForm(true)
|
|
}
|
|
|
|
// Handle Add/Edit Address Submission
|
|
const handleAddressSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!customer) return
|
|
|
|
setSubmittingAddress(true)
|
|
const payload = {
|
|
recipientName,
|
|
phone,
|
|
addressLine,
|
|
subdistrict,
|
|
district,
|
|
province,
|
|
zipcode,
|
|
isDefault
|
|
}
|
|
|
|
try {
|
|
const url = editingAddress
|
|
? `${API_BASE}/profile/${customer.id}/addresses/${editingAddress.id}`
|
|
: `${API_BASE}/profile/${customer.id}/addresses`
|
|
const method = editingAddress ? 'PUT' : 'POST'
|
|
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
})
|
|
|
|
if (res.ok) {
|
|
resetAddressForm()
|
|
fetchAddresses()
|
|
} else {
|
|
const err = await res.json()
|
|
alert(err.error || 'Failed to save address')
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
alert('Failed to connect to server')
|
|
} finally {
|
|
setSubmittingAddress(false)
|
|
}
|
|
}
|
|
|
|
// Handle Delete Address
|
|
const handleDeleteAddress = async (addrId: number) => {
|
|
if (!customer) return
|
|
if (!confirm('ยืนยันที่จะลบที่อยู่จัดส่งนี้?')) return
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/profile/${customer.id}/addresses/${addrId}`, {
|
|
method: 'DELETE'
|
|
})
|
|
if (res.ok) {
|
|
fetchAddresses()
|
|
} else {
|
|
alert('Failed to delete address')
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
}
|
|
}
|
|
|
|
// Handle Set Default Address
|
|
const handleSetDefault = async (addrId: number) => {
|
|
if (!customer) return
|
|
try {
|
|
const res = await fetch(`${API_BASE}/profile/${customer.id}/addresses/${addrId}/default`, {
|
|
method: 'PATCH'
|
|
})
|
|
if (res.ok) {
|
|
fetchAddresses()
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
}
|
|
}
|
|
|
|
// Handle Settings Submit (Update email & phone)
|
|
const handleSettingsSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!customer) return
|
|
|
|
setSubmittingSettings(true)
|
|
setSettingsSuccess(false)
|
|
|
|
try {
|
|
const res = await fetch(`${API_BASE}/profile/${customer.id}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
email: emailInput || null,
|
|
phone: phoneInput || null
|
|
})
|
|
})
|
|
|
|
if (res.ok) {
|
|
const updatedCustomer = await res.json()
|
|
setDbCustomer(updatedCustomer)
|
|
// Update store
|
|
if (token) {
|
|
setAuth(token, updatedCustomer)
|
|
}
|
|
setSettingsSuccess(true)
|
|
setTimeout(() => setSettingsSuccess(false), 3000)
|
|
} else {
|
|
const err = await res.json()
|
|
alert(err.error || 'Failed to update profile')
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
alert('Failed to connect to server')
|
|
} finally {
|
|
setSubmittingSettings(false)
|
|
}
|
|
}
|
|
|
|
const getOrderStatusColor = (status: string) => {
|
|
switch (status.toUpperCase()) {
|
|
case 'PAID':
|
|
return 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20'
|
|
case 'PENDING_PAYMENT':
|
|
return 'bg-amber-500/10 text-amber-600 border-amber-500/20'
|
|
case 'SHIPPED':
|
|
return 'bg-blue-500/10 text-blue-600 border-blue-500/20'
|
|
case 'CANCELLED':
|
|
return 'bg-rose-500/10 text-rose-600 border-rose-500/20'
|
|
default:
|
|
return 'bg-spa-stone/10 text-spa-stone border-spa-stone/20'
|
|
}
|
|
}
|
|
|
|
const translateStatus = (status: string) => {
|
|
switch (status.toUpperCase()) {
|
|
case 'PAID':
|
|
return 'ชำระเงินสำเร็จ'
|
|
case 'PENDING_PAYMENT':
|
|
return 'รอการชำระเงิน'
|
|
case 'SHIPPED':
|
|
return 'จัดส่งแล้ว'
|
|
case 'CANCELLED':
|
|
return 'ยกเลิกแล้ว'
|
|
default:
|
|
return status
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-pop-bg">
|
|
<Navbar categories={categories} />
|
|
|
|
<main className="max-w-5xl mx-auto px-6 pt-32 pb-24 text-pop-black">
|
|
|
|
{!customer ? (
|
|
/* Login CTA screen */
|
|
<div className="max-w-md mx-auto text-center space-y-8 py-20 animate-fade-in bg-white p-10 rounded-3xl border border-spa-stone/10 shadow-sm mt-10">
|
|
<div className="w-20 h-20 bg-spa-light rounded-full flex items-center justify-center mx-auto border border-spa-stone/10 shadow-inner">
|
|
<User className="w-10 h-10 text-spa-stone" />
|
|
</div>
|
|
<div className="space-y-3">
|
|
<h1 className="text-3xl font-bold text-pop-black">เข้าสู่ระบบสมาชิก</h1>
|
|
<p className="text-spa-stone text-sm">ล็อกอินเพื่อตรวจสอบประวัติคำสั่งซื้อ สะสมคะแนนสมาชิก และบันทึกสมุดที่อยู่จัดส่งสินค้า</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
const liff = (window as any).liff
|
|
if (liff) {
|
|
liff.login({ redirectUri: window.location.href })
|
|
} else {
|
|
alert('LINE LIFF System is initializing. Please refresh.')
|
|
}
|
|
}}
|
|
className="w-full py-4 bg-spa-forest hover:bg-spa-stone text-white font-bold rounded-xl tracking-widest uppercase transition-all duration-300 shadow-sm flex items-center justify-center gap-3"
|
|
>
|
|
<Award className="w-5 h-5" />
|
|
<span>เข้าสู่ระบบผ่าน LINE</span>
|
|
</button>
|
|
<Link to="/shop" className="block text-xs text-spa-stone hover:underline font-bold">กลับไปเลือกซื้อสินค้า</Link>
|
|
</div>
|
|
) : (
|
|
/* Logged In Dashboard Layout */
|
|
<div className="space-y-10 animate-fade-in">
|
|
{/* Header Profile Banner */}
|
|
<div className="bg-white rounded-2xl border border-spa-stone/10 p-5 sm:p-6 flex flex-col sm:flex-row justify-between items-center sm:items-center gap-4 shadow-sm relative overflow-hidden">
|
|
<div className="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-spa-sage via-spa-forest to-spa-stone opacity-40" />
|
|
|
|
{/* User Avatar & Info */}
|
|
<div className="flex items-center gap-4 w-full sm:w-auto">
|
|
<div className="w-14 h-14 sm:w-16 sm:h-16 rounded-full overflow-hidden border border-spa-forest/20 shadow-sm shrink-0">
|
|
<img
|
|
src={customer.pictureUrl || `https://ui-avatars.com/api/?name=${customer.displayName}`}
|
|
alt="LINE Profile"
|
|
className="w-full h-full object-cover"
|
|
/>
|
|
</div>
|
|
<div className="space-y-1 min-w-0">
|
|
<h1 className="text-xl sm:text-2xl font-bold text-pop-black truncate">{customer.displayName}</h1>
|
|
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] font-semibold text-spa-stone">
|
|
{dbCustomer?.phone && (
|
|
<span className="flex items-center gap-1">
|
|
<Phone className="w-3 h-3 text-spa-stone/70" />
|
|
<span>{dbCustomer.phone}</span>
|
|
</span>
|
|
)}
|
|
{dbCustomer?.email && (
|
|
<span className="flex items-center gap-1">
|
|
<Mail className="w-3 h-3 text-spa-stone/70" />
|
|
<span>{dbCustomer.email}</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Point Card Compact */}
|
|
<div className="bg-spa-light border border-spa-stone/10 rounded-xl px-4 py-3 flex items-center justify-between gap-4 w-full sm:w-auto min-w-[200px] shrink-0">
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="w-8 h-8 rounded-full bg-spa-forest/10 flex items-center justify-center shrink-0">
|
|
<Award className="w-4.5 h-4.5 text-spa-forest" />
|
|
</div>
|
|
<div className="text-left">
|
|
<p className="text-[9px] uppercase font-bold tracking-wider text-spa-stone">คะแนนสะสมของคุณ</p>
|
|
<p className="text-lg font-extrabold text-pop-black leading-none mt-0.5">
|
|
{dbCustomer?.points ?? customer.points ?? 0} <span className="text-[10px] font-semibold text-spa-stone ml-0.5">คะแนน</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dashboard Horizontal Tabs & Content */}
|
|
<div className="space-y-6">
|
|
|
|
{/* Horizontal Tab UI */}
|
|
<div className="border-b border-spa-stone/10 flex justify-between sm:justify-start gap-2 sm:gap-8 overflow-x-auto no-scrollbar">
|
|
<button
|
|
onClick={() => setActiveTab('orders')}
|
|
className={`pb-3 px-2 text-xs sm:text-sm font-bold flex items-center justify-center gap-2 transition-all relative shrink-0 ${
|
|
activeTab === 'orders'
|
|
? 'text-spa-forest'
|
|
: 'text-spa-stone hover:text-pop-black'
|
|
}`}
|
|
>
|
|
<Package className="w-4 h-4" />
|
|
<span className="hidden sm:inline">ออเดอร์</span>
|
|
<span className={`text-[9px] px-1.5 py-0.5 rounded-full font-extrabold transition-colors ${
|
|
activeTab === 'orders'
|
|
? 'bg-spa-forest/10 text-spa-forest'
|
|
: 'bg-spa-stone/10 text-spa-stone'
|
|
}`}>
|
|
{orders.length}
|
|
</span>
|
|
{activeTab === 'orders' && (
|
|
<div className="absolute bottom-0 inset-x-0 h-0.5 bg-spa-forest rounded-full" />
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('addresses')}
|
|
className={`pb-3 px-2 text-xs sm:text-sm font-bold flex items-center justify-center gap-2 transition-all relative shrink-0 ${
|
|
activeTab === 'addresses'
|
|
? 'text-spa-forest'
|
|
: 'text-spa-stone hover:text-pop-black'
|
|
}`}
|
|
>
|
|
<MapPin className="w-4 h-4" />
|
|
<span className="hidden sm:inline">ที่อยู่</span>
|
|
<span className={`text-[9px] px-1.5 py-0.5 rounded-full font-extrabold transition-colors ${
|
|
activeTab === 'addresses'
|
|
? 'bg-spa-forest/10 text-spa-forest'
|
|
: 'bg-spa-stone/10 text-spa-stone'
|
|
}`}>
|
|
{addresses.length}
|
|
</span>
|
|
{activeTab === 'addresses' && (
|
|
<div className="absolute bottom-0 inset-x-0 h-0.5 bg-spa-forest rounded-full" />
|
|
)}
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => setActiveTab('settings')}
|
|
className={`pb-3 px-2 text-xs sm:text-sm font-bold flex items-center justify-center gap-2 transition-all relative shrink-0 ${
|
|
activeTab === 'settings'
|
|
? 'text-spa-forest'
|
|
: 'text-spa-stone hover:text-pop-black'
|
|
}`}
|
|
>
|
|
<Settings className="w-4 h-4" />
|
|
<span className="hidden sm:inline">ตั้งค่า</span>
|
|
{activeTab === 'settings' && (
|
|
<div className="absolute bottom-0 inset-x-0 h-0.5 bg-spa-forest rounded-full" />
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tab Display Area */}
|
|
<div className="space-y-8">
|
|
|
|
{/* 1. ORDERS TAB */}
|
|
{activeTab === 'orders' && (
|
|
<div className="space-y-6">
|
|
<h2 className="text-xl font-bold border-b border-spa-stone/10 pb-4 flex items-center gap-2">
|
|
<Package className="w-5 h-5 text-spa-forest" />
|
|
<span>ประวัติคำสั่งซื้อทั้งหมด</span>
|
|
</h2>
|
|
|
|
{loadingOrders ? (
|
|
<div className="flex items-center justify-center py-20">
|
|
<Loader2 className="w-8 h-8 text-spa-forest animate-spin" />
|
|
</div>
|
|
) : orders.length === 0 ? (
|
|
<div className="text-center py-16 bg-white rounded-2xl border border-spa-stone/10 space-y-4 shadow-sm">
|
|
<Package className="w-12 h-12 text-spa-stone/40 mx-auto" />
|
|
<div>
|
|
<p className="font-bold text-pop-black">ไม่มีประวัติการสั่งซื้อ</p>
|
|
<p className="text-xs text-spa-stone font-medium">คุณยังไม่ได้ทำการสั่งซื้อสินค้าใดๆ กับแบรนด์ของเรา</p>
|
|
</div>
|
|
<Link
|
|
to="/shop"
|
|
className="inline-block px-6 py-2.5 bg-spa-forest hover:bg-spa-stone text-white font-bold rounded-lg text-xs uppercase tracking-wider transition-colors shadow-sm"
|
|
>
|
|
ไปหน้าช้อปปิ้ง
|
|
</Link>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
{orders.map(order => (
|
|
<div
|
|
key={order.id}
|
|
className="bg-white border border-spa-stone/10 rounded-2xl p-6 shadow-sm hover:border-spa-stone transition-all duration-300 space-y-4"
|
|
>
|
|
{/* Card Header */}
|
|
<div className="flex flex-col md:flex-row justify-between md:items-center gap-3 border-b border-spa-stone/10 pb-4">
|
|
<div>
|
|
<p className="text-xs text-spa-stone font-semibold">เลขออเดอร์ #{order.id.toString().padStart(4, '0')}</p>
|
|
<p className="text-[10px] text-spa-stone/85 mt-0.5">สั่งเมื่อ {new Date(order.createdAt).toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' })}</p>
|
|
</div>
|
|
<div className="flex items-center gap-3">
|
|
<span className={`px-3 py-1 rounded-full text-xs font-bold border ${getOrderStatusColor(order.status)}`}>
|
|
{translateStatus(order.status)}
|
|
</span>
|
|
<Link
|
|
to={`/order/${order.id}`}
|
|
className="p-1.5 hover:bg-spa-light rounded-lg border border-spa-stone/10 transition-colors"
|
|
title="ดูหน้าออเดอร์สำเร็จ"
|
|
>
|
|
<ExternalLink className="w-4 h-4 text-spa-stone" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Card Items */}
|
|
<div className="space-y-3">
|
|
{order.items.map(item => (
|
|
<div key={item.id} className="flex items-center gap-3 text-xs">
|
|
<div className="w-10 h-10 rounded-md bg-spa-stone/10 overflow-hidden border border-spa-stone/15 flex-shrink-0">
|
|
<img src={item.productImage || `${import.meta.env.VITE_API_URL.replace('/api', '')}/assets/oil_ozone.png`} alt="Product" className="w-full h-full object-cover" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<p className="font-bold text-pop-black truncate">{item.productName}</p>
|
|
<p className="text-[10px] text-spa-stone/80 mt-0.5">{item.variantSize} x {item.quantity}</p>
|
|
</div>
|
|
<p className="font-bold text-rose-500 shrink-0">{formatPrice(item.priceAtPurchase * item.quantity)}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Card Footer */}
|
|
<div className="border-t border-spa-stone/10 pt-4 flex flex-col md:flex-row justify-between items-start md:items-center gap-3 text-xs">
|
|
<div>
|
|
{order.trackingNo && (
|
|
<p className="font-semibold text-spa-stone">
|
|
เลขแทรกพัสดุ: <span className="font-mono text-spa-forest bg-spa-sage/15 px-2 py-0.5 rounded font-extrabold">{order.trackingNo}</span>
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center justify-between w-full md:w-auto gap-4 self-stretch md:self-auto border-t md:border-0 border-spa-stone/10 pt-3 md:pt-0">
|
|
<span className="text-spa-stone font-semibold">ยอดรวมสุทธิ:</span>
|
|
<span className="text-base font-extrabold text-rose-500">{formatPrice(order.totalAmount)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 2. ADDRESSES TAB */}
|
|
{activeTab === 'addresses' && (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center border-b border-spa-stone/10 pb-4 gap-4">
|
|
<h2 className="text-xl font-bold flex items-center gap-2">
|
|
<MapPin className="w-5 h-5 text-spa-forest" />
|
|
<span>สมุดที่อยู่จัดส่งของฉัน</span>
|
|
</h2>
|
|
{!showAddressForm && (
|
|
<button
|
|
onClick={() => {
|
|
resetAddressForm()
|
|
setShowAddressForm(true)
|
|
}}
|
|
className="px-4 py-2 bg-spa-forest hover:bg-spa-stone text-white font-bold rounded-lg text-xs uppercase tracking-wider transition-colors shadow-sm flex items-center gap-2"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
<span>เพิ่มที่อยู่ใหม่</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Address Creation/Editing Form */}
|
|
{showAddressForm && (
|
|
<form onSubmit={handleAddressSubmit} className="bg-white border border-spa-stone/15 p-6 rounded-2xl shadow-sm space-y-5 animate-slide-down">
|
|
<h3 className="text-sm font-bold uppercase tracking-widest text-spa-forest border-b border-spa-stone/10 pb-2">
|
|
{editingAddress ? 'แก้ไขที่อยู่จัดส่ง' : 'เพิ่มที่อยู่จัดส่งใหม่'}
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">ชื่อผู้รับ</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={recipientName}
|
|
onChange={e => setRecipientName(e.target.value)}
|
|
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all"
|
|
placeholder="เช่น สมชาย ใจดี"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">เบอร์โทรศัพท์ผู้รับ</label>
|
|
<input
|
|
type="tel"
|
|
required
|
|
value={phone}
|
|
onChange={e => setPhone(e.target.value)}
|
|
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all"
|
|
placeholder="เช่น 0812345678"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">ที่อยู่ (บ้านเลขที่, หมู่บ้าน, ถนน, ซอย)</label>
|
|
<input
|
|
type="text"
|
|
required
|
|
value={addressLine}
|
|
onChange={e => setAddressLine(e.target.value)}
|
|
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all"
|
|
placeholder="เช่น 99/9 หมู่ 9 ซอยสุขุมวิท 23"
|
|
/>
|
|
</div>
|
|
|
|
<ThaiAddressInput
|
|
value={{ subdistrict, district, province, zipcode }}
|
|
onChange={(val) => {
|
|
setSubdistrict(val.subdistrict);
|
|
setDistrict(val.district);
|
|
setProvince(val.province);
|
|
setZipcode(val.zipcode);
|
|
}}
|
|
/>
|
|
<div className="flex items-center gap-2.5 py-1">
|
|
<input
|
|
type="checkbox"
|
|
id="isDefault"
|
|
checked={isDefault}
|
|
onChange={e => setIsDefault(e.target.checked)}
|
|
className="w-4 h-4 text-spa-forest focus:ring-spa-forest border-spa-stone/30 rounded cursor-pointer"
|
|
/>
|
|
<label htmlFor="isDefault" className="text-xs font-semibold text-spa-stone cursor-pointer select-none">
|
|
ตั้งเป็นที่อยู่จัดส่งเริ่มต้น (Default Address)
|
|
</label>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-end gap-3 pt-3 border-t border-spa-stone/10">
|
|
<button
|
|
type="button"
|
|
onClick={resetAddressForm}
|
|
className="px-5 py-2.5 border border-spa-stone/25 hover:bg-spa-light text-spa-stone font-bold rounded-lg text-xs uppercase tracking-wider transition-colors"
|
|
>
|
|
ยกเลิก
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={submittingAddress}
|
|
className="px-5 py-2.5 bg-spa-forest hover:bg-spa-stone text-white font-bold rounded-lg text-xs uppercase tracking-wider transition-colors disabled:opacity-50 flex items-center gap-2"
|
|
>
|
|
{submittingAddress && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
|
|
<span>บันทึกที่อยู่</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
)}
|
|
|
|
{loadingAddresses ? (
|
|
<div className="flex items-center justify-center py-20">
|
|
<Loader2 className="w-8 h-8 text-spa-forest animate-spin" />
|
|
</div>
|
|
) : addresses.length === 0 ? (
|
|
<div className="text-center py-16 bg-white rounded-2xl border border-spa-stone/10 space-y-4 shadow-sm">
|
|
<MapPin className="w-12 h-12 text-spa-stone/40 mx-auto" />
|
|
<div>
|
|
<p className="font-bold text-pop-black">ไม่มีข้อมูลที่อยู่จัดส่ง</p>
|
|
<p className="text-xs text-spa-stone font-medium">กรุณากดปุ่มเพิ่มที่อยู่ด้านบนเพื่อบันทึกสมุดที่อยู่จัดส่งสินค้าของคุณ</p>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{addresses.map(addr => (
|
|
<div
|
|
key={addr.id}
|
|
className={`bg-white border rounded-2xl p-5 shadow-sm space-y-4 transition-all duration-300 relative ${
|
|
addr.isDefault
|
|
? 'border-spa-forest ring-1 ring-spa-forest/20'
|
|
: 'border-spa-stone/10 hover:border-spa-stone'
|
|
}`}
|
|
>
|
|
<div className="flex justify-between items-start gap-4">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
<p className="font-bold text-sm text-pop-black">{addr.recipientName}</p>
|
|
{addr.isDefault && (
|
|
<span className="bg-spa-forest/15 text-spa-forest text-[9px] font-extrabold px-2 py-0.5 rounded-full border border-spa-forest/20">
|
|
เริ่มต้น
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-[10px] text-spa-stone font-semibold flex items-center gap-1">
|
|
<Phone className="w-3 h-3 text-spa-stone/60" />
|
|
<span>{addr.phone}</span>
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1 shrink-0">
|
|
<button
|
|
onClick={() => startEditAddress(addr)}
|
|
className="p-1.5 hover:bg-spa-light rounded-lg border border-spa-stone/10 transition-colors"
|
|
title="แก้ไขที่อยู่"
|
|
>
|
|
<Edit3 className="w-3.5 h-3.5 text-spa-stone" />
|
|
</button>
|
|
<button
|
|
onClick={() => handleDeleteAddress(addr.id)}
|
|
className="p-1.5 hover:bg-rose-500/10 rounded-lg border border-rose-500/10 transition-colors"
|
|
title="ลบที่อยู่"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5 text-rose-500" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<p className="text-xs text-spa-stone/90 leading-relaxed font-medium">
|
|
{addr.addressLine} ต.{addr.subdistrict} อ.{addr.district} จ.{addr.province} {addr.zipcode}
|
|
</p>
|
|
|
|
{!addr.isDefault && (
|
|
<button
|
|
onClick={() => handleSetDefault(addr.id)}
|
|
className="w-full py-1.5 border border-spa-stone/20 hover:bg-spa-light text-[10px] font-bold uppercase tracking-wider text-spa-stone rounded-lg transition-colors"
|
|
>
|
|
ตั้งเป็นที่อยู่เริ่มต้น
|
|
</button>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 3. SETTINGS TAB */}
|
|
{activeTab === 'settings' && (
|
|
<div className="space-y-6">
|
|
<h2 className="text-xl font-bold border-b border-spa-stone/10 pb-4 flex items-center gap-2">
|
|
<Settings className="w-5 h-5 text-spa-forest" />
|
|
<span>ข้อมูลติดต่อสมาชิก</span>
|
|
</h2>
|
|
|
|
<form onSubmit={handleSettingsSubmit} className="bg-white border border-spa-stone/10 rounded-2xl p-6 shadow-sm max-w-lg space-y-5">
|
|
{settingsSuccess && (
|
|
<div className="p-4 bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 font-bold rounded-xl text-xs text-center flex items-center justify-center gap-2">
|
|
<Check className="w-4 h-4" />
|
|
<span>บันทึกข้อมูลติดต่อของคุณสำเร็จแล้ว</span>
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">ที่อยู่อีเมล (Email Address)</label>
|
|
<input
|
|
type="email"
|
|
value={emailInput}
|
|
onChange={e => setEmailInput(e.target.value)}
|
|
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3 text-xs text-pop-black focus:border-spa-forest outline-none transition-all placeholder:text-spa-stone/30"
|
|
placeholder="เช่น example@email.com"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-[10px] uppercase font-bold tracking-widest text-spa-stone/80">เบอร์โทรศัพท์ (Phone Number)</label>
|
|
<input
|
|
type="tel"
|
|
value={phoneInput}
|
|
onChange={e => setPhoneInput(e.target.value)}
|
|
className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3 text-xs text-pop-black focus:border-spa-forest outline-none transition-all placeholder:text-spa-stone/30"
|
|
placeholder="เช่น 0812345678"
|
|
/>
|
|
</div>
|
|
|
|
<div className="pt-2">
|
|
<button
|
|
type="submit"
|
|
disabled={submittingSettings}
|
|
className="w-full py-3 bg-spa-forest hover:bg-spa-stone text-white font-bold rounded-lg text-xs uppercase tracking-widest transition-colors disabled:opacity-50 flex items-center justify-center gap-2"
|
|
>
|
|
{submittingSettings ? <Loader2 className="w-4 h-4 animate-spin" /> : <Check className="w-4 h-4" />}
|
|
<span>บันทึกข้อมูลติดต่อ</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
</main>
|
|
|
|
<Footer categories={categories} />
|
|
</div>
|
|
)
|
|
}
|