Skip to content

Commit fdff02d

Browse files
committed
add 3-step checkout flow with address and payment
1 parent 0014c98 commit fdff02d

3 files changed

Lines changed: 262 additions & 2 deletions

File tree

frontend/src/App.jsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { ALL_PRODUCTS } from './data/products'
77
import { motion, AnimatePresence } from 'framer-motion'
88
import { X, ShoppingBag, Heart, User, Trash2 } from 'lucide-react'
99
import { GoogleLogin, googleLogout } from '@react-oauth/google'
10+
import Checkout from './components/Checkout'
1011

1112
function App() {
1213
const [selectedCategories, setSelectedCategories] = useState([]);
@@ -19,6 +20,7 @@ function App() {
1920
const [activeNav, setActiveNav] = useState('MEN');
2021
const [sneakersView, setSneakersView] = useState(false);
2122
const [loggedInUser, setLoggedInUser] = useState(null);
23+
const [showCheckout, setShowCheckout] = useState(false);
2224

2325
const toggleFilter = (item, type) => {
2426
if (type === 'category') {
@@ -163,14 +165,23 @@ function App() {
163165
onRemoveFromCart={removeFromCart}
164166
onToggleWishlist={toggleWishlist}
165167
onLoginSuccess={setLoggedInUser}
168+
onCheckout={() => { setActiveOverlay(null); setShowCheckout(true); }}
166169
/>
167170
)}
168171
</AnimatePresence>
172+
173+
{showCheckout && (
174+
<Checkout
175+
cartItems={cartItems}
176+
onClose={() => setShowCheckout(false)}
177+
onRemoveFromCart={removeFromCart}
178+
/>
179+
)}
169180
</div>
170181
)
171182
}
172183

173-
const Overlay = ({ type, onClose, cartItems, wishlistItems, onRemoveFromCart, onToggleWishlist, onLoginSuccess }) => {
184+
const Overlay = ({ type, onClose, cartItems, wishlistItems, onRemoveFromCart, onToggleWishlist, onLoginSuccess, onCheckout }) => {
174185
const [isSignUp, setIsSignUp] = useState(false);
175186
const isDrawer = type === 'cart' || type === 'wishlist';
176187
const cartTotal = cartItems.reduce((sum, i) => sum + i.price * i.qty, 0);
@@ -229,7 +240,7 @@ const Overlay = ({ type, onClose, cartItems, wishlistItems, onRemoveFromCart, on
229240
<span>Total</span>
230241
<span>{cartTotal.toLocaleString('en-IN')}</span>
231242
</div>
232-
<button className="btn-primary checkout-btn">PROCEED TO CHECKOUT</button>
243+
<button className="btn-primary checkout-btn" onClick={onCheckout}>PROCEED TO CHECKOUT</button>
233244
</div>
234245
</>
235246
)
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import React, { useState } from 'react';
2+
import { motion, AnimatePresence } from 'framer-motion';
3+
import { X, ChevronRight, MapPin, CreditCard, ShoppingBag, Check } from 'lucide-react';
4+
5+
const STEPS = ['MY BAG', 'ADDRESS', 'PAYMENT'];
6+
7+
const Checkout = ({ cartItems, onClose, onRemoveFromCart }) => {
8+
const [step, setStep] = useState(0);
9+
const [address, setAddress] = useState({ name: '', phone: '', pincode: '', city: '', state: '', street: '' });
10+
const [paymentMethod, setPaymentMethod] = useState('cod');
11+
const [orderPlaced, setOrderPlaced] = useState(false);
12+
13+
const cartTotal = cartItems.reduce((sum, i) => sum + i.price * i.qty, 0);
14+
const gst = Math.round(cartTotal * 0.05);
15+
const shipping = cartTotal > 999 ? 0 : 99;
16+
17+
const handlePlaceOrder = () => {
18+
setOrderPlaced(true);
19+
};
20+
21+
if (orderPlaced) {
22+
return (
23+
<div className="checkout-overlay">
24+
<div className="checkout-modal">
25+
<motion.div
26+
className="order-success"
27+
initial={{ scale: 0.8, opacity: 0 }}
28+
animate={{ scale: 1, opacity: 1 }}
29+
>
30+
<div className="success-icon"><Check size={40} /></div>
31+
<h2>Order Placed!</h2>
32+
<p>Your order has been placed successfully. You'll receive a confirmation soon.</p>
33+
<button className="btn-primary" onClick={onClose}>CONTINUE SHOPPING</button>
34+
</motion.div>
35+
</div>
36+
</div>
37+
);
38+
}
39+
40+
return (
41+
<div className="checkout-overlay">
42+
<motion.div
43+
className="checkout-modal"
44+
initial={{ opacity: 0, y: 40 }}
45+
animate={{ opacity: 1, y: 0 }}
46+
exit={{ opacity: 0, y: 40 }}
47+
>
48+
{/* Header */}
49+
<div className="checkout-header">
50+
<h2>CHECKOUT</h2>
51+
<button className="close-btn" onClick={onClose}><X size={22} /></button>
52+
</div>
53+
54+
{/* Steps */}
55+
<div className="checkout-steps">
56+
{STEPS.map((s, i) => (
57+
<React.Fragment key={s}>
58+
<div className={`step-item ${i <= step ? 'active' : ''} ${i < step ? 'done' : ''}`}>
59+
<div className="step-circle">{i < step ? <Check size={14} /> : i + 1}</div>
60+
<span>{s}</span>
61+
</div>
62+
{i < STEPS.length - 1 && <div className={`step-line ${i < step ? 'done' : ''}`} />}
63+
</React.Fragment>
64+
))}
65+
</div>
66+
67+
{/* Step Content */}
68+
<div className="checkout-body">
69+
<AnimatePresence mode="wait">
70+
71+
{/* STEP 1: MY BAG */}
72+
{step === 0 && (
73+
<motion.div key="bag" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }}>
74+
<div className="co-items">
75+
{cartItems.map((item, i) => (
76+
<div key={i} className="co-item">
77+
<img src={item.image} alt={item.name} />
78+
<div className="co-item-info">
79+
<p className="co-item-name">{item.name}</p>
80+
<p className="co-item-meta">{item.category}</p>
81+
<div className="co-item-tags">
82+
<span>Size: UK {item.size}</span>
83+
<span>Qty: {item.qty}</span>
84+
</div>
85+
</div>
86+
<div className="co-item-price">
87+
<p>{(item.price * item.qty).toLocaleString('en-IN')}</p>
88+
<span>MRP incl. of all taxes</span>
89+
</div>
90+
</div>
91+
))}
92+
</div>
93+
<div className="co-billing">
94+
<h4>BILLING DETAILS</h4>
95+
<div className="billing-row"><span>Cart Total <small>(Incl. of all taxes)</small></span><span>{cartTotal.toLocaleString('en-IN')}</span></div>
96+
<div className="billing-row"><span>Shipping Charges</span><span className="free-ship">{shipping === 0 ? 'FREE' : `₹ ${shipping}`}</span></div>
97+
<div className="billing-row total-row"><span>Total Amount <small>(Incl. of ₹{gst} GST)</small></span><span>{(cartTotal + shipping).toLocaleString('en-IN')}</span></div>
98+
</div>
99+
<button className="btn-primary co-next-btn" onClick={() => setStep(1)}>
100+
PROCEED TO ADDRESS <ChevronRight size={18} />
101+
</button>
102+
</motion.div>
103+
)}
104+
105+
{/* STEP 2: ADDRESS */}
106+
{step === 1 && (
107+
<motion.div key="address" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }}>
108+
<div className="co-address-form">
109+
<h4><MapPin size={16} /> DELIVERY ADDRESS</h4>
110+
<div className="form-row-2">
111+
<input placeholder="Full Name *" value={address.name} onChange={e => setAddress({ ...address, name: e.target.value })} />
112+
<input placeholder="Phone Number *" value={address.phone} onChange={e => setAddress({ ...address, phone: e.target.value })} />
113+
</div>
114+
<input placeholder="Street Address *" value={address.street} onChange={e => setAddress({ ...address, street: e.target.value })} />
115+
<div className="form-row-3">
116+
<input placeholder="Pincode *" value={address.pincode} onChange={e => setAddress({ ...address, pincode: e.target.value })} />
117+
<input placeholder="City *" value={address.city} onChange={e => setAddress({ ...address, city: e.target.value })} />
118+
<input placeholder="State *" value={address.state} onChange={e => setAddress({ ...address, state: e.target.value })} />
119+
</div>
120+
</div>
121+
<div className="co-btn-row">
122+
<button className="btn-outline co-back-btn" onClick={() => setStep(0)}>BACK</button>
123+
<button
124+
className="btn-primary co-next-btn"
125+
onClick={() => {
126+
if (!address.name || !address.phone || !address.street || !address.city || !address.pincode) {
127+
alert('Please fill all required fields');
128+
return;
129+
}
130+
setStep(2);
131+
}}
132+
>
133+
PROCEED TO PAYMENT <ChevronRight size={18} />
134+
</button>
135+
</div>
136+
</motion.div>
137+
)}
138+
139+
{/* STEP 3: PAYMENT */}
140+
{step === 2 && (
141+
<motion.div key="payment" initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }}>
142+
<div className="co-payment">
143+
<h4><CreditCard size={16} /> PAYMENT METHOD</h4>
144+
<div className="payment-options">
145+
{[
146+
{ id: 'cod', label: 'Cash on Delivery' },
147+
{ id: 'upi', label: 'UPI / GPay / PhonePe' },
148+
{ id: 'card', label: 'Credit / Debit Card' },
149+
].map(opt => (
150+
<label key={opt.id} className={`payment-option ${paymentMethod === opt.id ? 'selected' : ''}`}>
151+
<input type="radio" name="payment" value={opt.id} checked={paymentMethod === opt.id} onChange={() => setPaymentMethod(opt.id)} />
152+
<span>{opt.label}</span>
153+
</label>
154+
))}
155+
</div>
156+
157+
<div className="co-billing">
158+
<h4>ORDER SUMMARY</h4>
159+
<div className="billing-row"><span>Cart Total</span><span>{cartTotal.toLocaleString('en-IN')}</span></div>
160+
<div className="billing-row"><span>Shipping</span><span className="free-ship">{shipping === 0 ? 'FREE' : `₹ ${shipping}`}</span></div>
161+
<div className="billing-row total-row"><span>Total Amount</span><span>{(cartTotal + shipping).toLocaleString('en-IN')}</span></div>
162+
</div>
163+
</div>
164+
<div className="co-btn-row">
165+
<button className="btn-outline co-back-btn" onClick={() => setStep(1)}>BACK</button>
166+
<button className="btn-primary co-next-btn place-order-btn" onClick={handlePlaceOrder}>
167+
PLACE ORDER
168+
</button>
169+
</div>
170+
</motion.div>
171+
)}
172+
173+
</AnimatePresence>
174+
</div>
175+
</motion.div>
176+
</div>
177+
);
178+
};
179+
180+
export default Checkout;

frontend/src/index.css

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,3 +1031,72 @@ ul {
10311031
.dropdown-divider { border: none; border-top: 1px solid #f0f0f0; margin: 12px 0; }
10321032
.dropdown-logout { width: 100%; background: #fff0e6; color: #e85d04; border: none; border-radius: 8px; padding: 9px; font-weight: 600; font-size: 13px; cursor: pointer; }
10331033
.dropdown-logout:hover { background: #e85d04; color: #fff; }
1034+
1035+
/* ── Checkout ── */
1036+
.checkout-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; padding: 16px; }
1037+
.checkout-modal { background: #fff; border-radius: 20px; width: 100%; max-width: 680px; max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; }
1038+
.checkout-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 28px; border-bottom: 1px solid #f0f0f0; }
1039+
.checkout-header h2 { font-size: 16px; font-weight: 800; letter-spacing: 2px; }
1040+
1041+
/* Steps */
1042+
.checkout-steps { display: flex; align-items: center; justify-content: center; padding: 20px 28px; gap: 0; }
1043+
.step-item { display: flex; flex-direction: column; align-items: center; gap: 6px; }
1044+
.step-circle { width: 30px; height: 30px; border-radius: 50%; border: 2px solid #ddd; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; color: #aaa; background: #fff; }
1045+
.step-item span { font-size: 11px; font-weight: 700; color: #aaa; letter-spacing: 1px; }
1046+
.step-item.active .step-circle { border-color: #e85d04; color: #e85d04; }
1047+
.step-item.active span { color: #e85d04; }
1048+
.step-item.done .step-circle { background: #e85d04; border-color: #e85d04; color: #fff; }
1049+
.step-item.done span { color: #e85d04; }
1050+
.step-line { flex: 1; height: 2px; background: #eee; min-width: 60px; margin: 0 8px; margin-bottom: 20px; }
1051+
.step-line.done { background: #e85d04; }
1052+
1053+
/* Body */
1054+
.checkout-body { flex: 1; overflow-y: auto; padding: 0 28px 28px; }
1055+
1056+
/* Items */
1057+
.co-items { display: flex; flex-direction: column; gap: 0; margin-bottom: 20px; }
1058+
.co-item { display: flex; gap: 16px; padding: 16px 0; border-bottom: 1px solid #f2f2f2; align-items: center; }
1059+
.co-item img { width: 90px; height: 90px; object-fit: cover; border-radius: 12px; background: #f7f7f7; }
1060+
.co-item-info { flex: 1; }
1061+
.co-item-name { font-weight: 700; font-size: 14px; margin-bottom: 4px; }
1062+
.co-item-meta { font-size: 12px; color: #888; margin-bottom: 8px; }
1063+
.co-item-tags { display: flex; gap: 8px; }
1064+
.co-item-tags span { font-size: 12px; background: #f5f5f5; padding: 4px 10px; border-radius: 6px; color: #555; }
1065+
.co-item-price p { font-weight: 800; font-size: 15px; text-align: right; }
1066+
.co-item-price span { font-size: 11px; color: #aaa; white-space: nowrap; }
1067+
1068+
/* Billing */
1069+
.co-billing { background: #fafafa; border-radius: 14px; padding: 16px 20px; margin-bottom: 20px; }
1070+
.co-billing h4 { font-size: 12px; letter-spacing: 1.5px; color: #aaa; margin-bottom: 12px; font-weight: 700; }
1071+
.billing-row { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #f0f0f0; font-size: 14px; }
1072+
.billing-row:last-child { border-bottom: none; }
1073+
.billing-row small { font-size: 11px; color: #aaa; }
1074+
.billing-row.total-row { font-weight: 800; font-size: 15px; }
1075+
.free-ship { color: #16a34a; font-weight: 700; }
1076+
1077+
/* Buttons */
1078+
.co-next-btn { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px; padding: 16px; border-radius: 14px; font-size: 14px; letter-spacing: 1px; }
1079+
.co-btn-row { display: flex; gap: 12px; margin-top: 20px; }
1080+
.co-back-btn { flex: 0 0 auto; padding: 16px 24px; border-radius: 14px; font-size: 13px; letter-spacing: 1px; }
1081+
.place-order-btn { flex: 2; }
1082+
1083+
/* Address Form */
1084+
.co-address-form h4 { font-size: 13px; font-weight: 700; letter-spacing: 1px; margin-bottom: 16px; display: flex; align-items: center; gap: 6px; color: #333; }
1085+
.co-address-form input { width: 100%; padding: 12px 16px; border: 1.5px solid #e8e8e8; border-radius: 10px; font-size: 14px; margin-bottom: 12px; outline: none; box-sizing: border-box; }
1086+
.co-address-form input:focus { border-color: #e85d04; }
1087+
.form-row-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
1088+
.form-row-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; }
1089+
.form-row-2 input, .form-row-3 input { margin-bottom: 0; }
1090+
1091+
/* Payment */
1092+
.co-payment h4 { font-size: 13px; font-weight: 700; letter-spacing: 1px; margin-bottom: 16px; display: flex; align-items: center; gap: 6px; color: #333; }
1093+
.payment-options { display: flex; flex-direction: column; gap: 10px; margin-bottom: 20px; }
1094+
.payment-option { display: flex; align-items: center; gap: 12px; padding: 14px 16px; border: 1.5px solid #e8e8e8; border-radius: 12px; cursor: pointer; font-size: 14px; font-weight: 500; }
1095+
.payment-option.selected { border-color: #e85d04; background: #fff8f5; }
1096+
.payment-option input { accent-color: #e85d04; width: 16px; height: 16px; }
1097+
1098+
/* Order Success */
1099+
.order-success { text-align: center; padding: 48px 28px; }
1100+
.success-icon { width: 72px; height: 72px; background: #e85d04; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; margin: 0 auto 20px; }
1101+
.order-success h2 { font-size: 24px; font-weight: 800; margin-bottom: 10px; }
1102+
.order-success p { color: #888; margin-bottom: 28px; font-size: 14px; }

0 commit comments

Comments
 (0)