shopify-chatbot-widget 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/jaweb-chatbot.iife.js +31 -42
- package/package.json +1 -1
- package/src/Chatbot/Chatbot.tsx +1 -1
- package/src/Chatbot/components/Cart/Index.tsx +130 -118
- package/src/Chatbot/components/Messages/AssistantMesage.tsx +1 -1
- package/src/Chatbot/components/Messages/Carousel/CardSwiper.tsx +2 -2
- package/src/Chatbot/components/Messages/Carousel/CardSwiperSalla.tsx +1 -1
- package/src/Chatbot/components/Messages/Carousel/CardSwiperZid.tsx +1 -1
- package/src/Chatbot/components/Messages/ChatMessages.tsx +1 -0
- package/src/Chatbot/components/Messages/SallaAssistantMessage.tsx +1 -1
- package/src/Chatbot/components/Messages/ZidAssistantMesage.tsx +1 -1
- package/src/Chatbot/components/Messenger/Messenger.tsx +3 -3
- package/src/hooks/useCart.tsx +99 -91
package/package.json
CHANGED
package/src/Chatbot/Chatbot.tsx
CHANGED
|
@@ -12,7 +12,7 @@ import { motion, AnimatePresence } from 'framer-motion';
|
|
|
12
12
|
import CartLayout from './components/Cart/Index';
|
|
13
13
|
|
|
14
14
|
const Chatbot: React.FC = () => {
|
|
15
|
-
const [open, setOpen] = useState(
|
|
15
|
+
const [open, setOpen] = useState(true);
|
|
16
16
|
const [viewMode, setViewMode] = useState<'sessions' | 'create' | 'messenger' | 'cartView' >('sessions');
|
|
17
17
|
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
|
18
18
|
const [HasInfo, setHasInfo] = useState(false);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
1
|
+
import React, { useState, useMemo } from "react";
|
|
2
2
|
import {
|
|
3
3
|
CloseOutlined,
|
|
4
4
|
ArrowLeftOutlined,
|
|
5
5
|
DeleteOutlined,
|
|
6
6
|
ShoppingOutlined,
|
|
7
|
-
LoadingOutlined
|
|
7
|
+
LoadingOutlined,
|
|
8
8
|
} from "@ant-design/icons";
|
|
9
9
|
import { Button, Spin, message } from "antd";
|
|
10
10
|
import { useCart } from "../../../hooks/useCart";
|
|
@@ -30,7 +30,30 @@ const CartLayout: React.FC<CartSessionProps> = ({
|
|
|
30
30
|
const { checkOut } = useCartAPI();
|
|
31
31
|
const [isLoading, setIsLoading] = useState(false);
|
|
32
32
|
|
|
33
|
+
/** ✅ Parse price from strings like "$2,000" or "USD 49.99" */
|
|
34
|
+
const getNumericPrice = (price?: string) => {
|
|
35
|
+
if (!price) return { symbol: '', value: 0 }
|
|
36
|
+
|
|
37
|
+
// Extract first currency symbol (e.g. $, ₵, €, £, ₦)
|
|
38
|
+
const symbolMatch = price.match(/[^0-9.,\s]+/)
|
|
39
|
+
const symbol = symbolMatch ? symbolMatch[0] : ''
|
|
40
|
+
|
|
41
|
+
// Extract numeric portion
|
|
42
|
+
const value = parseFloat(price.replace(/[^0-9.]/g, ''))
|
|
43
|
+
return { symbol, value: isNaN(value) ? 0 : value }
|
|
44
|
+
}
|
|
45
|
+
|
|
33
46
|
|
|
47
|
+
/** ✅ Calculate subtotal */
|
|
48
|
+
const subtotal = useMemo(() => {
|
|
49
|
+
if (!items.length) return 0
|
|
50
|
+
return items.reduce((sum, item) => {
|
|
51
|
+
const { value } = getNumericPrice(item.price)
|
|
52
|
+
return sum + value * item.quantity
|
|
53
|
+
}, 0)
|
|
54
|
+
}, [items])
|
|
55
|
+
|
|
56
|
+
/** ✅ Handle checkout */
|
|
34
57
|
const handleCheckout = async () => {
|
|
35
58
|
if (!sessionId) {
|
|
36
59
|
message.warning("No session found — please start a chat first.");
|
|
@@ -48,9 +71,14 @@ const CartLayout: React.FC<CartSessionProps> = ({
|
|
|
48
71
|
}
|
|
49
72
|
};
|
|
50
73
|
|
|
51
|
-
const loadingIcon =
|
|
52
|
-
|
|
74
|
+
const loadingIcon = (
|
|
75
|
+
<LoadingOutlined
|
|
76
|
+
style={{ fontSize: 24, color: colorCode || "#7F28F8" }}
|
|
77
|
+
spin
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
53
80
|
|
|
81
|
+
/** ✅ Handle item removal */
|
|
54
82
|
const handleRemove = async (variantId: string) => {
|
|
55
83
|
await removeFromCart(variantId, 1);
|
|
56
84
|
message.info("Item removed from cart");
|
|
@@ -79,7 +107,7 @@ const CartLayout: React.FC<CartSessionProps> = ({
|
|
|
79
107
|
borderBottom: "1px solid #f5f5f5",
|
|
80
108
|
boxShadow: "0 2px 6px rgba(0,0,0,0.03)",
|
|
81
109
|
zIndex: 5,
|
|
82
|
-
height:
|
|
110
|
+
height: "8%",
|
|
83
111
|
}}
|
|
84
112
|
>
|
|
85
113
|
<Button
|
|
@@ -117,10 +145,8 @@ const CartLayout: React.FC<CartSessionProps> = ({
|
|
|
117
145
|
flex: 1,
|
|
118
146
|
overflowY: "auto",
|
|
119
147
|
padding: "16px",
|
|
120
|
-
height:
|
|
121
|
-
|
|
148
|
+
height: "66%",
|
|
122
149
|
}}
|
|
123
|
-
|
|
124
150
|
>
|
|
125
151
|
<h3 style={{ fontWeight: 600, marginBottom: 16 }}>
|
|
126
152
|
<ShoppingOutlined style={{ marginRight: 8 }} />
|
|
@@ -129,110 +155,100 @@ const CartLayout: React.FC<CartSessionProps> = ({
|
|
|
129
155
|
|
|
130
156
|
{loading ? (
|
|
131
157
|
<div style={{ textAlign: "center", marginTop: 40 }}>
|
|
132
|
-
|
|
133
|
-
|
|
158
|
+
<Spin indicator={loadingIcon} />
|
|
159
|
+
</div>
|
|
134
160
|
) : items.length === 0 ? (
|
|
135
161
|
<div style={{ textAlign: "center", color: "#888", marginTop: 60 }}>
|
|
136
162
|
Your cart is empty 🛒
|
|
137
163
|
</div>
|
|
138
164
|
) : (
|
|
139
|
-
|
|
140
|
-
style={{
|
|
141
|
-
flex: 1, // fill remaining space
|
|
142
|
-
overflowY: "auto", // scroll only this part
|
|
143
|
-
display: "flex",
|
|
144
|
-
flexDirection: "column",
|
|
145
|
-
height:'80%',
|
|
146
|
-
gap: "12px",
|
|
147
|
-
paddingRight: "4px",
|
|
148
|
-
}}
|
|
149
|
-
>
|
|
150
|
-
{items.map((item) => (
|
|
151
|
-
|
|
152
|
-
<div
|
|
153
|
-
key={item.variantId}
|
|
154
|
-
style={{
|
|
155
|
-
display: "flex",
|
|
156
|
-
justifyContent: "space-between",
|
|
157
|
-
alignItems: "center",
|
|
158
|
-
background: "#fafafa",
|
|
159
|
-
borderRadius: 10,
|
|
160
|
-
padding: "12px 10px",
|
|
161
|
-
transition: "all 0.2s ease",
|
|
162
|
-
}}
|
|
163
|
-
className="hover:shadow-sm"
|
|
164
|
-
>
|
|
165
|
-
{/* ✅ Left side */}
|
|
166
|
-
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
167
|
-
<img
|
|
168
|
-
src={item.variantImage}
|
|
169
|
-
alt={item.name}
|
|
170
|
-
style={{
|
|
171
|
-
width: 64,
|
|
172
|
-
height: 64,
|
|
173
|
-
borderRadius: 8,
|
|
174
|
-
objectFit: "cover",
|
|
175
|
-
backgroundColor: "#f3f3f3",
|
|
176
|
-
}}
|
|
177
|
-
/>
|
|
178
|
-
<div>
|
|
179
|
-
<p style={{ fontWeight: 600, color: "#222", marginBottom: 4 }}>
|
|
180
|
-
{item.name}
|
|
181
|
-
</p>
|
|
182
|
-
<p style={{ fontSize: 13, color: "#777", margin: 0 }}>
|
|
183
|
-
Qty: {item.quantity}
|
|
184
|
-
</p>
|
|
185
|
-
</div>
|
|
186
|
-
</div>
|
|
187
|
-
|
|
188
|
-
{/* ✅ Right side */}
|
|
189
|
-
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
190
|
-
<span style={{ fontWeight: 600 }}>£29.99</span>
|
|
191
|
-
<DeleteOutlined
|
|
192
|
-
onClick={() => handleRemove(item.variantId)}
|
|
193
|
-
style={{
|
|
194
|
-
cursor: "pointer",
|
|
195
|
-
color: "#999",
|
|
196
|
-
fontSize: 16,
|
|
197
|
-
transition: "color 0.2s ease",
|
|
198
|
-
}}
|
|
199
|
-
onMouseEnter={(e) =>
|
|
200
|
-
((e.currentTarget as HTMLElement).style.color = "#e74c3c")
|
|
201
|
-
}
|
|
202
|
-
onMouseLeave={(e) =>
|
|
203
|
-
((e.currentTarget as HTMLElement).style.color = "#999")
|
|
204
|
-
}
|
|
205
|
-
/>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
|
|
209
|
-
))}
|
|
210
|
-
</div>
|
|
211
|
-
)}
|
|
212
|
-
|
|
213
|
-
{/* <Divider style={{ margin: "20px 0" }} /> */}
|
|
214
|
-
|
|
215
|
-
{/* Discount Section */}
|
|
216
|
-
{/* <div style={{ display: "flex", gap: "8px", marginBottom: "16px" }}>
|
|
217
|
-
<Input placeholder="Enter discount code" size="large" />
|
|
218
|
-
<Button
|
|
219
|
-
type="primary"
|
|
220
|
-
size="large"
|
|
165
|
+
<div
|
|
221
166
|
style={{
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
167
|
+
flex: 1,
|
|
168
|
+
overflowY: "auto",
|
|
169
|
+
display: "flex",
|
|
170
|
+
flexDirection: "column",
|
|
171
|
+
height: "80%",
|
|
172
|
+
gap: "12px",
|
|
173
|
+
paddingRight: "4px",
|
|
225
174
|
}}
|
|
226
175
|
>
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
176
|
+
{items.map((item) => {
|
|
177
|
+
const { symbol, value } = getNumericPrice(item.price)
|
|
178
|
+
const total = value * item.quantity
|
|
230
179
|
|
|
231
|
-
|
|
180
|
+
return (
|
|
181
|
+
<div
|
|
182
|
+
key={item.variantId}
|
|
183
|
+
style={{
|
|
184
|
+
display: "flex",
|
|
185
|
+
justifyContent: "space-between",
|
|
186
|
+
alignItems: "center",
|
|
187
|
+
background: "#fafafa",
|
|
188
|
+
borderRadius: 10,
|
|
189
|
+
padding: "12px 10px",
|
|
190
|
+
transition: "all 0.2s ease",
|
|
191
|
+
}}
|
|
192
|
+
className="hover:shadow-sm"
|
|
193
|
+
>
|
|
194
|
+
{/* ✅ Left side */}
|
|
195
|
+
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
196
|
+
<img
|
|
197
|
+
src={item.variantImage}
|
|
198
|
+
alt={item.name}
|
|
199
|
+
style={{
|
|
200
|
+
width: 64,
|
|
201
|
+
height: 64,
|
|
202
|
+
borderRadius: 8,
|
|
203
|
+
objectFit: "cover",
|
|
204
|
+
backgroundColor: "#f3f3f3",
|
|
205
|
+
}}
|
|
206
|
+
/>
|
|
207
|
+
<div>
|
|
208
|
+
<p
|
|
209
|
+
style={{
|
|
210
|
+
fontWeight: 600,
|
|
211
|
+
color: "#222",
|
|
212
|
+
marginBottom: 4,
|
|
213
|
+
}}
|
|
214
|
+
>
|
|
215
|
+
{item.name}
|
|
216
|
+
</p>
|
|
217
|
+
<p style={{ fontSize: 13, color: "#777", margin: 0 }}>
|
|
218
|
+
Qty: {item.quantity}
|
|
219
|
+
</p>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
232
222
|
|
|
223
|
+
{/* ✅ Right side */}
|
|
224
|
+
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
|
225
|
+
<span style={{ fontWeight: 600 }}>
|
|
226
|
+
{symbol}{total.toLocaleString()}
|
|
227
|
+
</span>
|
|
228
|
+
<DeleteOutlined
|
|
229
|
+
onClick={() => handleRemove(item.variantId)}
|
|
230
|
+
style={{
|
|
231
|
+
cursor: "pointer",
|
|
232
|
+
color: "#999",
|
|
233
|
+
fontSize: 16,
|
|
234
|
+
transition: "color 0.2s ease",
|
|
235
|
+
}}
|
|
236
|
+
onMouseEnter={(e) =>
|
|
237
|
+
((e.currentTarget as HTMLElement).style.color = "#e74c3c")
|
|
238
|
+
}
|
|
239
|
+
onMouseLeave={(e) =>
|
|
240
|
+
((e.currentTarget as HTMLElement).style.color = "#999")
|
|
241
|
+
}
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
})}
|
|
247
|
+
</div>
|
|
248
|
+
)}
|
|
233
249
|
</div>
|
|
234
250
|
|
|
235
|
-
{/* Sticky Footer */}
|
|
251
|
+
{/* ✅ Sticky Footer with Subtotal + Checkout */}
|
|
236
252
|
<div
|
|
237
253
|
style={{
|
|
238
254
|
position: "sticky",
|
|
@@ -246,6 +262,19 @@ const CartLayout: React.FC<CartSessionProps> = ({
|
|
|
246
262
|
zIndex: 10,
|
|
247
263
|
}}
|
|
248
264
|
>
|
|
265
|
+
<div
|
|
266
|
+
style={{
|
|
267
|
+
display: "flex",
|
|
268
|
+
justifyContent: "space-between",
|
|
269
|
+
marginBottom: "10px",
|
|
270
|
+
fontWeight: 600,
|
|
271
|
+
color: "#222",
|
|
272
|
+
}}
|
|
273
|
+
>
|
|
274
|
+
<span>Subtotal:</span>
|
|
275
|
+
<span>{subtotal.toLocaleString()}</span>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
249
278
|
<Button
|
|
250
279
|
type="primary"
|
|
251
280
|
block
|
|
@@ -260,26 +289,9 @@ const CartLayout: React.FC<CartSessionProps> = ({
|
|
|
260
289
|
fontWeight: 600,
|
|
261
290
|
}}
|
|
262
291
|
>
|
|
263
|
-
<span style={{color:
|
|
264
|
-
CHECKOUT
|
|
265
|
-
|
|
266
|
-
</span>
|
|
292
|
+
<span style={{ color: "white" }}>CHECKOUT</span>
|
|
267
293
|
</Button>
|
|
268
294
|
</div>
|
|
269
|
-
|
|
270
|
-
{/* Scrollbar */}
|
|
271
|
-
<style>{`
|
|
272
|
-
.custom-scrollbar::-webkit-scrollbar {
|
|
273
|
-
width: 6px;
|
|
274
|
-
}
|
|
275
|
-
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
276
|
-
background-color: rgba(0,0,0,0.2);
|
|
277
|
-
border-radius: 3px;
|
|
278
|
-
}
|
|
279
|
-
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
280
|
-
background-color: rgba(0,0,0,0.4);
|
|
281
|
-
}
|
|
282
|
-
`}</style>
|
|
283
295
|
</div>
|
|
284
296
|
);
|
|
285
297
|
};
|
|
@@ -22,7 +22,7 @@ interface AssistantMessageProps {
|
|
|
22
22
|
colorCode: string;
|
|
23
23
|
isBusiness?: boolean;
|
|
24
24
|
disableCheckout?:boolean;
|
|
25
|
-
handleAddToCart: (variantId: string, name: string,variantImage:string, qty?: number) => Promise<void>;
|
|
25
|
+
handleAddToCart: (variantId: string, name: string,variantImage:string, price:string,qty?: number) => Promise<void>;
|
|
26
26
|
handleRemoveFromCart: (variantId: string, qty?: number) => Promise<void>;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -23,7 +23,7 @@ interface Props {
|
|
|
23
23
|
setSelectedVariants: React.Dispatch<React.SetStateAction<Record<string, string>>>;
|
|
24
24
|
counts: Record<string, number>;
|
|
25
25
|
setCounts: React.Dispatch<React.SetStateAction<Record<string, number>>>;
|
|
26
|
-
handleAddToCart: (variantId: string, name: string, variantImage:string,
|
|
26
|
+
handleAddToCart: (variantId: string, name: string, variantImage:string,price:string, qty?: number) => Promise<void>;
|
|
27
27
|
handleRemoveFromCart: (variantId: string, qty?: number) => Promise<void>;
|
|
28
28
|
colorCode?: string;
|
|
29
29
|
shortenName: (name: string) => string;
|
|
@@ -101,7 +101,7 @@ const ProductCarousel: FC<Props> = ({
|
|
|
101
101
|
[currentVariant.variant_id]: (prev[currentVariant.variant_id] || 0) + 1,
|
|
102
102
|
}));
|
|
103
103
|
try {
|
|
104
|
-
await handleAddToCart(currentVariant.variant_id, currentVariant.name,currentVariant.image, 1);
|
|
104
|
+
await handleAddToCart(currentVariant.variant_id, currentVariant.name,currentVariant.image,currentVariant.price, 1);
|
|
105
105
|
|
|
106
106
|
} catch (err: any) {
|
|
107
107
|
console.warn("Add to cart failed:", err.message);
|
|
@@ -21,7 +21,7 @@ interface Props {
|
|
|
21
21
|
setSelectedVariants: React.Dispatch<React.SetStateAction<Record<string, string>>>;
|
|
22
22
|
counts: Record<string, number>;
|
|
23
23
|
setCounts: React.Dispatch<React.SetStateAction<Record<string, number>>>;
|
|
24
|
-
handleAddToCart: (variantId: string, name: string,variantImage:string, qty: number) => Promise<void>;
|
|
24
|
+
handleAddToCart: (variantId: string, name: string,variantImage:string,price:string, qty: number) => Promise<void>;
|
|
25
25
|
handleRemoveFromCart: (variantId: string, qty: number) => Promise<void>;
|
|
26
26
|
colorCode?: string; // accent
|
|
27
27
|
shortenName: (name: string) => string;
|
|
@@ -21,7 +21,7 @@ interface Props {
|
|
|
21
21
|
setSelectedVariants: React.Dispatch<React.SetStateAction<Record<string, string>>>;
|
|
22
22
|
counts: Record<string, number>;
|
|
23
23
|
setCounts: React.Dispatch<React.SetStateAction<Record<string, number>>>;
|
|
24
|
-
handleAddToCart: (variantId: string, name: string, variantImage:string,qty: number) => Promise<void>;
|
|
24
|
+
handleAddToCart: (variantId: string, name: string, variantImage:string,price:string,qty: number) => Promise<void>;
|
|
25
25
|
handleRemoveFromCart: (variantId: string, qty: number) => Promise<void>;
|
|
26
26
|
colorCode?: string; // accent
|
|
27
27
|
shortenName: (name: string) => string;
|
|
@@ -20,7 +20,7 @@ interface AssistantMessageProps {
|
|
|
20
20
|
message: string;
|
|
21
21
|
colorCode: string;
|
|
22
22
|
isBusiness?: boolean;
|
|
23
|
-
handleAddToCart: (variantId: string, name: string,variantImage:string,
|
|
23
|
+
handleAddToCart: (variantId: string, name: string,variantImage:string,price:string, qty?: number) => Promise<void>;
|
|
24
24
|
handleRemoveFromCart: (variantId: string, qty?: number) => Promise<void>;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -20,7 +20,7 @@ interface AssistantMessageProps {
|
|
|
20
20
|
message: string;
|
|
21
21
|
colorCode: string;
|
|
22
22
|
isBusiness?: boolean;
|
|
23
|
-
handleAddToCart: (variantId: string, name: string,variantImage:string, qty?: number) => Promise<void>;
|
|
23
|
+
handleAddToCart: (variantId: string, name: string,variantImage:string,price:string, qty?: number) => Promise<void>;
|
|
24
24
|
handleRemoveFromCart: (variantId: string, qty?: number) => Promise<void>;
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -414,10 +414,10 @@ const socket = useChatSocket(sessionId, handleSocketData);
|
|
|
414
414
|
}
|
|
415
415
|
|
|
416
416
|
const handleToCart = useCallback(
|
|
417
|
-
async (variantId: string, name: string, variantImage: string, qty = 1) => {
|
|
417
|
+
async (variantId: string, name: string, variantImage: string,price: string ,qty = 1) => {
|
|
418
418
|
try {
|
|
419
|
-
await addToCart(variantId, name, qty, variantImage);
|
|
420
|
-
|
|
419
|
+
await addToCart(variantId, name, qty, variantImage,price);
|
|
420
|
+
|
|
421
421
|
const shortName = shorten(name, 20);
|
|
422
422
|
setNotification(`✅ ${qty}× ${shortName} added to cart`);
|
|
423
423
|
} catch (err: any) {
|
package/src/hooks/useCart.tsx
CHANGED
|
@@ -1,116 +1,124 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
-
import config from './config'
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
2
|
+
import config from './config'
|
|
3
3
|
|
|
4
4
|
export interface CartItem {
|
|
5
|
-
variantId: string
|
|
6
|
-
name: string
|
|
7
|
-
quantity: number
|
|
8
|
-
variantImage: string
|
|
5
|
+
variantId: string
|
|
6
|
+
name: string
|
|
7
|
+
quantity: number
|
|
8
|
+
variantImage: string
|
|
9
|
+
price: string
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export function useCart(sessionId: string) {
|
|
12
|
-
const [items, setItems] = useState<CartItem[]>([])
|
|
13
|
-
const [loading, setLoading] = useState(false)
|
|
13
|
+
const [items, setItems] = useState<CartItem[]>([])
|
|
14
|
+
const [loading, setLoading] = useState(false)
|
|
14
15
|
|
|
15
16
|
// ✅ Fetch current cart
|
|
16
17
|
const fetchCart = useCallback(async () => {
|
|
17
|
-
if (!sessionId) return
|
|
18
|
-
|
|
19
|
-
setLoading(true);
|
|
18
|
+
if (!sessionId) return
|
|
19
|
+
setLoading(true)
|
|
20
20
|
try {
|
|
21
|
-
const res = await fetch(`${config.apiUrl}cart/?session_id=${sessionId}`)
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
const res = await fetch(`${config.apiUrl}cart/?session_id=${sessionId}`)
|
|
22
|
+
if (!res.ok) throw new Error('Failed to fetch cart')
|
|
23
|
+
const json = await res.json()
|
|
24
|
+
setItems(json.items || [])
|
|
24
25
|
} catch (err) {
|
|
25
|
-
console.error('Failed to fetch cart', err)
|
|
26
|
+
console.error('Failed to fetch cart:', err)
|
|
26
27
|
} finally {
|
|
27
|
-
setLoading(false)
|
|
28
|
+
setLoading(false)
|
|
28
29
|
}
|
|
29
|
-
}, [sessionId])
|
|
30
|
+
}, [sessionId])
|
|
30
31
|
|
|
31
|
-
// ✅ Add to
|
|
32
|
-
const addToCart =
|
|
33
|
-
variantId: string,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
// ✅ Add item to cart
|
|
33
|
+
const addToCart = useCallback(
|
|
34
|
+
async (variantId: string, name: string, qty: number, variantImage: string, price: string) => {
|
|
35
|
+
if (!sessionId) return
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`${config.apiUrl}cart/add/`, {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
headers: { 'Content-Type': 'application/json' },
|
|
40
|
+
body: JSON.stringify({
|
|
41
|
+
session_id: sessionId,
|
|
42
|
+
variantId,
|
|
43
|
+
name,
|
|
44
|
+
quantity: qty,
|
|
45
|
+
variantImage,
|
|
46
|
+
price,
|
|
47
|
+
}),
|
|
48
|
+
})
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
method: 'POST',
|
|
43
|
-
headers: { 'Content-Type': 'application/json' },
|
|
44
|
-
body: JSON.stringify({
|
|
45
|
-
session_id: sessionId,
|
|
46
|
-
variantId,
|
|
47
|
-
name,
|
|
48
|
-
quantity: qty,
|
|
49
|
-
variantImage, // ✅ include image
|
|
50
|
-
}),
|
|
51
|
-
});
|
|
50
|
+
const json = await res.json()
|
|
51
|
+
if (!res.ok) throw new Error(json.error || 'Failed to add to cart')
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// ✅ Optimistic update
|
|
54
|
+
setItems((current) => {
|
|
55
|
+
const found = current.find((i) => i.variantId === variantId)
|
|
56
|
+
if (found) {
|
|
57
|
+
return current.map((i) =>
|
|
58
|
+
i.variantId === variantId
|
|
59
|
+
? { ...i, quantity: i.quantity + qty }
|
|
60
|
+
: i
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
return [
|
|
64
|
+
...current,
|
|
65
|
+
{ variantId, name, quantity: qty, variantImage, price },
|
|
66
|
+
]
|
|
67
|
+
})
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error('Error adding to cart:', err)
|
|
70
|
+
} finally {
|
|
71
|
+
// ✅ Always reconcile with backend
|
|
72
|
+
await fetchCart()
|
|
56
73
|
}
|
|
74
|
+
},
|
|
75
|
+
[sessionId, fetchCart]
|
|
76
|
+
)
|
|
57
77
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
// ✅ Remove item from cart
|
|
79
|
+
const removeFromCart = useCallback(
|
|
80
|
+
async (variantId: string, qty: number) => {
|
|
81
|
+
if (!sessionId) return
|
|
82
|
+
|
|
83
|
+
// Optimistic update
|
|
84
|
+
setItems((current) =>
|
|
85
|
+
current
|
|
86
|
+
.map((i) =>
|
|
63
87
|
i.variantId === variantId
|
|
64
|
-
? { ...i, quantity: i.quantity
|
|
88
|
+
? { ...i, quantity: i.quantity - qty }
|
|
65
89
|
: i
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
});
|
|
70
|
-
} catch (err: any) {
|
|
71
|
-
console.error('Error adding to cart:', err);
|
|
72
|
-
throw err;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
await fetchCart(); // ✅ reconcile with server
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// ✅ Remove from Cart
|
|
79
|
-
const removeFromCart = async (variantId: string, qty: number) => {
|
|
80
|
-
if (!sessionId) return;
|
|
90
|
+
)
|
|
91
|
+
.filter((i) => i.quantity > 0)
|
|
92
|
+
)
|
|
81
93
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
try {
|
|
95
|
+
const res = await fetch(`${config.apiUrl}cart/remove/`, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({
|
|
99
|
+
session_id: sessionId,
|
|
100
|
+
variantId,
|
|
101
|
+
quantity: qty,
|
|
102
|
+
}),
|
|
103
|
+
})
|
|
92
104
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
await fetchCart(); // ✅ reconcile with server
|
|
108
|
-
};
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const json = await res.json()
|
|
107
|
+
throw new Error(json.error || 'Failed to remove item')
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error('Error removing from cart:', err)
|
|
111
|
+
} finally {
|
|
112
|
+
await fetchCart()
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
[sessionId, fetchCart]
|
|
116
|
+
)
|
|
109
117
|
|
|
110
|
-
// ✅
|
|
118
|
+
// ✅ Initial fetch
|
|
111
119
|
useEffect(() => {
|
|
112
|
-
fetchCart()
|
|
113
|
-
}, [fetchCart])
|
|
120
|
+
fetchCart()
|
|
121
|
+
}, [fetchCart])
|
|
114
122
|
|
|
115
|
-
return { items, loading, addToCart, removeFromCart, refresh: fetchCart }
|
|
123
|
+
return { items, loading, addToCart, removeFromCart, refresh: fetchCart }
|
|
116
124
|
}
|