shopify-chatbot-widget 0.7.9 → 0.8.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.
Files changed (58) hide show
  1. package/dist/jaweb-chatbot.css +1 -1
  2. package/dist/jaweb-chatbot.iife.js +3 -3
  3. package/package.json +1 -1
  4. package/src/App.css +42 -0
  5. package/src/App.tsx +13 -0
  6. package/src/Chatbot/Chatbot.css +270 -0
  7. package/src/Chatbot/Chatbot.tsx +214 -0
  8. package/src/Chatbot/ChatbotTransition.css +19 -0
  9. package/src/Chatbot/OLDCHAT.tsx +259 -0
  10. package/src/Chatbot/components/ChatInput.css +127 -0
  11. package/src/Chatbot/components/ChatInput.tsx +354 -0
  12. package/src/Chatbot/components/Messages/Addcart.tsx +70 -0
  13. package/src/Chatbot/components/Messages/AgentStatus.tsx +37 -0
  14. package/src/Chatbot/components/Messages/AssistantMesage.tsx +206 -0
  15. package/src/Chatbot/components/Messages/AudioMessage.tsx +123 -0
  16. package/src/Chatbot/components/Messages/CalendlyEmbeds.tsx +66 -0
  17. package/src/Chatbot/components/Messages/CardSwiper.tsx +194 -0
  18. package/src/Chatbot/components/Messages/Carousel/CardSwiper.tsx +264 -0
  19. package/src/Chatbot/components/Messages/Carousel/CardSwiperSalla.tsx +208 -0
  20. package/src/Chatbot/components/Messages/Carousel/CardSwiperZid.tsx +208 -0
  21. package/src/Chatbot/components/Messages/Carousel/SallaSwiper.css +245 -0
  22. package/src/Chatbot/components/Messages/Carousel/TryOn.tsx +111 -0
  23. package/src/Chatbot/components/Messages/CartWidget.tsx +151 -0
  24. package/src/Chatbot/components/Messages/ChatImage.tsx +46 -0
  25. package/src/Chatbot/components/Messages/ChatImageText.tsx +74 -0
  26. package/src/Chatbot/components/Messages/ChatImageTextAssis.tsx +72 -0
  27. package/src/Chatbot/components/Messages/ChatMessages.css +31 -0
  28. package/src/Chatbot/components/Messages/ChatMessages.tsx +334 -0
  29. package/src/Chatbot/components/Messages/ProductCard.css +149 -0
  30. package/src/Chatbot/components/Messages/SallaAssistantMessage.tsx +196 -0
  31. package/src/Chatbot/components/Messages/Support.tsx +177 -0
  32. package/src/Chatbot/components/Messages/Typing.css +31 -0
  33. package/src/Chatbot/components/Messages/Typing.tsx +19 -0
  34. package/src/Chatbot/components/Messages/ZidAssistantMesage.tsx +196 -0
  35. package/src/Chatbot/components/Messages/cartwidget.css +123 -0
  36. package/src/Chatbot/components/Messenger/Messenger.tsx +531 -0
  37. package/src/Chatbot/components/Notification.tsx +28 -0
  38. package/src/Chatbot/components/Powered.css +29 -0
  39. package/src/Chatbot/components/Powered.tsx +12 -0
  40. package/src/Chatbot/components/Sessions/CreateSession.tsx +201 -0
  41. package/src/Chatbot/components/Sessions/RenderList.tsx +212 -0
  42. package/src/Chatbot/components/Suggestions.css +35 -0
  43. package/src/Chatbot/components/Suggestions.tsx +57 -0
  44. package/src/Chatbot/notification.css +17 -0
  45. package/src/assets/react.svg +1 -0
  46. package/src/chatbot-widget/index.tsx +14 -0
  47. package/src/hooks/config.tsx +13 -0
  48. package/src/hooks/useCart.tsx +107 -0
  49. package/src/hooks/useChatbotAPI.tsx +167 -0
  50. package/src/hooks/useChatbotData.tsx +132 -0
  51. package/src/hooks/useSessionMessages.tsx +50 -0
  52. package/src/hooks/useWebsocke.tsx +45 -0
  53. package/src/i18n.tsx +30 -0
  54. package/src/index.css +2 -0
  55. package/src/main.tsx +10 -0
  56. package/src/types/chatbot.ts +40 -0
  57. package/src/utils/cookies.tsx +14 -0
  58. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,123 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import WaveSurfer from "wavesurfer.js";
3
+ import {
4
+ PlayCircleOutlined,
5
+ PauseCircleOutlined
6
+ } from "@ant-design/icons";
7
+
8
+ interface AudioVisualizerProps {
9
+ link: string;
10
+ colorCode: string;
11
+ transcription?: string;
12
+ }
13
+
14
+ const AudioVisualizer: React.FC<AudioVisualizerProps> = ({
15
+ link,
16
+ colorCode,
17
+ transcription,
18
+ }) => {
19
+ const audioRef = useRef<HTMLDivElement | null>(null);
20
+ const wavesurferRef = useRef<WaveSurfer | null>(null);
21
+ const [isPlaying, setIsPlaying] = useState(false);
22
+ const [showTranscription, setShowTranscription] = useState(false);
23
+
24
+ useEffect(() => {
25
+ if (!audioRef.current) return;
26
+
27
+ wavesurferRef.current?.destroy();
28
+
29
+ wavesurferRef.current = WaveSurfer.create({
30
+ container: audioRef.current,
31
+ waveColor: "white",
32
+ progressColor: "white",
33
+ cursorWidth: 0,
34
+ barWidth: 3,
35
+ barHeight: 1.5,
36
+ barGap: 2,
37
+ height: 40,
38
+ interact: true,
39
+ });
40
+
41
+ wavesurferRef.current.load(link);
42
+ wavesurferRef.current.on("finish", () => setIsPlaying(false));
43
+
44
+ return () => {
45
+ wavesurferRef.current?.destroy();
46
+ };
47
+ }, [link, colorCode]);
48
+
49
+ const togglePlay = () => {
50
+ if (wavesurferRef.current) {
51
+ wavesurferRef.current.playPause();
52
+ setIsPlaying((prev) => !prev);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <div
58
+ style={{
59
+ backgroundColor:colorCode,
60
+ padding: "12px",
61
+ borderRadius: "20px",
62
+ color: "white",
63
+ maxWidth: "350px",
64
+ marginBottom: "16px",
65
+ }}
66
+ >
67
+ <div style={{ display: "flex", alignItems: "center" }}>
68
+ <button
69
+ onClick={togglePlay}
70
+ style={{
71
+ background: "transparent",
72
+ border: "none",
73
+ color: "white",
74
+ marginRight: "12px",
75
+ cursor: "pointer",
76
+ }}
77
+ >
78
+ {isPlaying ? (
79
+ <PauseCircleOutlined style={{ fontSize: "24px" }} />
80
+ ) : (
81
+ <PlayCircleOutlined style={{ fontSize: "24px" }} />
82
+ )}
83
+ </button>
84
+
85
+ <div
86
+ ref={audioRef}
87
+ style={{
88
+ width: "130px",
89
+ height: "40px",
90
+ overflow: "hidden",
91
+ color:'white'
92
+ }}
93
+ />
94
+
95
+ </div>
96
+
97
+ {transcription && (
98
+ <div style={{ marginTop: "12px", fontSize: "14px", color: "rgba(255,255,255,0.85)" }}>
99
+ <button
100
+ onClick={() => setShowTranscription(!showTranscription)}
101
+ style={{
102
+ background: "none",
103
+ border: "none",
104
+ color: "white",
105
+ textDecoration: "underline",
106
+ fontSize: "13px",
107
+ cursor: "pointer",
108
+ padding: 0,
109
+ }}
110
+ >
111
+ {showTranscription ? "Hide transcript" : "Show transcript"}
112
+ </button>
113
+
114
+ {showTranscription && (
115
+ <p style={{ marginTop: "6px", lineHeight: "1.4" }}>{transcription}</p>
116
+ )}
117
+ </div>
118
+ )}
119
+ </div>
120
+ );
121
+ };
122
+
123
+ export default AudioVisualizer;
@@ -0,0 +1,66 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+
3
+ interface CalendlyEmbedsProps {
4
+ calendlyLink: string;
5
+ }
6
+
7
+ declare global {
8
+ interface Window {
9
+ Calendly?: {
10
+ initInlineWidget: (options: {
11
+ url: string;
12
+ parentElement: HTMLElement;
13
+ prefill?: Record<string, unknown>;
14
+ utm?: Record<string, unknown>;
15
+ }) => void;
16
+ };
17
+ }
18
+ }
19
+
20
+ export default function CalendlyEmbeds({ calendlyLink }: CalendlyEmbedsProps) {
21
+ const calendlyRef = useRef<HTMLDivElement | null>(null); // Typed ref
22
+ const [scriptLoaded, setScriptLoaded] = useState(false);
23
+
24
+ useEffect(() => {
25
+ // Only load the script if it's not already loaded
26
+ if (!window.Calendly) {
27
+ const script = document.createElement("script");
28
+ script.src = "https://assets.calendly.com/assets/external/widget.js";
29
+ script.async = true;
30
+ script.onload = () => setScriptLoaded(true);
31
+ document.body.appendChild(script);
32
+
33
+ return () => {
34
+ document.body.removeChild(script);
35
+ };
36
+ } else {
37
+ setScriptLoaded(true); // Calendly already loaded
38
+ }
39
+ }, []);
40
+
41
+ useEffect(() => {
42
+ if (scriptLoaded && calendlyLink && calendlyRef.current && window.Calendly) {
43
+ // Initialize Calendly widget once the script is loaded and the ref container is available
44
+ window.Calendly.initInlineWidget({
45
+ url: calendlyLink,
46
+ parentElement: calendlyRef.current,
47
+ prefill: {},
48
+ utm: {},
49
+ });
50
+ }
51
+ }, [scriptLoaded, calendlyLink]);
52
+
53
+ return (
54
+ <div
55
+ ref={calendlyRef}
56
+ style={{
57
+ minWidth: '320px',
58
+ height: '350px',
59
+ borderRadius: '8px',
60
+ boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)',
61
+ }}
62
+ >
63
+ {/* Calendly widget will be injected here */}
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,194 @@
1
+ import React from 'react';
2
+ import type { FC } from 'react';
3
+ import 'keen-slider/keen-slider.min.css';
4
+ import type { KeenSliderInstance } from 'keen-slider/react';
5
+
6
+
7
+ interface ProductVariant {
8
+ variant_id: string;
9
+ name: string;
10
+ image: string;
11
+ link: string;
12
+ price: string;
13
+ color?: string;
14
+ size?: string;
15
+ }
16
+
17
+ interface Props {
18
+ groupedProducts: Record<string, ProductVariant[]>;
19
+ selectedVariants: Record<string, string>;
20
+ setSelectedVariants: React.Dispatch<React.SetStateAction<Record<string, string>>>;
21
+ counts: Record<string, number>;
22
+ setCounts: React.Dispatch<React.SetStateAction<Record<string, number>>>;
23
+ handleAddToCart: (variantId: string, name: string, qty: number) => Promise<void>;
24
+ handleRemoveFromCart: (variantId: string, qty: number) => Promise<void>;
25
+ colorCode?: string;
26
+ shortenName: (name: string) => string;
27
+
28
+ sliderRef: React.Ref<HTMLDivElement>;
29
+ sliderInstance: React.MutableRefObject<KeenSliderInstance | null>;
30
+ currentSlide: number;
31
+ totalSlides: number;
32
+ }
33
+
34
+ const ProductCarousel: FC<Props> = ({
35
+ groupedProducts,
36
+ selectedVariants,
37
+ setSelectedVariants,
38
+ counts,
39
+ setCounts,
40
+ handleAddToCart,
41
+ handleRemoveFromCart,
42
+ colorCode = '#7F28F8',
43
+ shortenName,
44
+ sliderRef,
45
+ totalSlides,
46
+ }) => {
47
+
48
+
49
+ if (totalSlides === 0) return null;
50
+
51
+ return (
52
+ <div className="relative w-full">
53
+ {/* Slider container with padding for arrow overlay */}
54
+ <div ref={sliderRef} className="keen-slider relative px-8">
55
+ {Object.entries(groupedProducts).map(([tag, group]) => {
56
+ const currentVariantId = selectedVariants[tag] || group[0].variant_id;
57
+ const currentVariant = group.find(v => v.variant_id === currentVariantId) || group[0];
58
+ const count = counts[currentVariant.variant_id] || 0;
59
+
60
+ const colors = Array.from(
61
+ new Set(
62
+ group.map(v => v.color?.trim() || '').filter(c => c && c.toLowerCase() !== 'none')
63
+ )
64
+ );
65
+ const sizes = Array.from(
66
+ new Set(
67
+ group.map(v => v.size?.trim() || '').filter(s => s && s.toLowerCase() !== 'none')
68
+ )
69
+ );
70
+
71
+ const isComboValid = (color?: string, size?: string) =>
72
+ group.some(v => v.color === color && v.size === size);
73
+
74
+ const setActiveVariant = (color?: string, size?: string) => {
75
+ const match = group.find(
76
+ v => (!color || v.color === color) && (!size || v.size === size)
77
+ );
78
+ if (match) {
79
+ setSelectedVariants(prev => ({
80
+ ...prev,
81
+ [tag]: match.variant_id,
82
+ }));
83
+ }
84
+ };
85
+
86
+ const increment = async () => {
87
+ setCounts(prev => ({
88
+ ...prev,
89
+ [currentVariant.variant_id]: (prev[currentVariant.variant_id] || 0) + 1,
90
+ }));
91
+ await handleAddToCart(currentVariant.variant_id, currentVariant.name, 1);
92
+ };
93
+
94
+ const decrement = async () => {
95
+ if (count === 0) return;
96
+ setCounts(prev => ({
97
+ ...prev,
98
+ [currentVariant.variant_id]: prev[currentVariant.variant_id] - 1,
99
+ }));
100
+ await handleRemoveFromCart(currentVariant.variant_id, 1);
101
+ };
102
+
103
+ return (
104
+ <div key={tag} className="keen-slider__slide">
105
+ <div className="product-card">
106
+ <div className="product-image-container">
107
+ <img
108
+ src={currentVariant.image}
109
+ alt={currentVariant.name}
110
+ className="product-image"
111
+ onClick={() => window.open(currentVariant.link, '_blank')}
112
+ />
113
+ </div>
114
+ <div className="product-details">
115
+ <a
116
+ href={currentVariant.link}
117
+ target="_blank"
118
+ rel="noopener noreferrer"
119
+ className="product-name"
120
+ >
121
+ {shortenName(currentVariant.name)}
122
+ </a>
123
+ <p className="product-price">{currentVariant.price}</p>
124
+
125
+ {colors.length > 0 && (
126
+ <div>
127
+ <p className="label">Color:</p>
128
+ <div className="color-options">
129
+ {colors.map(color => {
130
+ const disabled = !isComboValid(color, currentVariant.size);
131
+ const isActive = color === currentVariant.color;
132
+ return (
133
+ <button
134
+ key={color}
135
+ disabled={disabled}
136
+ onClick={() => setActiveVariant(color, currentVariant.size)}
137
+ className={`color-button ${isActive ? 'active' : ''} ${disabled ? 'disabled' : ''}`}
138
+ style={isActive ? { backgroundColor: colorCode, color: '#fff' } : {}}
139
+ >
140
+ {color === 'None' ? 'One Color' : color}
141
+ </button>
142
+ );
143
+ })}
144
+ </div>
145
+ </div>
146
+ )}
147
+
148
+ {sizes.length > 0 && (
149
+ <div>
150
+ <p className="label">Size:</p>
151
+ <div className="size-options">
152
+ {sizes.map(size => {
153
+ const disabled = !isComboValid(currentVariant.color, size);
154
+ const isActive = size === currentVariant.size;
155
+ return (
156
+ <button
157
+ key={size}
158
+ disabled={disabled}
159
+ onClick={() => setActiveVariant(currentVariant.color, size)}
160
+ className={`size-button ${isActive ? 'active' : ''} ${disabled ? 'disabled' : ''}`}
161
+ style={isActive ? { backgroundColor: colorCode, color: '#fff' } : {}}
162
+ >
163
+ {size === 'None' ? 'One size' : size}
164
+ </button>
165
+ );
166
+ })}
167
+ </div>
168
+ </div>
169
+ )}
170
+
171
+ <div className="cart-controls">
172
+ <button onClick={decrement} className="cart-button">–</button>
173
+ <span className="cart-count">{count}</span>
174
+ <button
175
+ onClick={increment}
176
+ className="cart-button increment"
177
+ style={{ backgroundColor: colorCode, color: '#fff' }}
178
+ >
179
+ +
180
+ </button>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ );
186
+ })}
187
+
188
+
189
+ </div>
190
+ </div>
191
+ );
192
+ };
193
+
194
+ export default ProductCarousel;
@@ -0,0 +1,264 @@
1
+ import React, { useState } from "react";
2
+ import type { FC } from "react";
3
+ import "keen-slider/keen-slider.min.css";
4
+ import type { KeenSliderInstance } from "keen-slider/react";
5
+ import "./SallaSwiper.css";
6
+ import { Button } from "antd";
7
+ import TryOnBox from "./TryOn";
8
+
9
+ interface ProductVariant {
10
+ variant_id: string;
11
+ name: string;
12
+ image: string;
13
+ link: string;
14
+ price: string;
15
+ option?: string; // e.g. "Red - XL"
16
+ variant_tag_name: string;
17
+ tag?: string; // optional: "clothing" | "jewelry" | ...
18
+ }
19
+
20
+ interface Props {
21
+ groupedProducts: Record<string, ProductVariant[]>;
22
+ selectedVariants: Record<string, string>;
23
+ setSelectedVariants: React.Dispatch<React.SetStateAction<Record<string, string>>>;
24
+ counts: Record<string, number>;
25
+ setCounts: React.Dispatch<React.SetStateAction<Record<string, number>>>;
26
+ handleAddToCart: (variantId: string, name: string, qty?: number) => Promise<void>;
27
+ handleRemoveFromCart: (variantId: string, qty?: number) => Promise<void>;
28
+ colorCode?: string;
29
+ shortenName: (name: string) => string;
30
+ disableCheckout?: boolean;
31
+
32
+ // slider
33
+ sliderRef: React.Ref<HTMLDivElement>;
34
+ sliderInstance: React.MutableRefObject<KeenSliderInstance | null>;
35
+ currentSlide: number;
36
+ totalSlides: number;
37
+ }
38
+
39
+ const ProductCarousel: FC<Props> = ({
40
+ groupedProducts,
41
+ selectedVariants,
42
+ setSelectedVariants,
43
+ counts,
44
+ setCounts,
45
+ handleAddToCart,
46
+ handleRemoveFromCart,
47
+ colorCode = "#7F28F8",
48
+ shortenName,
49
+ sliderRef,
50
+ totalSlides,
51
+ disableCheckout = false,
52
+ }) => {
53
+ if (!totalSlides || totalSlides === 0) return null;
54
+
55
+ // track which variant is open for Try On
56
+ const [openTryOn, setOpenTryOn] = useState<string | null>(null);
57
+
58
+ return (
59
+ <section
60
+ className="sallaX"
61
+ style={{ "--sallaX-accent": colorCode } as React.CSSProperties}
62
+ aria-label="Recommended products"
63
+ >
64
+ <div ref={sliderRef} className="keen-slider sallaX-slider">
65
+ {Object.entries(groupedProducts).map(([tag, group]) => {
66
+ // figure out current active variant within this group
67
+ const currentVariantId =
68
+ selectedVariants[tag] || (group.length > 0 ? group[0].variant_id : "");
69
+ const currentVariant =
70
+ group.find(v => v.variant_id === currentVariantId) || group[0];
71
+ if (!currentVariant) return null;
72
+
73
+ const count = counts[currentVariant.variant_id] || 0;
74
+
75
+ // Build unique, cleaned list of options (exclude empty/'none')
76
+ const options = Array.from(
77
+ new Set(
78
+ group
79
+ .map(v => (v.option?.trim() ?? ""))
80
+ .filter(o => o && o.toLowerCase() !== "none")
81
+ )
82
+ );
83
+
84
+ // Update active variant by chosen option
85
+ const setActiveVariant = (option?: string) => {
86
+ const match = group.find(v => v.option === option);
87
+ if (match) {
88
+ setSelectedVariants(prev => ({
89
+ ...prev,
90
+ [tag]: match.variant_id,
91
+ }));
92
+ }
93
+ };
94
+
95
+ // Ensure select has a stable value (fallback to first option)
96
+ const selectValue = currentVariant.option ?? options[0] ?? "";
97
+
98
+ const increment = async () => {
99
+ setCounts(prev => ({
100
+ ...prev,
101
+ [currentVariant.variant_id]: (prev[currentVariant.variant_id] || 0) + 1,
102
+ }));
103
+ try {
104
+ // wait for backend to confirm success
105
+ await handleAddToCart(currentVariant.variant_id, currentVariant.name, 1);
106
+
107
+ // only update counts if the request succeeded
108
+
109
+ } catch (err: any) {
110
+ console.warn("Add to cart failed:", err.message);
111
+ setCounts(prev => ({
112
+ ...prev,
113
+ [currentVariant.variant_id]: (prev[currentVariant.variant_id] || 0) - 1,
114
+ }));
115
+ }
116
+ };
117
+
118
+
119
+ const decrement = async () => {
120
+ if (count === 0) return;
121
+ setCounts(prev => ({
122
+ ...prev,
123
+ [currentVariant.variant_id]: Math.max(
124
+ 0,
125
+ (prev[currentVariant.variant_id] || 0) - 1
126
+ ),
127
+ }));
128
+ await handleRemoveFromCart(currentVariant.variant_id, 1);
129
+ };
130
+
131
+ const isTryOnOpen = openTryOn === currentVariant.variant_id;
132
+ const canTryOn =
133
+ currentVariant.tag
134
+ ? currentVariant.tag.toLowerCase() === "clothing"
135
+ : true; // show by default unless you want to gate strictly by tag
136
+
137
+ return (
138
+ <article
139
+ key={tag}
140
+ className="keen-slider__slide sallaX-card"
141
+ role="group"
142
+ aria-roledescription="slide"
143
+ >
144
+ <div className="sallaX-media" onClick={() => window.open(currentVariant.link, "_blank")}>
145
+ <img
146
+ src={currentVariant.image}
147
+ alt={currentVariant.name}
148
+ className="sallaX-img"
149
+ loading="lazy"
150
+ />
151
+ <a
152
+ href={currentVariant.link}
153
+ target="_blank"
154
+ rel="noopener noreferrer"
155
+ className="sallaX-visit"
156
+ aria-label="Open product page"
157
+ onClick={(e) => e.stopPropagation()}
158
+ >
159
+ View
160
+ </a>
161
+ </div>
162
+
163
+ <div className="sallaX-body">
164
+ <a
165
+ href={currentVariant.link}
166
+ target="_blank"
167
+ rel="noopener noreferrer"
168
+ className="sallaX-name"
169
+ title={currentVariant.name}
170
+ >
171
+ {shortenName(currentVariant.name)}
172
+ </a>
173
+
174
+ <div className="sallaX-row">
175
+ <span className="sallaX-price" aria-label="Price">
176
+ {currentVariant.price}
177
+ </span>
178
+ </div>
179
+
180
+ {/* ✅ Options dropdown restored */}
181
+ {options.length > 1 && (
182
+ <div className="sallaX-selectWrap">
183
+ <label
184
+ htmlFor={`sallaX-select-${tag}`}
185
+ className="sallaX-selectLabel"
186
+ >
187
+ Choose an option
188
+ </label>
189
+ <select
190
+ id={`sallaX-select-${tag}`}
191
+ className="sallaX-select"
192
+ aria-label="Choose an option"
193
+ value={selectValue}
194
+ onChange={(e) => setActiveVariant(e.target.value)}
195
+ >
196
+ {options.map((option) => (
197
+ <option key={option} value={option}>
198
+ {option}
199
+ </option>
200
+ ))}
201
+ </select>
202
+ </div>
203
+ )}
204
+
205
+ <div className="flex items-center justify-between w-full space-x-4">
206
+ {!disableCheckout && (
207
+ <div className="sallaX-cta" aria-label="Quantity controls">
208
+ <button
209
+ type="button"
210
+ className="sallaX-qty sallaX-qty--minus"
211
+ onClick={decrement}
212
+ aria-label="Decrease quantity"
213
+ disabled={count === 0}
214
+ >
215
+
216
+ </button>
217
+ <output
218
+ className="sallaX-qtyCount"
219
+ aria-live="polite"
220
+ aria-atomic="true"
221
+ >
222
+ {count}
223
+ </output>
224
+ <button
225
+ type="button"
226
+ className="sallaX-qty sallaX-qty--plus"
227
+ onClick={increment}
228
+ aria-label="Increase quantity"
229
+ >
230
+ +
231
+ </button>
232
+ </div>
233
+ )}
234
+
235
+ {/* Try On toggle button (kept) */}
236
+ {canTryOn && (
237
+ <Button
238
+ style={{ backgroundColor: colorCode, color: "white" }}
239
+ className="px-6 py-2 rounded-lg shadow-md transition"
240
+ onClick={() =>
241
+ setOpenTryOn(isTryOnOpen ? null : currentVariant.variant_id)
242
+ }
243
+ >
244
+ {isTryOnOpen ? "Close Try On" : "Try On"}
245
+ </Button>
246
+ )}
247
+ </div>
248
+
249
+ {/* Try On panel */}
250
+ {canTryOn && isTryOnOpen && (
251
+ <div className="mt-4">
252
+ <TryOnBox productID={currentVariant.variant_id} productImage={currentVariant.image} />
253
+ </div>
254
+ )}
255
+ </div>
256
+ </article>
257
+ );
258
+ })}
259
+ </div>
260
+ </section>
261
+ );
262
+ };
263
+
264
+ export default ProductCarousel;