Skip to content

Commit f530973

Browse files
committed
add cancel order flow
1 parent a40af88 commit f530973

4 files changed

Lines changed: 256 additions & 55 deletions

File tree

frontend/src/App.jsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,13 @@ function App() {
194194
onClose={() => setShowCheckout(false)}
195195
onRemoveFromCart={removeFromCart}
196196
onOrderPlaced={(items) => {
197-
const updated = [...placedOrders, ...items];
197+
const newOrders = items.map(item => ({
198+
...item,
199+
orderId: Math.floor(10000000 + Math.random() * 90000000).toString(),
200+
placedAt: new Date().toISOString(),
201+
status: 'Placed',
202+
}));
203+
const updated = [...placedOrders, ...newOrders];
198204
setPlacedOrders(updated);
199205
localStorage.setItem('zappify_orders', JSON.stringify(updated));
200206
setCartItems([]);
@@ -208,6 +214,11 @@ function App() {
208214
orders={placedOrders}
209215
onClose={() => setShowAccount(false)}
210216
onLogout={() => { handleLogout(); setShowAccount(false); }}
217+
onCancelOrder={(orderId) => {
218+
const updated = placedOrders.map(o => o.orderId === orderId ? { ...o, status: 'Cancelled' } : o);
219+
setPlacedOrders(updated);
220+
localStorage.setItem('zappify_orders', JSON.stringify(updated));
221+
}}
211222
/>
212223
)}
213224
</div>

frontend/src/components/AccountModal.jsx

Lines changed: 196 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,213 @@
1-
import React from 'react';
2-
import { motion } from 'framer-motion';
3-
import { X, Package, LogOut, User } from 'lucide-react';
1+
import React, { useState } from 'react';
2+
import { motion, AnimatePresence } from 'framer-motion';
3+
import { X, Package, LogOut, ArrowLeft, ChevronRight } from 'lucide-react';
4+
5+
const CANCEL_REASONS = [
6+
'Changed my mind',
7+
'Ordered by mistake',
8+
'Found a better price elsewhere',
9+
'Delivery time is too long',
10+
'Product no longer needed',
11+
'Other',
12+
];
13+
14+
const TRACKING_STEPS = ['Order Placed', 'Shipped', 'In-Transit', 'Out For Delivery', 'Delivered'];
15+
16+
const formatDate = (dateStr) => {
17+
const d = new Date(dateStr);
18+
return d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
19+
};
20+
21+
const getEstDelivery = (dateStr) => {
22+
const d = new Date(dateStr);
23+
d.setDate(d.getDate() + 7);
24+
return d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
25+
};
26+
27+
const AccountModal = ({ user, onClose, onLogout, orders, onCancelOrder }) => {
28+
const [view, setView] = useState('main'); // main | detail | cancel | cancelled
29+
const [selectedOrder, setSelectedOrder] = useState(null);
30+
const [cancelReason, setCancelReason] = useState('');
31+
const [remarks, setRemarks] = useState('');
32+
const [showConfirmPopup, setShowConfirmPopup] = useState(false);
33+
34+
const handleCancelConfirm = () => {
35+
if (!cancelReason) { alert('Please select a reason'); return; }
36+
setShowConfirmPopup(true);
37+
};
38+
39+
const handleContinueAfterCancel = () => {
40+
onCancelOrder(selectedOrder.orderId);
41+
setShowConfirmPopup(false);
42+
setView('main');
43+
setSelectedOrder(null);
44+
setCancelReason('');
45+
setRemarks('');
46+
};
447

5-
const AccountModal = ({ user, onClose, onLogout, orders }) => {
648
return (
749
<div className="overlay-system">
8-
<motion.div
9-
className="backdrop"
10-
initial={{ opacity: 0 }}
11-
animate={{ opacity: 1 }}
12-
exit={{ opacity: 0 }}
13-
onClick={onClose}
14-
/>
15-
<motion.div
16-
className="drawer"
17-
initial={{ x: '100%' }}
18-
animate={{ x: 0 }}
19-
exit={{ x: '100%' }}
20-
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
21-
>
50+
<motion.div className="backdrop" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} />
51+
<motion.div className="drawer" initial={{ x: '100%' }} animate={{ x: 0 }} exit={{ x: '100%' }} transition={{ type: 'spring', damping: 25, stiffness: 200 }}>
52+
2253
<div className="overlay-header">
23-
<h3>MY ACCOUNT</h3>
54+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
55+
{view !== 'main' && (
56+
<button className="close-btn" onClick={() => setView(view === 'cancel' ? 'detail' : 'main')}>
57+
<ArrowLeft size={20} />
58+
</button>
59+
)}
60+
<h3>
61+
{view === 'main' && 'MY ACCOUNT'}
62+
{view === 'detail' && 'ORDER DETAILS'}
63+
{view === 'cancel' && 'CANCEL ORDER'}
64+
</h3>
65+
</div>
2466
<button className="close-btn" onClick={onClose}><X size={24} /></button>
2567
</div>
2668

2769
<div className="account-body">
28-
{/* Profile Card */}
29-
<div className="account-profile-card">
30-
<img src={user.picture} alt={user.name} className="account-avatar" />
31-
<div>
32-
<p className="account-name">{user.name}</p>
33-
<p className="account-email">{user.email}</p>
34-
</div>
35-
</div>
3670

37-
{/* My Orders */}
38-
<div className="account-section">
39-
<h4 className="account-section-title"><Package size={16} /> MY ORDERS</h4>
40-
{orders && orders.length > 0 ? (
41-
<div className="orders-list">
42-
{orders.map((order, i) => (
43-
<div key={i} className="order-item">
44-
<img src={order.image} alt={order.name} />
45-
<div className="order-info">
46-
<p className="order-name">{order.name}</p>
47-
<p className="order-meta">Size: UK {order.size} · Qty: {order.qty}</p>
48-
<p className="order-price">{(order.price * order.qty).toLocaleString('en-IN')}</p>
49-
</div>
50-
<span className="order-status">Placed</span>
71+
{/* ── MAIN VIEW ── */}
72+
{view === 'main' && (
73+
<>
74+
<div className="account-profile-card">
75+
<img src={user.picture} alt={user.name} className="account-avatar" />
76+
<div>
77+
<p className="account-name">{user.name}</p>
78+
<p className="account-email">{user.email}</p>
79+
</div>
80+
</div>
81+
82+
<div className="account-section">
83+
<h4 className="account-section-title"><Package size={16} /> MY ORDERS</h4>
84+
{orders && orders.length > 0 ? (
85+
<div className="orders-list">
86+
{orders.map((order, i) => (
87+
<div key={i} className="order-item" onClick={() => { setSelectedOrder(order); setView('detail'); }} style={{ cursor: 'pointer' }}>
88+
<img src={order.image} alt={order.name} />
89+
<div className="order-info">
90+
<p className="order-name">{order.name}</p>
91+
<p className="order-meta">Size: UK {order.size} · Qty: {order.qty}</p>
92+
<p className="order-price">{(order.price * order.qty).toLocaleString('en-IN')}</p>
93+
</div>
94+
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 6 }}>
95+
<span className={`order-status ${order.status === 'Cancelled' ? 'cancelled' : ''}`}>
96+
{order.status || 'Placed'}
97+
</span>
98+
<ChevronRight size={16} color="#aaa" />
99+
</div>
100+
</div>
101+
))}
51102
</div>
52-
))}
103+
) : (
104+
<div className="orders-empty">
105+
<Package size={36} />
106+
<p>No orders yet</p>
107+
<span>Your placed orders will appear here</span>
108+
</div>
109+
)}
110+
</div>
111+
112+
<button className="account-logout-btn" onClick={() => { onLogout(); onClose(); }}>
113+
<LogOut size={16} /> LOGOUT
114+
</button>
115+
</>
116+
)}
117+
118+
{/* ── ORDER DETAIL VIEW ── */}
119+
{view === 'detail' && selectedOrder && (
120+
<div className="order-detail-view">
121+
<div className="od-header">
122+
<div><span className="od-label">Order ID :</span> <span className="od-value">{selectedOrder.orderId}</span></div>
123+
<div><span className="od-label">Date :</span> <span className="od-value">{formatDate(selectedOrder.placedAt)}</span></div>
53124
</div>
54-
) : (
55-
<div className="orders-empty">
56-
<Package size={36} />
57-
<p>No orders yet</p>
58-
<span>Your placed orders will appear here</span>
125+
126+
<div className="od-item">
127+
<img src={selectedOrder.image} alt={selectedOrder.name} />
128+
<div className="od-item-info">
129+
<p className="od-item-name">{selectedOrder.name}</p>
130+
<p className="od-item-meta">{selectedOrder.category}</p>
131+
<p className="od-item-meta">Size: UK {selectedOrder.size} · Qty: {selectedOrder.qty}</p>
132+
<p className="od-item-price">{(selectedOrder.price * selectedOrder.qty).toLocaleString('en-IN')}</p>
133+
{selectedOrder.status !== 'Cancelled' && (
134+
<button className="od-cancel-btn" onClick={() => setView('cancel')}>Cancel</button>
135+
)}
136+
</div>
137+
</div>
138+
139+
{/* Tracking */}
140+
{selectedOrder.status !== 'Cancelled' ? (
141+
<div className="od-tracking">
142+
{TRACKING_STEPS.map((step, i) => (
143+
<div key={step} className="track-step">
144+
<div className={`track-dot ${i === 0 ? 'active' : ''}`} />
145+
{i < TRACKING_STEPS.length - 1 && <div className="track-line" />}
146+
<div className="track-info">
147+
<p className={`track-label ${i === 0 ? 'active' : ''}`}>{step}</p>
148+
{i === 0 && <p className="track-date">{formatDate(selectedOrder.placedAt)}</p>}
149+
{i === TRACKING_STEPS.length - 1 && <p className="track-date">Est. Delivery by {getEstDelivery(selectedOrder.placedAt)}</p>}
150+
</div>
151+
</div>
152+
))}
153+
</div>
154+
) : (
155+
<div className="od-cancelled-badge">● Order Cancelled</div>
156+
)}
157+
</div>
158+
)}
159+
160+
{/* ── CANCEL VIEW ── */}
161+
{view === 'cancel' && selectedOrder && (
162+
<div className="cancel-view">
163+
<div className="od-header">
164+
<div><span className="od-label">Order ID :</span> <span className="od-value">{selectedOrder.orderId}</span></div>
165+
<div><span className="od-label">Date :</span> <span className="od-value">{formatDate(selectedOrder.placedAt)}</span></div>
59166
</div>
60-
)}
61-
</div>
62167

63-
{/* Logout */}
64-
<button className="account-logout-btn" onClick={() => { onLogout(); onClose(); }}>
65-
<LogOut size={16} /> LOGOUT
66-
</button>
168+
<div className="od-item">
169+
<img src={selectedOrder.image} alt={selectedOrder.name} />
170+
<div className="od-item-info">
171+
<p className="od-item-name">{selectedOrder.name}</p>
172+
<p className="od-item-meta">{selectedOrder.category}</p>
173+
<p className="od-item-meta">Size: UK {selectedOrder.size} · Qty: {selectedOrder.qty}</p>
174+
<p className="od-item-price">{(selectedOrder.price * selectedOrder.qty).toLocaleString('en-IN')}</p>
175+
</div>
176+
</div>
177+
178+
<div className="cancel-form">
179+
<h4>Reason for Cancellation</h4>
180+
<p>Please tell us the reason for cancellation as it will help us serve you better in the future.</p>
181+
<select value={cancelReason} onChange={e => setCancelReason(e.target.value)}>
182+
<option value="">Select reason *</option>
183+
{CANCEL_REASONS.map(r => <option key={r} value={r}>{r}</option>)}
184+
</select>
185+
<label>Additional Remarks</label>
186+
<textarea value={remarks} onChange={e => setRemarks(e.target.value)} placeholder="Optional..." rows={3} />
187+
</div>
188+
189+
<div className="cancel-btns">
190+
<button className="btn-outline cancel-back-btn" onClick={() => setView('detail')}>BACK</button>
191+
<button className="btn-primary cancel-confirm-btn" onClick={handleCancelConfirm}>CONFIRM</button>
192+
</div>
193+
</div>
194+
)}
195+
67196
</div>
197+
198+
{/* Confirmation Popup */}
199+
<AnimatePresence>
200+
{showConfirmPopup && (
201+
<motion.div className="cancel-popup-overlay" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
202+
<motion.div className="cancel-popup" initial={{ scale: 0.9 }} animate={{ scale: 1 }} exit={{ scale: 0.9 }}>
203+
<p>Your cancellation request has been received. You will also receive an email regarding the same with further instructions.</p>
204+
<hr />
205+
<button className="cancel-popup-btn" onClick={handleContinueAfterCancel}>Continue</button>
206+
</motion.div>
207+
</motion.div>
208+
)}
209+
</AnimatePresence>
210+
68211
</motion.div>
69212
</div>
70213
);

frontend/src/data/products.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const ALL_PRODUCTS = [
77
category: 'Men High Top Sneakers',
88
theme: 'Cyber Sport',
99
price: 3799,
10-
image: '/shoes/shoe1.jpg',
10+
image: '/shoes/shoe31.jpg',
1111
isTrending: true,
1212
description: 'Smash your limits with The Incredible Hulk High Tops. Featuring heavy-duty impact cushioning and gamma-green accents.'
1313
},

frontend/src/index.css

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,3 +1140,50 @@ ul {
11401140

11411141
/* Auth error */
11421142
.auth-error { color: #e85d04; font-size: 13px; text-align: center; margin: -4px 0; }
1143+
1144+
/* ── Order Detail & Cancel ── */
1145+
.order-detail-view, .cancel-view { display: flex; flex-direction: column; gap: 16px; }
1146+
.od-header { display: flex; justify-content: space-between; background: #f7f7f7; border-radius: 10px; padding: 12px 16px; font-size: 13px; }
1147+
.od-label { color: #888; }
1148+
.od-value { font-weight: 700; }
1149+
.od-item { display: flex; gap: 14px; padding: 16px; border: 1px solid #f0f0f0; border-radius: 12px; }
1150+
.od-item img { width: 80px; height: 80px; object-fit: cover; border-radius: 10px; background: #f5f5f5; }
1151+
.od-item-info { flex: 1; }
1152+
.od-item-name { font-weight: 700; font-size: 14px; margin-bottom: 4px; }
1153+
.od-item-meta { font-size: 12px; color: #888; margin-bottom: 3px; }
1154+
.od-item-price { font-weight: 800; font-size: 14px; color: #1a1a1a; margin: 6px 0; }
1155+
.od-cancel-btn { font-size: 12px; font-weight: 700; border: 1.5px solid #ddd; background: #fff; border-radius: 6px; padding: 5px 14px; cursor: pointer; color: #555; }
1156+
.od-cancel-btn:hover { border-color: #e85d04; color: #e85d04; }
1157+
1158+
/* Tracking */
1159+
.od-tracking { display: flex; flex-direction: column; padding: 8px 0; }
1160+
.track-step { display: flex; gap: 14px; }
1161+
.track-dot { width: 14px; height: 14px; border-radius: 50%; border: 2px solid #ddd; background: #fff; flex-shrink: 0; margin-top: 3px; }
1162+
.track-dot.active { background: #e85d04; border-color: #e85d04; }
1163+
.track-line { width: 2px; height: 36px; background: #eee; margin-left: 6px; }
1164+
.track-info { padding-bottom: 8px; }
1165+
.track-label { font-size: 13px; font-weight: 600; color: #aaa; }
1166+
.track-label.active { color: #1a1a1a; font-weight: 800; }
1167+
.track-date { font-size: 12px; color: #aaa; margin-top: 2px; }
1168+
.od-cancelled-badge { color: #dc2626; font-weight: 700; font-size: 14px; padding: 12px 0; }
1169+
1170+
/* Cancel Form */
1171+
.cancel-form { display: flex; flex-direction: column; gap: 10px; }
1172+
.cancel-form h4 { font-weight: 800; font-size: 14px; }
1173+
.cancel-form p { font-size: 13px; color: #888; }
1174+
.cancel-form select { padding: 12px 14px; border: 1.5px solid #e8e8e8; border-radius: 10px; font-size: 14px; outline: none; }
1175+
.cancel-form select:focus { border-color: #e85d04; }
1176+
.cancel-form label { font-size: 13px; font-weight: 600; color: #555; }
1177+
.cancel-form textarea { padding: 12px 14px; border: 1.5px solid #e8e8e8; border-radius: 10px; font-size: 14px; outline: none; resize: none; }
1178+
.cancel-form textarea:focus { border-color: #e85d04; }
1179+
.cancel-btns { display: flex; gap: 12px; margin-top: 4px; }
1180+
.cancel-back-btn { flex: 1; padding: 14px; border-radius: 12px; font-size: 13px; letter-spacing: 1px; }
1181+
.cancel-confirm-btn { flex: 2; padding: 14px; border-radius: 12px; font-size: 13px; letter-spacing: 1px; }
1182+
1183+
/* Cancel Popup */
1184+
.cancel-popup-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; z-index: 10; border-radius: inherit; }
1185+
.cancel-popup { background: #fff; border-radius: 14px; padding: 24px; margin: 20px; box-shadow: 0 8px 30px rgba(0,0,0,0.15); }
1186+
.cancel-popup p { font-size: 14px; color: #333; line-height: 1.6; margin-bottom: 16px; }
1187+
.cancel-popup hr { border: none; border-top: 1px solid #eee; margin-bottom: 12px; }
1188+
.cancel-popup-btn { float: right; background: none; border: 1.5px solid #e85d04; color: #e85d04; font-weight: 700; font-size: 14px; padding: 8px 20px; border-radius: 8px; cursor: pointer; }
1189+
.order-status.cancelled { background: #fef2f2; color: #dc2626; }

0 commit comments

Comments
 (0)