File manager - Edit - /home/webapp69.cm.in.th/u69319090046/02_shop/js/app.js
Back
const { useState, useEffect, useMemo, useCallback, useRef } = React; // --- DYNAMIC STORE CONFIG FROM PHP OR FALLBACKS --- const STORE_CONFIG = window.PHP_STORE_CONFIG || { productPrice: 390, shippingFee: 50, freeShippingMinQty: 2, campaignGoal: 350, initialOrdered: 284, remainingStock: 66 }; const PRODUCT_PRICE = STORE_CONFIG.productPrice; const SHIPPING_FEE = STORE_CONFIG.shippingFee; const CAMPAIGN_GOAL = STORE_CONFIG.campaignGoal; const COLOR_OPTIONS = window.PHP_COLOR_OPTIONS || [ { id: 'black', name: 'Classic Black (ดำคลาสสิก)', hex: '#18181b', collarHex: '#09090b', textDark: false, printColor: '#f8fafc', accentColor: '#10b981', shadow: 'rgba(0,0,0,0.6)', tag: 'Popular' }, { id: 'off-white', name: 'Off-White Cream (ขาวครีม)', hex: '#f5f5f4', collarHex: '#e7e5e4', textDark: true, printColor: '#0f172a', accentColor: '#059669', shadow: 'rgba(0,0,0,0.15)', tag: 'Trending' }, { id: 'navy', name: 'Navy Blue (น้ำเงินกรมท่า)', hex: '#1e293b', collarHex: '#0f172a', textDark: false, printColor: '#38bdf8', accentColor: '#38bdf8', shadow: 'rgba(15,23,42,0.6)', tag: '' }, { id: 'olive', name: 'Olive Green (เขียวออลีฟ)', hex: '#3f6212', collarHex: '#1a2e05', textDark: false, printColor: '#fef08a', accentColor: '#eab308', shadow: 'rgba(26,46,5,0.6)', tag: 'Limited' }, { id: 'crimson', name: 'Vintage Crimson (แดงไวน์)', hex: '#881337', collarHex: '#4c0519', textDark: false, printColor: '#fecdd3', accentColor: '#fb7185', shadow: 'rgba(76,5,25,0.6)', tag: '' }, { id: 'slate', name: 'Slate Gray (เทาสเลท)', hex: '#475569', collarHex: '#334155', textDark: false, printColor: '#e2e8f0', accentColor: '#818cf8', shadow: 'rgba(51,65,85,0.6)', tag: '' } ]; const SIZE_OPTIONS = window.PHP_SIZE_OPTIONS || [ { id: 'S', label: 'S', chest: 38, length: 27, shoulder: 19 }, { id: 'M', label: 'M', chest: 40, length: 28, shoulder: 20 }, { id: 'L', label: 'L', chest: 44, length: 29, shoulder: 21 }, { id: 'XL', label: 'XL', chest: 48, length: 30, shoulder: 22 }, { id: '2XL', label: '2XL', chest: 52, length: 31, shoulder: 23 }, { id: '3XL', label: '3XL', chest: 56, length: 32, shoulder: 24 } ]; const PROMO_CODES = { 'PREORDER10': { type: 'percent', amount: 10, label: '10% Off Pre-order Special' }, 'FREESHIP': { type: 'freeship', amount: 50, label: 'Free Shipping Voucher' }, 'APEXVIP': { type: 'percent', amount: 15, label: '15% VIP Early-Bird Discount' } }; // --- ICON COMPONENTS --- const Icons = { ShoppingBag: ({ className = "w-5 h-5" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /> </svg> ), ShoppingCart: ({ className = "w-5 h-5" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" /> </svg> ), Check: ({ className = "w-5 h-5" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}> <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" /> </svg> ), Plus: ({ className = "w-4 h-4" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}> <path strokeLinecap="round" strokeLinejoin="round" d="M12 4v16m8-8H4" /> </svg> ), Minus: ({ className = "w-4 h-4" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}> <path strokeLinecap="round" strokeLinejoin="round" d="M20 12H4" /> </svg> ), Trash: ({ className = "w-4 h-4" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> </svg> ), X: ({ className = "w-5 h-5" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> </svg> ), Sparkles: ({ className = "w-5 h-5" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" /> </svg> ), Clock: ({ className = "w-4 h-4" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> ), Truck: ({ className = "w-5 h-5" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M9 17a2 2 0 11-4 0 2 2 0 014 0zM19 17a2 2 0 11-4 0 2 2 0 014 0z" /> <path strokeLinecap="round" strokeLinejoin="round" d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1 1H9m4-1e1 1h4.586a1 1 0 01.707.293l2.414 2.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6-1a1 1 0 001 1h1M5 17a2 2 0 100-4 2 2 0 000 4zm10 0a2 2 0 100-4 2 2 0 000 4z" /> </svg> ), RotateCcw: ({ className = "w-4 h-4" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> </svg> ), Ruler: ({ className = "w-5 h-5" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /> </svg> ), Shirt: ({ className = "w-5 h-5" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M7 4V2h10v2l3 3-2 5-3-1v10H9V11L6 12 4 7l3-3z" /> </svg> ), QrCode: ({ className = "w-6 h-6" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M12 4v1m6 0h2m-6 0h-2v4h4V5zm-6 0H4v4h4V5zm0 10H4v4h4v-4zm10 0h-4v4h4v-4zm-4-6h.01M12 12h4v4h-4v-4zm-6 0h.01M6 12v.01M18 12v.01" /> </svg> ), User: ({ className = "w-4 h-4" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> </svg> ), Search: ({ className = "w-4 h-4" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> </svg> ), Lock: ({ className = "w-3.5 h-3.5" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /> </svg> ), Layers: ({ className = "w-4 h-4" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /> </svg> ), CreditCard: ({ className = "w-5 h-5" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M3 10h18M7 15h1m4 0h1m-7 4h12a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /> </svg> ), Box: ({ className = "w-5 h-5" }) => ( <svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <path strokeLinecap="round" strokeLinejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /> </svg> ) }; // --- DYNAMIC SVG VECTOR T-SHIRT SHOWCASE --- function TShirtVector({ color, isBackView = false, isZoomed = false }) { const shirtColor = color.hex; const collarColor = color.collarHex; const printColor = color.printColor; const accentColor = color.accentColor; return ( <div className={`relative w-full max-w-[460px] aspect-[4/5] mx-auto flex items-center justify-center transition-transform duration-500 ${isZoomed ? 'scale-125' : 'scale-100'}`}> <svg viewBox="0 0 500 600" className="w-full h-full filter drop-shadow-[0_20px_35px_rgba(0,0,0,0.5)] transition-all duration-300" > <defs> <filter id="fabricShadow" x="-20%" y="-20%" width="140%" height="140%"> <feDropShadow dx="0" dy="12" stdDeviation="15" floodColor={color.shadow} floodOpacity="0.8" /> </filter> <linearGradient id="foldHighlight" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="0%" stopColor="#ffffff" stopOpacity="0.12" /> <stop offset="50%" stopColor="#ffffff" stopOpacity="0.0" /> <stop offset="100%" stopColor="#000000" stopOpacity="0.25" /> </linearGradient> <pattern id="heatherTexture" width="6" height="6" patternUnits="userSpaceOnUse"> <rect width="6" height="6" fill={shirtColor} /> <circle cx="2" cy="2" r="0.8" fill="#ffffff" fillOpacity="0.05" /> <circle cx="5" cy="5" r="0.8" fill="#000000" fillOpacity="0.08" /> </pattern> </defs> <ellipse cx="250" cy="540" rx="170" ry="25" fill={color.shadow} opacity="0.6" filter="blur(10px)" /> {!isBackView ? ( /* FRONT VIEW */ <g filter="url(#fabricShadow)"> <path className="tshirt-svg-body" d="M 175,70 C 150,85 100,110 55,135 C 45,140 40,155 48,175 L 85,255 C 92,270 108,265 118,245 L 142,190 L 142,500 C 142,515 152,525 170,525 L 330,525 C 348,525 358,515 358,500 L 358,190 L 382,245 C 392,265 408,270 415,255 L 452,175 C 460,155 455,140 445,135 C 400,110 350,85 325,70 C 300,75 275,100 250,100 C 225,100 200,75 175,70 Z" fill="url(#heatherTexture)" stroke="rgba(255,255,255,0.08)" strokeWidth="1.5" /> <path d="M 175,70 C 200,75 225,100 250,100 C 275,100 300,75 325,70 C 300,60 275,50 250,50 C 225,50 200,60 175,70 Z" fill={collarColor} stroke="rgba(0,0,0,0.3)" strokeWidth="2" /> <path d="M 185,68 C 205,75 230,85 250,85 C 270,85 295,75 315,68 C 295,58 275,55 250,55 C 225,55 205,58 185,68 Z" fill="#09090b" opacity="0.8" /> <rect x="220" y="60" width="60" height="20" rx="3" fill="#18181b" stroke="#334155" strokeWidth="1" /> <text x="250" y="73" fill="#10b981" fontSize="9" fontWeight="bold" textAnchor="middle" letterSpacing="1">APEX • L</text> <path d="M 142,190 Q 200,240 142,320" fill="none" stroke="url(#foldHighlight)" strokeWidth="12" opacity="0.5" /> <path d="M 358,190 Q 300,240 358,320" fill="none" stroke="url(#foldHighlight)" strokeWidth="12" opacity="0.5" /> <line x1="145" y1="518" x2="355" y2="518" stroke="rgba(0,0,0,0.3)" strokeWidth="2" strokeDasharray="4 3" /> <g transform="translate(250, 260)"> <circle cx="0" cy="-40" r="28" fill="none" stroke={accentColor} strokeWidth="2.5" strokeDasharray="120 10" /> <text x="0" y="-34" fill={printColor} fontSize="20" fontWeight="900" textAnchor="middle" letterSpacing="2">A</text> <text x="0" y="10" fill={printColor} fontSize="17" fontWeight="800" textAnchor="middle" letterSpacing="5">APEX STUDIO</text> <text x="0" y="28" fill={accentColor} fontSize="11" fontWeight="700" textAnchor="middle" letterSpacing="3">LIMITED PRE-ORDER 2026</text> <line x1="-70" y1="40" x2="70" y2="40" stroke={printColor} strokeWidth="1.5" opacity="0.6" /> <text x="0" y="55" fill={printColor} fontSize="9" fontWeight="600" textAnchor="middle" letterSpacing="2" opacity="0.9">HEAVYWEIGHT 220 GSM • OVERSIZED FIT</text> <text x="0" y="68" fill={accentColor} fontSize="8" fontWeight="500" textAnchor="middle" letterSpacing="1">TOKYO CREATIVE LAB • BATCH #04</text> </g> </g> ) : ( /* BACK VIEW */ <g filter="url(#fabricShadow)"> <path className="tshirt-svg-body" d="M 175,65 C 150,80 100,105 55,130 C 45,135 40,150 48,170 L 85,250 C 92,265 108,260 118,240 L 142,185 L 142,500 C 142,515 152,525 170,525 L 330,525 C 348,525 358,515 358,500 L 358,185 L 382,240 C 392,260 408,265 415,250 L 452,170 C 460,150 455,135 445,130 C 400,105 350,80 325,65 C 300,58 275,55 250,55 C 225,55 200,58 175,65 Z" fill="url(#heatherTexture)" stroke="rgba(255,255,255,0.08)" strokeWidth="1.5" /> <path d="M 175,65 C 200,58 225,55 250,55 C 275,55 300,58 325,65 C 300,68 275,70 250,70 C 225,70 200,68 175,65 Z" fill={collarColor} stroke="rgba(0,0,0,0.3)" strokeWidth="1.5" /> <g transform="translate(250, 110)"> <rect x="-35" y="-12" width="70" height="24" rx="4" fill="#0f172a" stroke={accentColor} strokeWidth="1.5" /> <text x="0" y="4" fill={printColor} fontSize="10" fontWeight="bold" textAnchor="middle" letterSpacing="2">APEX 04</text> </g> <g transform="translate(250, 290)"> <rect x="-100" y="-100" width="200" height="200" rx="8" fill="none" stroke={printColor} strokeWidth="1" strokeDasharray="4 4" opacity="0.3" /> <text x="0" y="-30" fill={printColor} fontSize="42" fontWeight="900" textAnchor="middle" letterSpacing="4">APEX</text> <text x="0" y="10" fill={accentColor} fontSize="14" fontWeight="800" textAnchor="middle" letterSpacing="6">DESIGN LAB</text> <text x="0" y="40" fill={printColor} fontSize="10" fontWeight="600" textAnchor="middle" letterSpacing="2" opacity="0.8">BANGKOK • TOKYO • NEW YORK</text> </g> </g> )} </svg> <div className="absolute bottom-3 left-1/2 -translate-x-1/2 bg-slate-900/80 backdrop-blur-md px-3 py-1 rounded-full border border-slate-700/60 text-xs font-medium text-slate-300 flex items-center space-x-1.5 shadow-lg"> <span className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span> <span>{isBackView ? 'Back View (มุมมองด้านหลัง)' : 'Front View (มุมมองด้านหน้า)'}</span> </div> </div> ); } // --- MAIN REACT APPLICATION COMPONENT --- function App() { const [selectedColor, setSelectedColor] = useState(COLOR_OPTIONS[0]); const [selectedSize, setSelectedSize] = useState(SIZE_OPTIONS[2]); const [quantity, setQuantity] = useState(1); const [isBackView, setIsBackView] = useState(false); const [isZoomed, setIsZoomed] = useState(false); // Cart & Stock State const [cart, setCart] = useState([]); const [isCartOpen, setIsCartOpen] = useState(false); const [isBadgeAnimating, setIsBadgeAnimating] = useState(false); const [totalOrderedCount, setTotalOrderedCount] = useState(STORE_CONFIG.totalOrdered || STORE_CONFIG.initialOrdered); const [remainingStock, setRemainingStock] = useState( typeof STORE_CONFIG.remainingStock !== 'undefined' ? STORE_CONFIG.remainingStock : Math.max(0, CAMPAIGN_GOAL - totalOrderedCount) ); const [isSizeGuideOpen, setIsSizeGuideOpen] = useState(false); const [confirmedOrder, setConfirmedOrder] = useState(null); // Checkout Form Inputs const [promoCodeInput, setPromoCodeInput] = useState(''); const [appliedPromo, setAppliedPromo] = useState(null); const [formData, setFormData] = useState({ fullName: '', phone: '', address: '', note: '', paymentMethod: 'promptpay' // 'promptpay' or 'cod' }); const [formErrors, setFormErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); const [toasts, setToasts] = useState([]); const addToast = useCallback((message, type = 'success') => { const id = Date.now(); setToasts(prev => [...prev, { id, message, type }]); setTimeout(() => { setToasts(prev => prev.filter(t => t.id !== id)); }, 3500); }, []); const currentSubtotal = PRODUCT_PRICE * quantity; const totalCartCount = useMemo(() => { return cart.reduce((sum, item) => sum + item.quantity, 0); }, [cart]); const cartSubtotal = useMemo(() => { return cart.reduce((sum, item) => sum + (item.price * item.quantity), 0); }, [cart]); const shippingCost = useMemo(() => { if (cart.length === 0) return 0; if (appliedPromo && appliedPromo.type === 'freeship') return 0; return totalCartCount >= STORE_CONFIG.freeShippingMinQty ? 0 : SHIPPING_FEE; }, [cart, totalCartCount, appliedPromo]); // Small COD handling fee (+฿20 if cash on delivery) const codFee = useMemo(() => { return (formData.paymentMethod === 'cod' && cart.length > 0) ? 20 : 0; }, [formData.paymentMethod, cart]); const discountAmount = useMemo(() => { if (!appliedPromo) return 0; if (appliedPromo.type === 'percent') { return Math.round((cartSubtotal * appliedPromo.amount) / 100); } return 0; }, [cartSubtotal, appliedPromo]); const grandTotal = useMemo(() => { return Math.max(0, cartSubtotal + shippingCost + codFee - discountAmount); }, [cartSubtotal, shippingCost, codFee, discountAmount]); const handleAddToCart = (directCheckout = false) => { if (remainingStock <= 0) { addToast('สินค้าหมดแล้ว (Sold Out)', 'error'); return; } if (quantity > remainingStock) { addToast(`สินค้าคงเหลือเพียง ${remainingStock} ตัวเท่านั้น`, 'error'); return; } const existingIndex = cart.findIndex( item => item.color.id === selectedColor.id && item.size.id === selectedSize.id ); let updatedCart; if (existingIndex > -1) { updatedCart = [...cart]; updatedCart[existingIndex].quantity += quantity; } else { updatedCart = [ ...cart, { id: `${selectedColor.id}-${selectedSize.id}-${Date.now()}`, color: selectedColor, size: selectedSize, price: PRODUCT_PRICE, quantity: quantity } ]; } setCart(updatedCart); setIsBadgeAnimating(true); setTimeout(() => setIsBadgeAnimating(false), 500); addToast(`Added ${quantity}x ${selectedColor.name} (${selectedSize.label}) to Cart!`, 'success'); if (directCheckout) { setIsCartOpen(true); } }; const handleUpdateCartQty = (id, delta) => { setCart(prev => prev .map(item => { if (item.id === id) { const newQty = item.quantity + delta; if (newQty > remainingStock) { addToast(`สินค้าคงเหลือเพียง ${remainingStock} ตัว`, 'error'); return item; } return newQty > 0 ? { ...item, quantity: newQty } : null; } return item; }) .filter(Boolean) ); }; const handleRemoveCartItem = (id) => { setCart(prev => prev.filter(item => item.id !== id)); addToast('Item removed from cart', 'info'); }; const handleApplyPromo = (e) => { e.preventDefault(); const code = promoCodeInput.trim().toUpperCase(); if (PROMO_CODES[code]) { setAppliedPromo(PROMO_CODES[code]); addToast(`Promo Code '${code}' Applied Successfully!`, 'success'); } else { addToast('Invalid or expired promo code', 'error'); } }; const handlePhoneChange = (e) => { let val = e.target.value.replace(/\D/g, ''); if (val.length > 10) val = val.substring(0, 10); let formatted = val; if (val.length > 6) { formatted = `${val.substring(0, 3)}-${val.substring(3, 6)}-${val.substring(6)}`; } else if (val.length > 3) { formatted = `${val.substring(0, 3)}-${val.substring(3)}`; } setFormData(prev => ({ ...prev, phone: formatted })); if (formErrors.phone) setFormErrors(prev => ({ ...prev, phone: null })); }; const handleInputChange = (field, value) => { setFormData(prev => ({ ...prev, [field]: value })); if (formErrors[field]) { setFormErrors(prev => ({ ...prev, [field]: null })); } }; const validateForm = () => { const errors = {}; if (!formData.fullName.trim()) { errors.fullName = 'กรุณากรอกชื่อ-นามสกุล'; } const rawPhone = formData.phone.replace(/\D/g, ''); if (!rawPhone) { errors.phone = 'กรุณากรอกเบอร์โทรศัพท์'; } else if (rawPhone.length !== 10 || !/^(06|08|09)/.test(rawPhone)) { errors.phone = 'เบอร์โทรศัพท์ต้องมี 10 หลัก ขึ้นต้นด้วย 06, 08 หรือ 09'; } if (!formData.address.trim()) { errors.address = 'กรุณากรอกที่อยู่จัดส่งสินค้าให้ครบถ้วน'; } setFormErrors(errors); return Object.keys(errors).length === 0; }; // CHECKOUT SUBMISSION const handleCheckoutSubmit = async (e) => { e.preventDefault(); if (cart.length === 0) { addToast('Your cart is empty!', 'error'); return; } if (!validateForm()) { addToast('Please complete required shipping details', 'error'); return; } setIsSubmitting(true); try { const response = await fetch('./api/order.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fullName: formData.fullName, phone: formData.phone, address: formData.address, note: formData.note, paymentMethod: formData.paymentMethod, discount: discountAmount, items: cart }) }); const resData = await response.json(); if (response.ok && resData.success) { setConfirmedOrder(resData.order); if (typeof resData.totalOrdered !== 'undefined') { setTotalOrderedCount(resData.totalOrdered); } if (typeof resData.remainingStock !== 'undefined') { setRemainingStock(resData.remainingStock); } setCart([]); setIsCartOpen(false); addToast('สั่งจองสำเร็จ! บันทึกข้อมูลการชำระเงินเรียบร้อยแล้ว', 'success'); } else { addToast(resData.message || 'Server error saving order', 'error'); } } catch (err) { addToast('Network error connecting to PHP server', 'error'); } finally { setIsSubmitting(false); } }; return ( <div className="min-h-screen flex flex-col bg-slate-950 text-slate-100 selection:bg-emerald-500 selection:text-white"> {/* TOP ANNOUNCEMENT BAR */} <div className="bg-gradient-to-r from-emerald-600 via-indigo-600 to-purple-600 text-white text-xs font-semibold py-2.5 px-6 lg:px-12 shadow-md"> <div className="max-w-[1600px] mx-auto flex flex-col sm:flex-row items-center justify-between gap-2 text-center sm:text-left"> <div className="flex items-center space-x-2"> <span className="bg-white/20 text-white px-2 py-0.5 rounded-full text-[10px] uppercase tracking-wider font-extrabold flex items-center gap-1"> <Icons.Sparkles className="w-3 h-3 animate-spin" /> Batch 04 Open </span> <span>🔥 Pre-order Special Price <strong>390 THB</strong> • รองรับ Mobile Banking QR & เก็บเงินปลายทาง (COD)!</span> </div> <div className="flex items-center space-x-4 text-[11px] text-emerald-100 font-medium"> <span className="flex items-center space-x-1"> <Icons.Clock className="w-3.5 h-3.5" /> <span>Ends in: <strong>3 Days 14 Hours</strong></span> </span> </div> </div> </div> {/* HEADER */} <header className="sticky top-0 z-40 bg-slate-900/80 backdrop-blur-xl border-b border-slate-800 shadow-xl"> <div className="max-w-[1600px] mx-auto px-6 lg:px-12 h-20 flex items-center justify-between"> <div className="flex items-center space-x-3"> <div className="w-10 h-10 rounded-xl bg-gradient-to-tr from-emerald-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-emerald-500/20 font-black text-xl tracking-tighter text-white"> A </div> <div> <a href="#" className="text-xl font-extrabold tracking-tight bg-gradient-to-r from-white via-slate-100 to-slate-400 bg-clip-text text-transparent"> APEX <span className="text-emerald-400 font-light">STUDIO</span> </a> <p className="text-[10px] text-slate-400 tracking-widest uppercase font-semibold">Urban Oversized Pre-Order</p> </div> </div> <nav className="hidden md:flex items-center space-x-10 text-sm font-semibold text-slate-300"> <a href="#showcase" className="hover:text-emerald-400 transition-colors">Showcase</a> <a href="./track.php" className="hover:text-emerald-400 transition-colors flex items-center space-x-1.5"> <Icons.Search className="w-4 h-4 text-emerald-400" /> <span>Track Order (ติดตามคำสั่งซื้อ)</span> </a> </nav> <div className="flex items-center space-x-4"> <button onClick={() => setIsCartOpen(true)} className="relative touch-target px-5 py-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 border border-slate-700/80 transition-all flex items-center space-x-3 group shadow-lg" > <div className="relative"> <Icons.ShoppingBag className="w-5 h-5 text-emerald-400 group-hover:scale-110 transition-transform" /> {totalCartCount > 0 && ( <span className={`absolute -top-2 -right-2 bg-gradient-to-r from-emerald-500 to-teal-400 text-slate-950 font-black text-[11px] w-5 h-5 rounded-full flex items-center justify-center shadow-md ${isBadgeAnimating ? 'animate-pop-badge' : ''}`}> {totalCartCount} </span> )} </div> <div className="hidden sm:flex flex-col text-left text-xs"> <span className="text-slate-400 font-medium">Cart (ตะกร้า)</span> <span className="text-emerald-400 font-bold">฿{cartSubtotal.toLocaleString()}</span> </div> </button> </div> </div> </header> {/* HERO & CAMPAIGN PROGRESS SECTION */} <section className="bg-slate-900/40 border-b border-slate-800/80 py-6 px-6 lg:px-12"> <div className="max-w-[1600px] mx-auto flex flex-col md:flex-row items-center justify-between gap-6"> <div className="w-full md:w-2/3 space-y-2"> <div className="flex items-center justify-between"> <div className="flex items-center space-x-2"> <span className="w-2.5 h-2.5 rounded-full bg-emerald-500 animate-ping"></span> <h2 className="text-xs font-bold uppercase tracking-wider text-emerald-400">Pre-order Campaign Progress</h2> </div> <div className="px-3 py-1 rounded-full bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 text-xs font-extrabold flex items-center space-x-1.5 shadow-md"> <Icons.Layers className="w-3.5 h-3.5 text-emerald-400" /> <span>สินค้าคงเหลือ: <strong className="text-white text-sm font-black">{remainingStock}</strong> ตัว</span> </div> </div> <div className="space-y-1.5"> <div className="flex justify-between text-xs font-semibold"> <span className="text-slate-300">Reserved (จองแล้ว): <strong className="text-emerald-400 font-bold">{totalOrderedCount} / {CAMPAIGN_GOAL} Pieces</strong></span> <span className="text-indigo-400 font-bold">Only {remainingStock} Left!</span> </div> <div className="w-full h-3.5 bg-slate-800 rounded-full overflow-hidden p-0.5 border border-slate-700/50"> <div className="h-full bg-gradient-to-r from-emerald-500 via-teal-400 to-indigo-500 rounded-full transition-all duration-700 shadow-lg" style={{ width: `${Math.min(100, (totalOrderedCount / CAMPAIGN_GOAL) * 100)}%` }} ></div> </div> </div> </div> <div className="flex items-center space-x-6 text-xs text-slate-300 border-t md:border-t-0 md:border-l border-slate-800 pt-4 md:pt-0 md:pl-6"> <div className="flex items-center space-x-2"> <div className="p-2.5 rounded-xl bg-slate-800 text-emerald-400"> <Icons.Truck className="w-5 h-5" /> </div> <div> <p className="font-bold text-slate-100">Dispatches Aug 25</p> <p className="text-[11px] text-slate-400">Kerry / Flash Express</p> </div> </div> </div> </div> </section> {/* MAIN SHOWCASE */} <main id="showcase" className="flex-1 max-w-[1600px] w-full mx-auto px-6 lg:px-12 py-8 sm:py-12"> <div className="grid grid-cols-1 lg:grid-cols-12 gap-10 lg:gap-14 items-start"> {/* LEFT SHOWCASE */} <div className="lg:col-span-7 space-y-6 lg:sticky lg:top-28"> <div className="glass-panel rounded-3xl p-6 sm:p-10 relative overflow-hidden shadow-2xl border border-slate-800"> <div className="flex items-center justify-between z-10 relative mb-4"> <span className="px-3.5 py-1 rounded-full text-xs font-bold bg-emerald-500/20 text-emerald-300 border border-emerald-500/30"> {selectedColor.tag || 'Pre-order Special'} </span> <div className="flex items-center space-x-2"> <button onClick={() => setIsBackView(!isBackView)} className="px-3.5 py-1.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-xs font-semibold text-slate-200 border border-slate-700 transition-all flex items-center space-x-1.5 shadow-md active:scale-95" > <Icons.RotateCcw className="w-3.5 h-3.5 text-emerald-400" /> <span>{isBackView ? 'Switch to Front' : 'Switch to Back'}</span> </button> <button onClick={() => setIsZoomed(!isZoomed)} className="p-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700" > <Icons.Sparkles className="w-4 h-4 text-indigo-400" /> </button> </div> </div> <TShirtVector color={selectedColor} isBackView={isBackView} isZoomed={isZoomed} /> <div className="mt-6 pt-4 border-t border-slate-800/80 flex flex-wrap items-center justify-between text-xs text-slate-400 gap-2"> <span><strong className="text-slate-200">Color:</strong> {selectedColor.name}</span> <span>220 GSM Comb Cotton • Oversized Fit</span> </div> </div> <div className="grid grid-cols-2 gap-4"> <button onClick={() => setIsBackView(false)} className={`p-3.5 rounded-2xl border transition-all text-left flex items-center space-x-3 ${!isBackView ? 'bg-slate-800 border-emerald-500 shadow-lg' : 'bg-slate-900/60 border-slate-800 opacity-70'}`} > <div className="w-9 h-9 rounded-lg border border-slate-700 flex items-center justify-center text-xs font-bold" style={{ backgroundColor: selectedColor.hex, color: selectedColor.textDark ? '#000' : '#fff' }}> F </div> <div> <p className="text-xs font-bold text-slate-200">Front View (หน้า)</p> <p className="text-[11px] text-slate-400">Urban Print</p> </div> </button> <button onClick={() => setIsBackView(true)} className={`p-3.5 rounded-2xl border transition-all text-left flex items-center space-x-3 ${isBackView ? 'bg-slate-800 border-emerald-500 shadow-lg' : 'bg-slate-900/60 border-slate-800 opacity-70'}`} > <div className="w-9 h-9 rounded-lg border border-slate-700 flex items-center justify-center text-xs font-bold" style={{ backgroundColor: selectedColor.hex, color: selectedColor.textDark ? '#000' : '#fff' }}> B </div> <div> <p className="text-xs font-bold text-slate-200">Back View (หลัง)</p> <p className="text-[11px] text-slate-400">Minimalist Logo</p> </div> </button> </div> </div> {/* RIGHT CUSTOMIZER */} <div className="lg:col-span-5 space-y-8"> <div className="space-y-3"> <div className="flex items-center space-x-2"> <span className="px-2.5 py-0.5 rounded-full text-[11px] font-bold bg-indigo-500/20 text-indigo-300 border border-indigo-500/30"> LIMITED EDITION </span> <span className={`px-2.5 py-0.5 rounded-full text-[11px] font-bold border ${remainingStock > 10 ? 'bg-emerald-500/20 text-emerald-300 border-emerald-500/30' : 'bg-rose-500/20 text-rose-300 border-rose-500/30 animate-pulse'}`}> {remainingStock > 0 ? `Stock: ${remainingStock} left` : 'SOLD OUT'} </span> </div> <h1 className="text-3xl sm:text-4xl lg:text-5xl font-extrabold tracking-tight text-white leading-tight"> Apex Heavyweight Oversized Tee </h1> <div className="flex items-baseline space-x-3"> <span className="text-4xl font-black text-emerald-400">฿390</span> <span className="text-xl text-slate-500 line-through font-semibold">฿690</span> <span className="px-2.5 py-1 rounded-md bg-emerald-500/20 text-emerald-300 text-xs font-bold">SAVE 43%</span> </div> <p className="text-xs sm:text-sm text-slate-300 leading-relaxed"> เสื้อยืดทรง Oversized พรีเมียม Cotton Comb 100% 220 GSM ผ่านการฟอก Bio-washed ออกแบบลายสกรีนคมชัดสไตล์สตรีทเวียร์ </p> </div> {/* COLOR SELECTOR */} <div className="space-y-3 pt-2"> <div className="flex justify-between items-center text-sm font-semibold"> <span className="text-slate-200">Color Selector: <strong className="text-emerald-400">{selectedColor.name}</strong></span> </div> <div className="grid grid-cols-3 sm:grid-cols-6 gap-3"> {COLOR_OPTIONS.map((color) => { const isSelected = selectedColor.id === color.id; return ( <button key={color.id} onClick={() => setSelectedColor(color)} className={`relative p-2 rounded-2xl border transition-all flex flex-col items-center gap-1.5 touch-target ${ isSelected ? 'bg-slate-800 border-emerald-500 ring-2 ring-emerald-500/50 shadow-lg scale-105' : 'bg-slate-900/80 border-slate-800' }`} > <div className="w-10 h-10 rounded-xl border border-slate-600/50 shadow-md flex items-center justify-center" style={{ backgroundColor: color.hex }}> {isSelected && <Icons.Check className={color.textDark ? 'text-slate-950 w-5 h-5' : 'text-white w-5 h-5'} />} </div> <span className="text-[10px] font-semibold text-slate-300 truncate">{color.id}</span> </button> ); })} </div> </div> {/* SIZE SELECTOR */} <div className="space-y-3 pt-2"> <div className="flex justify-between items-center text-sm font-semibold"> <span className="text-slate-200">Size Selector (เลือกไซส์):</span> <button onClick={() => setIsSizeGuideOpen(true)} className="text-xs text-emerald-400 hover:underline flex items-center space-x-1"> <Icons.Ruler className="w-3.5 h-3.5" /> <span>Size Chart</span> </button> </div> <div className="grid grid-cols-3 sm:grid-cols-6 gap-2.5"> {SIZE_OPTIONS.map((size) => { const isSelected = selectedSize.id === size.id; return ( <button key={size.id} onClick={() => setSelectedSize(size)} className={`touch-target py-3.5 px-2 rounded-xl border font-bold text-sm transition-all flex flex-col items-center justify-center ${ isSelected ? 'bg-emerald-500 text-slate-950 font-extrabold shadow-lg scale-105' : 'bg-slate-900/80 text-slate-300 border-slate-800' }`} > <span>{size.label}</span> <span className={`text-[9px] ${isSelected ? 'text-slate-950' : 'text-slate-400'}`}>{size.chest}" Chest</span> </button> ); })} </div> </div> {/* QUANTITY STEPPER */} <div className="glass-card p-6 rounded-2xl space-y-4 border border-slate-800"> <div className="flex items-center justify-between"> <div> <p className="text-xs font-bold text-slate-300">Quantity (จำนวน):</p> <p className="text-[11px] text-emerald-400 font-semibold"> {remainingStock > 0 ? `คงเหลือในคลัง ${remainingStock} ตัว` : 'สินค้าหมดแล้ว'} </p> </div> <div className="flex items-center space-x-3 bg-slate-900 p-1.5 rounded-xl border border-slate-700"> <button onClick={() => setQuantity(Math.max(1, quantity - 1))} disabled={quantity <= 1 || remainingStock <= 0} className="w-9 h-9 rounded-lg bg-slate-800 disabled:opacity-30 flex items-center justify-center"> <Icons.Minus className="w-4 h-4" /> </button> <span className="w-10 text-center font-extrabold text-base text-emerald-400">{quantity}</span> <button onClick={() => setQuantity(Math.min(remainingStock, quantity + 1))} disabled={quantity >= remainingStock || remainingStock <= 0} className="w-9 h-9 rounded-lg bg-slate-800 disabled:opacity-30 flex items-center justify-center"> <Icons.Plus className="w-4 h-4" /> </button> </div> </div> <div className="pt-3 border-t border-slate-800/80 flex items-center justify-between"> <span className="text-xs text-slate-400">Item Subtotal:</span> <span className="text-2xl font-black text-white">฿{currentSubtotal.toLocaleString()}</span> </div> </div> {/* ACTION CTA */} <div className="space-y-3 pt-2"> <button onClick={() => handleAddToCart(false)} disabled={remainingStock <= 0} className="w-full touch-target py-4 px-6 rounded-2xl bg-gradient-to-r from-emerald-500 via-teal-500 to-indigo-600 text-slate-950 font-black text-base transition-all shadow-xl glow-emerald flex items-center justify-center space-x-3 disabled:opacity-40 disabled:cursor-not-allowed" > <Icons.ShoppingCart className="w-5 h-5 text-slate-950" /> <span>{remainingStock > 0 ? `ADD TO CART (เพิ่มลงตะกร้า) • ฿${currentSubtotal.toLocaleString()}` : 'SOLD OUT'}</span> </button> <button onClick={() => handleAddToCart(true)} disabled={remainingStock <= 0} className="w-full touch-target py-3.5 px-6 rounded-2xl bg-slate-800 hover:bg-slate-700 text-slate-100 font-bold text-sm border border-slate-700 flex items-center justify-center space-x-2 disabled:opacity-40 disabled:cursor-not-allowed" > <Icons.Sparkles className="w-4 h-4 text-emerald-400" /> <span>EXPRESS PRE-ORDER NOW</span> </button> </div> </div> </div> </main> {/* CART DRAWER & CHECKOUT SYSTEM */} {isCartOpen && ( <div className="fixed inset-0 z-50 overflow-hidden"> <div onClick={() => setIsCartOpen(false)} className="absolute inset-0 bg-slate-950/80 backdrop-blur-md"></div> <div className="fixed inset-y-0 right-0 max-w-full flex pl-10"> <div className="w-screen max-w-md bg-slate-900 border-l border-slate-800 shadow-2xl flex flex-col justify-between animate-slide-in-right"> <div className="p-5 border-b border-slate-800 flex items-center justify-between bg-slate-950/50"> <div className="flex items-center space-x-2"> <Icons.ShoppingBag className="w-5 h-5 text-emerald-400" /> <h3 className="font-bold text-slate-100 text-base">Your Cart (ตะกร้าสินค้า)</h3> <span className="px-2 py-0.5 rounded-full text-xs font-extrabold bg-emerald-500/20 text-emerald-400"> {totalCartCount} items </span> </div> <button onClick={() => setIsCartOpen(false)} className="p-2 rounded-xl text-slate-400 hover:text-white bg-slate-800"> <Icons.X className="w-5 h-5" /> </button> </div> <div className="flex-1 overflow-y-auto p-5 space-y-6"> {cart.length === 0 ? ( <div className="py-16 text-center space-y-4"> <p className="font-bold text-slate-300 text-sm">ยังไม่มีสินค้าในตะกร้า</p> <button onClick={() => setIsCartOpen(false)} className="px-5 py-2.5 rounded-xl bg-emerald-500 text-slate-950 font-bold text-xs"> เริ่มเลือกสินค้า </button> </div> ) : ( <div className="space-y-4"> {cart.map((item) => ( <div key={item.id} className="p-4 rounded-2xl bg-slate-800/60 border border-slate-800 flex space-x-4 items-center"> <div className="w-14 h-16 rounded-xl border border-slate-700 flex items-center justify-center shrink-0 font-black text-xs" style={{ backgroundColor: item.color.hex, color: item.color.textDark ? '#000' : '#fff' }}> {item.size.label} </div> <div className="flex-1 min-w-0 space-y-1"> <h4 className="font-bold text-slate-200 text-xs truncate">{item.color.name}</h4> <p className="text-[11px] text-slate-400">Size: <strong>{item.size.label}</strong> • ฿{item.price}</p> <div className="flex items-center space-x-1.5 bg-slate-900 px-2 py-1 rounded-lg border border-slate-700 w-fit"> <button onClick={() => handleUpdateCartQty(item.id, -1)} className="w-5 h-5 text-slate-300"><Icons.Minus className="w-3 h-3" /></button> <span className="text-xs font-bold text-emerald-400 w-5 text-center">{item.quantity}</span> <button onClick={() => handleUpdateCartQty(item.id, 1)} className="w-5 h-5 text-slate-300"><Icons.Plus className="w-3 h-3" /></button> </div> </div> <div className="text-right space-y-2"> <span className="font-bold text-sm text-slate-100 block">฿{(item.price * item.quantity).toLocaleString()}</span> <button onClick={() => handleRemoveCartItem(item.id)} className="text-rose-400 p-1 text-xs"><Icons.Trash className="w-4 h-4" /></button> </div> </div> ))} <div className="pt-2"> <form onSubmit={handleApplyPromo} className="flex space-x-2"> <input type="text" placeholder="Promo Code (PREORDER10)" value={promoCodeInput} onChange={(e) => setPromoCodeInput(e.target.value)} className="w-full py-2 px-3 rounded-xl bg-slate-950 border border-slate-800 text-xs text-slate-200 uppercase" /> <button type="submit" className="px-4 py-2 rounded-xl bg-slate-800 text-xs font-bold text-slate-200">Apply</button> </form> </div> {/* CUSTOMER & PAYMENT SELECTION FORM */} <div className="border-t border-slate-800 pt-5 space-y-4"> <h4 className="font-extrabold text-sm text-slate-100 flex items-center space-x-2"> <Icons.User className="w-4 h-4 text-emerald-400" /> <span>Customer Details (ข้อมูลจัดส่ง)</span> </h4> <form id="checkout-form" onSubmit={handleCheckoutSubmit} className="space-y-4 text-xs"> <div> <label className="block font-semibold text-slate-300 mb-1">ชื่อ-นามสกุล <span className="text-rose-400">*</span></label> <input type="text" placeholder="นายสมชาย ใจดี" value={formData.fullName} onChange={(e) => handleInputChange('fullName', e.target.value)} className="w-full py-2.5 px-3 rounded-xl bg-slate-950 border border-slate-800 text-slate-100 text-xs focus:border-emerald-500 focus:outline-none" /> </div> <div> <label className="block font-semibold text-slate-300 mb-1">เบอร์โทรศัพท์ <span className="text-rose-400">*</span></label> <input type="tel" placeholder="081-234-5678" value={formData.phone} onChange={handlePhoneChange} className="w-full py-2.5 px-3 rounded-xl bg-slate-950 border border-slate-800 text-slate-100 text-xs focus:border-emerald-500 focus:outline-none" /> </div> <div> <label className="block font-semibold text-slate-300 mb-1">ที่อยู่จัดส่ง <span className="text-rose-400">*</span></label> <textarea rows={3} placeholder="ที่อยู่จัดส่งสินค้า พร้อมรหัสไปรษณีย์" value={formData.address} onChange={(e) => handleInputChange('address', e.target.value)} className="w-full py-2.5 px-3 rounded-xl bg-slate-950 border border-slate-800 text-slate-100 text-xs focus:border-emerald-500 focus:outline-none" ></textarea> </div> {/* PAYMENT METHOD SELECTOR (MOBILE BANKING QR & COD) */} <div className="pt-2"> <label className="block font-bold text-slate-200 mb-2"> เลือกช่องทางชำระเงิน (Payment Method) <span className="text-rose-400">*</span> </label> <div className="grid grid-cols-2 gap-2.5"> {/* Option 1: Mobile Banking QR Code */} <button type="button" onClick={() => handleInputChange('paymentMethod', 'promptpay')} className={`p-3 rounded-2xl border text-left transition-all flex flex-col space-y-1 relative ${ formData.paymentMethod === 'promptpay' ? 'bg-emerald-500/20 border-emerald-500 text-emerald-300 ring-2 ring-emerald-500/30' : 'bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700' }`} > <div className="flex items-center justify-between"> <Icons.QrCode className="w-5 h-5 text-emerald-400" /> {formData.paymentMethod === 'promptpay' && <Icons.Check className="w-4 h-4 text-emerald-400" />} </div> <span className="font-extrabold text-xs text-slate-100 mt-1">Mobile Banking QR</span> <span className="text-[10px] text-slate-400">สแกนจ่ายผ่านพร้อมเพย์</span> </button> {/* Option 2: Cash on Delivery (COD) */} <button type="button" onClick={() => handleInputChange('paymentMethod', 'cod')} className={`p-3 rounded-2xl border text-left transition-all flex flex-col space-y-1 relative ${ formData.paymentMethod === 'cod' ? 'bg-indigo-500/20 border-indigo-500 text-indigo-300 ring-2 ring-indigo-500/30' : 'bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700' }`} > <div className="flex items-center justify-between"> <Icons.Box className="w-5 h-5 text-indigo-400" /> {formData.paymentMethod === 'cod' && <Icons.Check className="w-4 h-4 text-indigo-400" />} </div> <span className="font-extrabold text-xs text-slate-100 mt-1">เก็บเงินปลายทาง (COD)</span> <span className="text-[10px] text-slate-400">ชำระเงินเมื่อได้รับสินค้า</span> </button> </div> </div> </form> </div> </div> )} </div> {cart.length > 0 && ( <div className="p-5 border-t border-slate-800 bg-slate-950/80 space-y-3"> <div className="space-y-1 text-xs text-slate-300"> <div className="flex justify-between"><span>Subtotal</span><span>฿{cartSubtotal.toLocaleString()}</span></div> <div className="flex justify-between"><span>Shipping</span><span>{shippingCost === 0 ? 'FREE' : `฿${shippingCost}`}</span></div> {codFee > 0 && ( <div className="flex justify-between text-indigo-400"><span>COD Handling Fee (ค่าธรรมเนียมปลายทาง)</span><span>+฿{codFee}</span></div> )} {discountAmount > 0 && ( <div className="flex justify-between text-emerald-400"><span>Discount</span><span>-฿{discountAmount}</span></div> )} <div className="flex justify-between font-bold text-white pt-2 border-t border-slate-800 text-sm"> <span>Grand Total</span><span className="text-emerald-400 text-lg font-black">฿{grandTotal.toLocaleString()}</span> </div> </div> <button type="submit" form="checkout-form" disabled={isSubmitting} className="w-full py-3.5 px-4 rounded-xl bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-black text-sm flex items-center justify-center space-x-2 shadow-lg" > {isSubmitting ? <span>Processing Pre-order...</span> : <span>CONFIRM PRE-ORDER (ยืนยันการสั่งจอง)</span>} </button> </div> )} </div> </div> </div> )} {/* CONFIRMED ORDER MODAL (MOBILE BANKING QR / COD) */} {confirmedOrder && ( <div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-950/85 backdrop-blur-md"> <div className="bg-slate-900 border border-slate-800 rounded-3xl max-w-lg w-full p-6 sm:p-8 space-y-6 shadow-2xl animate-fade-in my-8"> <div className="text-center space-y-2"> <div className="w-16 h-16 rounded-full bg-emerald-500/20 text-emerald-400 border border-emerald-500/40 mx-auto flex items-center justify-center shadow-lg"> <Icons.Check className="w-8 h-8" /> </div> <h3 className="text-2xl font-black text-white">Pre-order Confirmed!</h3> <p className="text-xs text-emerald-400 font-semibold"> บันทึกการสั่งจองสำเร็จ • {confirmedOrder.customer.paymentLabel} </p> <div className="inline-block px-3.5 py-1 rounded-full bg-slate-800 border border-slate-700 text-emerald-400 font-mono font-bold text-sm"> Reference: {confirmedOrder.orderId} </div> </div> {/* IF MOBILE BANKING -> DISPLAY PROMPTPAY QR CODE */} {confirmedOrder.customer.paymentMethod === 'promptpay' ? ( <div className="glass-card p-5 rounded-2xl text-center space-y-3 border border-slate-800"> <div className="flex items-center justify-center space-x-2 text-emerald-400 font-bold text-xs"> <Icons.QrCode className="w-4 h-4" /> <span>สแกน QR Code ผ่าน Mobile Banking (ทุกธนาคาร)</span> </div> <div className="w-48 h-48 bg-white p-3 rounded-2xl mx-auto flex flex-col items-center justify-center shadow-inner relative group"> <Icons.QrCode className="w-40 h-40 text-slate-900" /> <span className="text-[9px] text-slate-600 font-black tracking-widest uppercase">Thai QR PromptPay</span> </div> <div className="text-xs space-y-1 pt-1"> <p className="text-slate-300">ยอดเงินที่ต้องชำระ: <strong className="text-emerald-400 text-lg font-black">฿{confirmedOrder.grandTotal.toLocaleString()}</strong></p> <p className="text-slate-400">ชื่อบัญชี: <strong>บริษัท เอเพ็กซ์ สตูดิโอ จำกัด</strong></p> <p className="text-slate-400">ธนาคารกสิกรไทย (KBank): <strong>012-3-45678-9</strong></p> </div> </div> ) : ( /* IF COD -> DISPLAY COD INSTRUCTIONS */ <div className="glass-card p-6 rounded-2xl text-center space-y-3 border border-indigo-500/40 bg-indigo-950/20"> <div className="w-12 h-12 rounded-2xl bg-indigo-500/20 text-indigo-400 border border-indigo-500/30 mx-auto flex items-center justify-center"> <Icons.Box className="w-6 h-6" /> </div> <h4 className="font-extrabold text-slate-100 text-base">ชำระเงินเก็บปลายทาง (Cash on Delivery)</h4> <p className="text-xs text-slate-300 leading-relaxed"> ยอดรวมทั้งหมด <strong className="text-indigo-300 text-base font-black">฿{confirmedOrder.grandTotal.toLocaleString()}</strong> จะถูกเก็บโดยพนักงานขนส่ง Kerry / Flash Express เมื่อนำพัสดุไปส่งถึงที่อยู่ของคุณ </p> <span className="inline-block px-3 py-1 rounded-lg bg-indigo-500/20 text-indigo-300 text-[11px] font-bold"> 🚚 เตรียมเงินสด หรือ สแกนจ่ายกับพนักงานขนส่งวันจัดส่ง (25 ส.ค. 2026) </span> </div> )} <div className="flex gap-3"> <a href="./track.php" className="flex-1 py-3 rounded-xl bg-slate-800 text-xs font-bold text-slate-200 text-center"> Track Order Status </a> <button onClick={() => setConfirmedOrder(null)} className="flex-1 py-3 rounded-xl bg-emerald-500 text-slate-950 font-black text-xs"> Back to Store </button> </div> </div> </div> )} {/* TOASTS */} <div className="fixed bottom-5 right-5 z-50 space-y-2 pointer-events-none"> {toasts.map((toast) => ( <div key={toast.id} className="pointer-events-auto px-4 py-3 rounded-2xl bg-emerald-950 text-emerald-100 border border-emerald-500/50 text-xs font-bold flex items-center space-x-2 shadow-2xl"> <Icons.Sparkles className="w-4 h-4 text-emerald-400" /> <span>{toast.message}</span> </div> ))} </div> {/* FOOTER */} <footer className="mt-auto border-t border-slate-800/80 bg-slate-950 py-8 px-6 lg:px-12 text-xs text-slate-500"> <div className="max-w-[1600px] mx-auto flex flex-col sm:flex-row items-center justify-between gap-4 text-center sm:text-left"> <div className="space-y-1"> <p className="font-bold text-slate-300">APEX STUDIO APPAREL CO., LTD.</p> <p>© 2026 APEX STUDIO. All rights reserved.</p> </div> <div className="flex items-center space-x-6"> <a href="#" className="hover:text-slate-300">Privacy Policy</a> <a href="./track.php" className="hover:text-emerald-400">Track Order</a> <a href="./orders.php" title="Staff Portal (Password Protected)" className="opacity-40 hover:opacity-100 transition-opacity p-1 text-slate-400 hover:text-emerald-400 flex items-center space-x-1" > <Icons.Lock className="w-3.5 h-3.5" /> <span className="text-[10px]">Staff</span> </a> </div> </div> </footer> </div> ); } ReactDOM.createRoot(document.getElementById('root')).render(<App />);
| ver. 1.4 |
Github
|
.
| PHP 7.4.33 | Generation time: 0.28 |
proxy
|
phpinfo
|
Settings