Skip to content

Instantly share code, notes, and snippets.

@parvezpreo
Created April 23, 2026 15:08
Show Gist options
  • Select an option

  • Save parvezpreo/95398e87bc065d2f6ddee3af27292a0f to your computer and use it in GitHub Desktop.

Select an option

Save parvezpreo/95398e87bc065d2f6ddee3af27292a0f to your computer and use it in GitHub Desktop.
Checkout.jsx page codes
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