Created
April 23, 2026 15:08
-
-
Save parvezpreo/95398e87bc065d2f6ddee3af27292a0f to your computer and use it in GitHub Desktop.
Checkout.jsx page codes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useState, useEffect } from 'react'; | |
| import { useNavigate } from 'react-router-dom'; | |
| import { useCart } from '../CartContext'; | |
| import { | |
| Trash2, ShoppingBag, MapPin, Phone, User, | |
| Truck, ArrowLeft, CreditCard, ShieldCheck, Zap, AlertCircle, Minus, Plus | |
| } from 'lucide-react'; | |
| import api, { STORAGE_URL } from '../api/api'; | |
| const Checkout = () => { | |
| const navigate = useNavigate(); | |
| const { cart, removeFromCart, clearCart, updateQuantity } = useCart(); | |
| const [formData, setFormData] = useState({ | |
| name: '', | |
| phone: '', | |
| address: '' | |
| }); | |
| const [locations, setLocations] = useState([]); | |
| const [settings, setSettings] = useState(null); // 🌟 সেটিংসের জন্য নতুন স্টেট | |
| const [selectedLocation, setSelectedLocation] = useState(null); | |
| const [deliveryCharge, setDeliveryCharge] = useState(0); | |
| const [loading, setLoading] = useState(false); | |
| const [sessionId, setSessionId] = useState(''); | |
| // ১. ডাইনামিক পেজ টাইটেল সেট করা (সংশোধিত) | |
| useEffect(() => { | |
| // settings অবজেক্টের ভেতর থেকে site_name চেক করা | |
| const dynamicAppName = settings?.site_name || 'Webit Shop'; | |
| document.title = `Checkout - ${dynamicAppName}`; | |
| // ডিবাগিং এর জন্য কনসোলে চেক করতে পারেন | |
| // console.log("Settings Data:", settings); | |
| }, [settings?.site_name]); | |
| // ২. লোকেশন এবং সেটিংস ফেচ করা (সংশোধিত) | |
| useEffect(() => { | |
| const fetchInitialData = async () => { | |
| try { | |
| // লোকেশন ফেচ | |
| const locResponse = await api.get('/delivery-locations'); | |
| setLocations(locResponse.data?.data || locResponse.data || []); | |
| // 🌟 সেটিংস ফেচ - এখানে ডাটা স্ট্রাকচার চেক করা হয়েছে | |
| const settingsResponse = await api.get('/settings'); | |
| /** * লারাভেল এপিআই যদি { success: true, data: { site_name: '...' } } এই ফরম্যাটে পাঠায়, | |
| * তবে সেটিংস হবে settingsResponse.data.data। | |
| * আর যদি সরাসরি অবজেক্ট পাঠায়, তবে settingsResponse.data। | |
| */ | |
| const settingsData = settingsResponse.data?.data || settingsResponse.data; | |
| setSettings(settingsData); | |
| } catch (error) { | |
| console.error("Error fetching checkout data:", error); | |
| } | |
| }; | |
| fetchInitialData(); | |
| window.scrollTo(0, 0); | |
| let currentSessionId = localStorage.getItem('checkout_session_id'); | |
| if (!currentSessionId) { | |
| currentSessionId = 'sess_' + Math.random().toString(36).substr(2, 9); | |
| localStorage.setItem('checkout_session_id', currentSessionId); | |
| } | |
| setSessionId(currentSessionId); | |
| }, []); | |
| const handleLocationChange = (e) => { | |
| const locId = e.target.value; | |
| const location = locations.find(l => l.id === parseInt(locId)); | |
| if (location) { | |
| setSelectedLocation(location); | |
| setDeliveryCharge(Number(location.charge || location.amount || 0)); | |
| } else { | |
| setSelectedLocation(null); | |
| setDeliveryCharge(0); | |
| } | |
| }; | |
| const subtotal = cart.reduce((total, item) => total + (Number(item.price) * (item.quantity || 1)), 0); | |
| const totalPrice = subtotal + deliveryCharge; | |
| // BACKGROUND AUTO-SAVE | |
| useEffect(() => { | |
| if (loading || !sessionId) return; | |
| if (!formData.name && !formData.phone && !formData.address) return; | |
| const delayTimer = setTimeout(async () => { | |
| try { | |
| await api.post('/checkout/auto-save', { | |
| session_id: sessionId, | |
| customer_name: formData.name, | |
| phone: formData.phone, | |
| address: formData.address, | |
| total_amount: totalPrice, | |
| shipping_charge: deliveryCharge, | |
| delivery_charge_id: selectedLocation ? selectedLocation.id : null, | |
| cart_items: cart, | |
| }); | |
| console.log('✅ Background Draft Saved!'); | |
| } catch (error) { | |
| console.error('❌ Auto-save failed', error); | |
| } | |
| }, 1500); | |
| return () => clearTimeout(delayTimer); | |
| }, [formData, sessionId, totalPrice, cart, deliveryCharge, selectedLocation, loading]); | |
| // FINAL FORM SUBMIT | |
| const handleSubmit = async (e) => { | |
| e.preventDefault(); | |
| if (!selectedLocation) { | |
| alert("Please select a delivery region"); | |
| return; | |
| } | |
| setLoading(true); | |
| const orderData = { | |
| session_id: sessionId, | |
| customer_name: formData.name, | |
| phone: formData.phone, | |
| address: formData.address, | |
| shipping_charge: deliveryCharge, | |
| total_amount: totalPrice, | |
| delivery_charge_id: selectedLocation.id, | |
| items: cart.map(item => ({ | |
| product_id: item.id, | |
| price: item.price, | |
| name: item.name, | |
| color: item.color || null, | |
| size: item.size || null, | |
| quantity: item.quantity || 1 | |
| })) | |
| }; | |
| try { | |
| const response = await api.post('/order-submit', orderData); | |
| if (response.status === 201 || response.status === 200 || response.data.success) { | |
| localStorage.removeItem('checkout_session_id'); | |
| setSessionId(''); | |
| clearCart(); | |
| // সফল অর্ডারের পর Thank You পেজে রিডাইরেক্ট | |
| navigate('/thank-you', { | |
| state: { | |
| orderDetails: { | |
| ...response.data.order_details, | |
| address: formData.address, | |
| shipping_charge: deliveryCharge | |
| }, | |
| orderedItems: cart | |
| } | |
| }); | |
| } | |
| } catch (error) { | |
| console.error("Order Submission Error:", error.response?.data); | |
| alert(`Error: ${error.response?.data?.message || "Something went wrong. Please try again."}`); | |
| } finally { | |
| setLoading(false); | |
| } | |
| }; | |
| const getImageUrl = (image) => { | |
| const placeholder = 'https://via.placeholder.com/150'; | |
| if (!image) return placeholder; | |
| try { | |
| const imagesArray = typeof image === 'string' && image.startsWith('[') ? JSON.parse(image) : image; | |
| const path = Array.isArray(imagesArray) ? imagesArray[0] : image; | |
| return path.startsWith('http') ? path : `${STORAGE_URL}${path}`; | |
| } catch (e) { return placeholder; } | |
| }; | |
| if (cart.length === 0) { | |
| return ( | |
| <div className="max-w-7xl mx-auto px-4 py-20 md:py-32 text-center"> | |
| <div className="w-16 h-16 md:w-20 md:h-20 bg-slate-50 text-slate-200 rounded-full flex items-center justify-center mx-auto mb-8"> | |
| <ShoppingBag size={32} /> | |
| </div> | |
| <h2 className="text-2xl md:text-3xl font-black text-slate-300 mb-8 tracking-tighter uppercase italic">Your bag is empty</h2> | |
| <button onClick={() => navigate('/')} className="flex items-center gap-2 mx-auto font-black text-[10px] md:text-xs uppercase tracking-[0.2em] text-indigo-600 hover:gap-4 transition-all"> | |
| <ArrowLeft size={16} /> Explore Collection | |
| </button> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="bg-[#fcfdfe] min-h-screen py-8 md:py-16 px-4 md:px-6 font-sans selection:bg-indigo-100"> | |
| <div className="max-w-7xl mx-auto grid grid-cols-1 lg:grid-cols-12 gap-8 lg:gap-16 items-start"> | |
| {/* LEFT: CART REVIEW */} | |
| <div className="lg:col-span-7 space-y-6 md:space-y-10 order-2 lg:order-1"> | |
| <div className="space-y-2"> | |
| <h2 className="text-3xl md:text-4xl font-black text-slate-900 tracking-tighter uppercase italic">Review Order</h2> | |
| <div className="flex items-center gap-2 text-indigo-500 text-[10px] font-black uppercase tracking-[0.2em]"> | |
| <Zap size={12} fill="currentColor" /> {cart.length} Premium Items | |
| </div> | |
| </div> | |
| <div className="space-y-4"> | |
| {cart.map((item, index) => ( | |
| <div key={`${item.id}-${index}`} className="group bg-white p-4 md:p-6 rounded-[1.5rem] border border-slate-100 flex flex-col gap-4 hover:shadow-xl transition-all duration-500"> | |
| <div className="flex justify-between items-start w-full"> | |
| <h4 className="font-black text-slate-900 leading-tight text-base md:text-xl tracking-tight uppercase italic flex-1 pr-2"> | |
| {item.name} | |
| </h4> | |
| <p className="text-base md:text-xl font-black text-indigo-600 tracking-tighter shrink-0"> | |
| ৳{Number(item.price) * (item.quantity || 1)} | |
| </p> | |
| </div> | |
| <div className="flex gap-4 md:gap-6 items-center"> | |
| <div className="w-20 h-20 md:w-24 md:h-24 rounded-2xl overflow-hidden bg-slate-50 shrink-0 border border-slate-100 p-1"> | |
| <img | |
| src={getImageUrl(item.image || item.images)} | |
| alt={item.name} | |
| className="w-full h-full object-cover rounded-xl" | |
| /> | |
| </div> | |
| <div className="flex-1 space-y-3"> | |
| <div className="flex flex-wrap gap-2"> | |
| {item.color && ( | |
| <span className="flex items-center gap-1 text-[8px] md:text-[9px] font-black uppercase tracking-widest bg-slate-50 border border-slate-200 text-slate-600 px-2 py-1 rounded-full"> | |
| <div className="w-1.5 h-1.5 rounded-full" style={{backgroundColor: item.color.toLowerCase()}}></div> | |
| {item.color} | |
| </span> | |
| )} | |
| {item.size && ( | |
| <span className="text-[8px] md:text-[9px] font-black uppercase tracking-widest bg-slate-50 border border-slate-200 text-slate-600 px-2 py-1 rounded-full"> | |
| Size: {item.size} | |
| </span> | |
| )} | |
| </div> | |
| <div className="flex items-center gap-2 bg-slate-50 w-fit p-1 rounded-lg border border-slate-100"> | |
| <button | |
| type="button" | |
| onClick={() => updateQuantity(item.id, item.color, item.size, -1)} | |
| className="w-6 h-6 md:w-8 md:h-8 flex items-center justify-center rounded-md hover:bg-white transition-all text-slate-400" | |
| > | |
| <Minus size={12} /> | |
| </button> | |
| <span className="w-6 md:w-8 text-center font-black text-slate-900 text-xs md:text-sm">{item.quantity || 1}</span> | |
| <button | |
| type="button" | |
| onClick={() => updateQuantity(item.id, item.color, item.size, 1)} | |
| className="w-6 h-6 md:w-8 md:h-8 flex items-center justify-center rounded-md hover:bg-white transition-all text-slate-400" | |
| > | |
| <Plus size={12} /> | |
| </button> | |
| </div> | |
| </div> | |
| <button | |
| type="button" | |
| onClick={() => removeFromCart(item)} | |
| className="p-2 md:p-3 text-slate-300 hover:text-red-500 transition-all" | |
| > | |
| <Trash2 size={20} /> | |
| </button> | |
| </div> | |
| </div> | |
| ))} | |
| </div> | |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-6"> | |
| <div className="flex items-center gap-4 p-5 rounded-3xl bg-white border border-slate-100 shadow-sm"> | |
| <ShieldCheck className="text-indigo-600 shrink-0" size={24} /> | |
| <div> | |
| <p className="text-[9px] font-black uppercase tracking-widest text-slate-900">Secured Order</p> | |
| <p className="text-[9px] text-slate-400 font-bold uppercase">SSL Encrypted</p> | |
| </div> | |
| </div> | |
| <div className="flex items-center gap-4 p-5 rounded-3xl bg-white border border-slate-100 shadow-sm"> | |
| <CreditCard className="text-indigo-600 shrink-0" size={24} /> | |
| <div> | |
| <p className="text-[9px] font-black uppercase tracking-widest text-slate-900">COD Available</p> | |
| <p className="text-[9px] text-slate-400 font-bold uppercase">Pay on Delivery</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {/* RIGHT: SHIPPING & BILLING */} | |
| <div className="lg:col-span-5 order-1 lg:order-2"> | |
| <div className="bg-slate-900 p-6 md:p-10 rounded-[2rem] shadow-2xl lg:sticky lg:top-10 border border-white/5"> | |
| <h2 className="text-xl md:text-2xl font-black mb-8 md:mb-10 text-white tracking-tighter flex items-center gap-3 uppercase italic"> | |
| <MapPin className="text-indigo-400" size={20} /> Shipping Info | |
| </h2> | |
| <form className="space-y-4 md:space-y-6" onSubmit={handleSubmit}> | |
| <div className="relative group"> | |
| <User className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-600 group-focus-within:text-indigo-400 transition-colors" size={18} /> | |
| <input | |
| required | |
| type="text" | |
| value={formData.name} | |
| onChange={(e) => setFormData({...formData, name: e.target.value})} | |
| className="w-full pl-12 pr-4 py-4 bg-white/5 border border-white/10 rounded-2xl text-white focus:ring-2 focus:ring-indigo-500 outline-none transition-all font-bold placeholder:text-slate-600 text-sm" | |
| placeholder="Full Name" | |
| /> | |
| </div> | |
| <div className="relative group"> | |
| <Phone className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-600 group-focus-within:text-indigo-400 transition-colors" size={18} /> | |
| <input | |
| required | |
| type="tel" | |
| value={formData.phone} | |
| onChange={(e) => setFormData({...formData, phone: e.target.value})} | |
| className="w-full pl-12 pr-4 py-4 bg-white/5 border border-white/10 rounded-2xl text-white focus:ring-2 focus:ring-indigo-500 outline-none transition-all font-bold placeholder:text-slate-600 text-sm" | |
| placeholder="Phone Number" | |
| /> | |
| </div> | |
| <textarea | |
| required | |
| rows="3" | |
| value={formData.address} | |
| onChange={(e) => setFormData({...formData, address: e.target.value})} | |
| className="w-full p-4 md:p-6 bg-white/5 border border-white/10 rounded-2xl text-white focus:ring-2 focus:ring-indigo-500 outline-none transition-all font-bold placeholder:text-slate-600 text-sm" | |
| placeholder="Full Shipping Address"> | |
| </textarea> | |
| <div className="relative group"> | |
| <Truck className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-600 group-focus-within:text-indigo-400 transition-colors pointer-events-none" size={18} /> | |
| <select | |
| required | |
| onChange={handleLocationChange} | |
| className="w-full pl-12 pr-10 py-4 md:py-5 bg-white/5 border border-white/10 rounded-2xl text-white focus:ring-2 focus:ring-indigo-500 outline-none transition-all font-bold appearance-none cursor-pointer text-sm" | |
| > | |
| <option value="" className="bg-slate-900 text-slate-400">Select Region</option> | |
| {locations.map(loc => ( | |
| <option key={loc.id} value={loc.id} className="bg-slate-900 text-white italic"> | |
| {loc.location_name || loc.name} (+৳{loc.charge || loc.amount}) | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| <div className="bg-white/5 rounded-2xl p-5 md:p-6 space-y-3 md:space-y-4 mt-6 border border-white/5"> | |
| <div className="flex justify-between text-slate-500 font-black uppercase text-[9px] tracking-widest"> | |
| <span>Subtotal</span> | |
| <span className="text-white">৳{subtotal}</span> | |
| </div> | |
| <div className="flex justify-between text-slate-500 font-black uppercase text-[9px] tracking-widest"> | |
| <span>Shipping</span> | |
| <span className="text-indigo-400">৳{deliveryCharge}</span> | |
| </div> | |
| <div className="h-px bg-white/10 my-2"></div> | |
| <div className="flex justify-between items-center"> | |
| <span className="text-white font-black italic uppercase tracking-widest text-[11px]">Total</span> | |
| <span className="text-3xl md:text-4xl font-black text-white leading-none tracking-wider font-mono">৳{totalPrice}</span> | |
| </div> | |
| </div> | |
| <button | |
| disabled={loading || !selectedLocation} | |
| type="submit" | |
| className={`w-full py-4 md:py-6 rounded-2xl font-black text-[10px] md:text-xs uppercase tracking-[0.3em] transition-all shadow-2xl relative overflow-hidden group ${ | |
| loading || !selectedLocation | |
| ? 'bg-slate-800 text-slate-600 cursor-not-allowed' | |
| : 'bg-indigo-600 text-white hover:bg-white hover:text-indigo-600 active:scale-95' | |
| }`} | |
| > | |
| {loading ? 'Processing...' : ( | |
| <span className="flex items-center justify-center gap-2"> | |
| Confirm Order <ArrowLeft size={16} className="rotate-180" /> | |
| </span> | |
| )} | |
| </button> | |
| {!selectedLocation && ( | |
| <div className="flex items-center justify-center gap-2 text-[9px] font-black uppercase tracking-widest text-indigo-400/50 animate-pulse"> | |
| <AlertCircle size={10} /> Please Select Region | |
| </div> | |
| )} | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default Checkout; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment