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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "shopify-chatbot-widget",
3
3
  "private": false,
4
- "version": "0.9.0",
4
+ "version": "0.9.2",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -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(false);
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 = <LoadingOutlined style={{ fontSize: 24, color: colorCode||'#7F28F8' }} spin />;
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:'8%'
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:'66%'
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
- <Spin indicator={loadingIcon} />
133
- </div>
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
- <div
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
- backgroundColor: colorCode || "black",
223
- borderColor: colorCode || "black",
224
- borderRadius: 6,
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
- APPLY
228
- </Button>
229
- </div> */}
176
+ {items.map((item) => {
177
+ const { symbol, value } = getNumericPrice(item.price)
178
+ const total = value * item.quantity
230
179
 
231
- {/* Summary */}
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:'white'}}>
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, qty?: number) => Promise<void>;
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;
@@ -38,6 +38,7 @@ interface ChatMessagesProps {
38
38
  variantId: string,
39
39
  name: string,
40
40
  variantImage:string,
41
+ price:string,
41
42
  qty?: number,
42
43
 
43
44
  ) => Promise<void>;
@@ -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
 
@@ -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) {
@@ -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
- const json = await res.json();
23
- setItems(json.items || []);
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 Cart (includes image)
32
- const addToCart = async (
33
- variantId: string,
34
- name: string,
35
- qty: number,
36
- variantImage: string
37
- ) => {
38
- if (!sessionId) return;
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
- try {
41
- const res = await fetch(`${config.apiUrl}cart/add/`, {
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
- const json = await res.json();
54
- if (!res.ok) {
55
- throw new Error(json.error || 'Failed to add to cart');
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
- // ✅ Optimistic update
59
- setItems((current) => {
60
- const found = current.find((i) => i.variantId === variantId);
61
- if (found) {
62
- return current.map((i) =>
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 + qty }
88
+ ? { ...i, quantity: i.quantity - qty }
65
89
  : i
66
- );
67
- }
68
- return [...current, { variantId, name, quantity: qty, variantImage }];
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
- // Optimistic update
83
- setItems((current) =>
84
- current
85
- .map((i) =>
86
- i.variantId === variantId
87
- ? { ...i, quantity: i.quantity - qty }
88
- : i
89
- )
90
- .filter((i) => i.quantity > 0)
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
- try {
94
- await fetch(`${config.apiUrl}cart/remove/`, {
95
- method: 'POST',
96
- headers: { 'Content-Type': 'application/json' },
97
- body: JSON.stringify({
98
- session_id: sessionId,
99
- variantId,
100
- quantity: qty,
101
- }),
102
- });
103
- } catch (err) {
104
- console.error('Error removing from cart:', err);
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
- // ✅ Fetch cart on mount
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
  }