shopify-chatbot-widget 1.0.9 → 1.0.10
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/.env.development +2 -0
- package/dist/jaweb-chatbot.css +1 -1
- package/dist/jaweb-chatbot.iife.js +192 -599
- package/package.json +1 -1
- package/src/App.tsx +9 -0
- package/src/Chatbot/Chatbot.css +193 -113
- package/src/Chatbot/Chatbot.tsx +26 -19
- package/src/Chatbot/components/Cart/Index.tsx +10 -8
- package/src/Chatbot/components/ChatInput.tsx +11 -11
- package/src/Chatbot/components/Home/Home.css +456 -0
- package/src/Chatbot/components/Home/Home.tsx +293 -0
- package/src/Chatbot/components/Messages/Addcart.tsx +1 -1
- package/src/Chatbot/components/Messages/AssistantMesage.tsx +78 -37
- package/src/Chatbot/components/Messages/CardSwiper.tsx +1 -1
- package/src/Chatbot/components/Messages/Carousel/CardSwiper.tsx +87 -92
- package/src/Chatbot/components/Messages/Carousel/ProductSwiper.css +361 -0
- package/src/Chatbot/components/Messages/Carousel/TryOn.tsx +2 -2
- package/src/Chatbot/components/Messages/ChatMessages.css +32 -1
- package/src/Chatbot/components/Messages/ChatMessages.tsx +21 -38
- package/src/Chatbot/components/Messages/OrderCard.css +177 -0
- package/src/Chatbot/components/Messages/OrderCard.tsx +120 -0
- package/src/Chatbot/components/Messages/ProductCard.css +1 -1
- package/src/Chatbot/components/Messages/Support.tsx +311 -118
- package/src/Chatbot/components/Messages/Typing.css +31 -35
- package/src/Chatbot/components/Messages/Typing.tsx +19 -18
- package/src/Chatbot/components/Messenger/Messenger.tsx +67 -14
- package/src/Chatbot/components/Powered.css +26 -28
- package/src/Chatbot/components/Powered.tsx +1 -1
- package/src/Chatbot/components/Sessions/CreateSession.tsx +12 -10
- package/src/Chatbot/components/TryOn/View.tsx +2 -2
- package/src/Chatbot/components/VoiceMode.css +247 -0
- package/src/Chatbot/components/VoiceMode.tsx +613 -0
- package/src/hooks/config.tsx +9 -10
- package/src/hooks/useChatbotAPI.tsx +146 -61
- package/src/hooks/useChatbotData.tsx +8 -5
- package/src/hooks/useSessionMessages.tsx +0 -1
- package/src/hooks/useWebsocke.tsx +40 -18
- package/src/i18n.tsx +384 -10
- package/src/Chatbot/components/Messages/Carousel/CardSwiperSalla.tsx +0 -187
- package/src/Chatbot/components/Messages/Carousel/CardSwiperZid.tsx +0 -208
- package/src/Chatbot/components/Messages/Carousel/SallaSwiper.css +0 -245
- package/src/Chatbot/components/Messages/SallaAssistantMessage.tsx +0 -202
- package/src/Chatbot/components/Messages/ZidAssistantMesage.tsx +0 -203
- package/src/Chatbot/components/Sessions/RenderList.tsx +0 -519
package/src/i18n.tsx
CHANGED
|
@@ -1,30 +1,404 @@
|
|
|
1
|
-
// src/i18n.ts
|
|
2
1
|
import i18n from 'i18next';
|
|
3
2
|
import { initReactI18next } from 'react-i18next';
|
|
4
3
|
|
|
4
|
+
/** Visitor-facing widget languages. Persisted per browser; first visit
|
|
5
|
+
* follows the visitor's browser language. */
|
|
6
|
+
const LANG_KEY = 'jaweb_widget_lang';
|
|
7
|
+
export const WIDGET_LANGS = ['en', 'fr', 'ar'] as const;
|
|
8
|
+
export type WidgetLang = (typeof WIDGET_LANGS)[number];
|
|
9
|
+
const RTL_LANGS: string[] = ['ar'];
|
|
10
|
+
|
|
11
|
+
const detect = (): WidgetLang => {
|
|
12
|
+
try {
|
|
13
|
+
const stored = localStorage.getItem(LANG_KEY);
|
|
14
|
+
if (stored && (WIDGET_LANGS as readonly string[]).includes(stored)) return stored as WidgetLang;
|
|
15
|
+
} catch {
|
|
16
|
+
/* ignore */
|
|
17
|
+
}
|
|
18
|
+
const nav = (navigator.language || 'en').slice(0, 2).toLowerCase();
|
|
19
|
+
return (WIDGET_LANGS as readonly string[]).includes(nav) ? (nav as WidgetLang) : 'en';
|
|
20
|
+
};
|
|
21
|
+
|
|
5
22
|
const resources = {
|
|
6
23
|
en: {
|
|
7
24
|
translation: {
|
|
8
|
-
|
|
9
|
-
|
|
25
|
+
home: {
|
|
26
|
+
greeting: 'Hi there',
|
|
27
|
+
subCompany: "You're chatting with {{company}}. ",
|
|
28
|
+
sub: "Questions, orders, anything — we're here.",
|
|
29
|
+
startChat: 'Start a chat',
|
|
30
|
+
whatsapp: 'WhatsApp',
|
|
31
|
+
conversations: 'Conversations',
|
|
32
|
+
new: 'New',
|
|
33
|
+
newChat: 'New chat',
|
|
34
|
+
yourTickets: 'Your tickets',
|
|
35
|
+
ticketN: 'Ticket #{{id}}',
|
|
36
|
+
showLess: 'Show less',
|
|
37
|
+
showMore: 'Show {{count}} more',
|
|
38
|
+
tapToContinue: 'Tap to continue',
|
|
39
|
+
conversationEnded: 'Conversation ended',
|
|
40
|
+
newReplies_one: '{{count}} new reply',
|
|
41
|
+
newReplies_other: '{{count}} new replies',
|
|
42
|
+
closeChat: 'Close chat',
|
|
43
|
+
online: 'Online',
|
|
44
|
+
language: 'Language',
|
|
45
|
+
},
|
|
46
|
+
status: { Open: 'Open', 'In Progress': 'In Progress', Solved: 'Solved' },
|
|
47
|
+
time: { justNow: 'just now', m: '{{n}}m ago', h: '{{n}}h ago', d: '{{n}}d ago' },
|
|
48
|
+
messenger: {
|
|
49
|
+
onlineNow: 'Online now',
|
|
50
|
+
aiAssistant: 'Ai Assistant',
|
|
51
|
+
talkToUs: 'Talk to us',
|
|
52
|
+
voiceMode: 'Voice mode',
|
|
53
|
+
},
|
|
54
|
+
input: {
|
|
55
|
+
placeholder: 'Enter Message ...',
|
|
56
|
+
dropImage: 'Drop image here',
|
|
57
|
+
uploadImage: 'Upload image',
|
|
58
|
+
sendMessage: 'Send message',
|
|
59
|
+
recordVoice: 'Record voice',
|
|
60
|
+
stopRecording: 'Stop recording',
|
|
61
|
+
cancelRecording: 'Cancel recording',
|
|
62
|
+
imageTooBig: 'Image size should be less than 5MB',
|
|
63
|
+
recordingUnsupported: 'Voice recording is not supported in your browser',
|
|
64
|
+
micDenied: 'Microphone permission denied',
|
|
65
|
+
},
|
|
66
|
+
support: {
|
|
67
|
+
reaching: 'Reaching the team…',
|
|
68
|
+
requestSent: 'Your request has been sent',
|
|
69
|
+
connectingTo: 'Connecting you to {{name}}',
|
|
70
|
+
oneMoment: 'One moment…',
|
|
71
|
+
chattingWith: "You're chatting with {{name}}",
|
|
72
|
+
liveNow: 'Live now',
|
|
73
|
+
teamOffline: "The team isn't online right now",
|
|
74
|
+
savedSpot:
|
|
75
|
+
"We've saved your spot — we'll email you as soon as they're back so you can continue this chat",
|
|
76
|
+
ticketSubmitted: 'Ticket submitted',
|
|
77
|
+
teamMember: 'a team member',
|
|
78
|
+
},
|
|
79
|
+
order: {
|
|
80
|
+
order: 'Order',
|
|
81
|
+
placedOn: 'Placed {{date}}',
|
|
82
|
+
progress: 'Order progress',
|
|
83
|
+
stepPlaced: 'Placed',
|
|
84
|
+
stepPaid: 'Paid',
|
|
85
|
+
stepShipped: 'Shipped',
|
|
86
|
+
stepDelivered: 'Delivered',
|
|
87
|
+
delivered: 'Delivered',
|
|
88
|
+
onItsWay: 'On its way',
|
|
89
|
+
preparing: 'Preparing',
|
|
90
|
+
awaitingPayment: 'Awaiting payment',
|
|
91
|
+
trackPackage: 'Track package',
|
|
92
|
+
},
|
|
93
|
+
product: {
|
|
94
|
+
addToCart: 'Add to cart',
|
|
95
|
+
added: 'Added',
|
|
96
|
+
addedToCart: 'Added to cart',
|
|
97
|
+
tryOn: 'Try on',
|
|
98
|
+
view: 'View ↗',
|
|
99
|
+
recommended: 'Recommended products',
|
|
100
|
+
openProduct: 'Open product page',
|
|
101
|
+
chooseOption: 'Choose an option',
|
|
102
|
+
price: 'Price',
|
|
103
|
+
qty: 'Quantity controls',
|
|
104
|
+
decrease: 'Decrease quantity',
|
|
105
|
+
increase: 'Increase quantity',
|
|
106
|
+
},
|
|
107
|
+
cart: {
|
|
108
|
+
yourCart: 'Your Cart ({{count}})',
|
|
109
|
+
empty: 'Your cart is empty',
|
|
110
|
+
subtotal: 'Subtotal:',
|
|
111
|
+
qty: 'Qty: {{count}}',
|
|
112
|
+
checkout: 'CHECKOUT',
|
|
113
|
+
checkoutFailed: 'Checkout failed. Please try again.',
|
|
114
|
+
addFailed: 'Failed to add item',
|
|
115
|
+
},
|
|
116
|
+
voice: {
|
|
117
|
+
listening: 'Listening — just talk',
|
|
118
|
+
thinking: 'Thinking…',
|
|
119
|
+
speaking: '{{name}} is speaking — tap to interrupt',
|
|
120
|
+
tapToTalk: 'Tap the bars when you want to talk',
|
|
121
|
+
micNeeded: 'Microphone access is needed for voice mode',
|
|
122
|
+
didntCatch: "Didn't catch that — try again",
|
|
123
|
+
wentWrong: 'Something went wrong — tap the bars to retry',
|
|
124
|
+
title: 'Voice mode',
|
|
125
|
+
exit: 'Exit voice mode',
|
|
126
|
+
},
|
|
127
|
+
form: {
|
|
128
|
+
back: 'Back',
|
|
129
|
+
welcome: 'Welcome',
|
|
130
|
+
shareDetails: 'Please share a few details so we can get started.',
|
|
131
|
+
yourName: 'Your Name',
|
|
132
|
+
namePh: 'Enter your full name',
|
|
133
|
+
emailAddress: 'Email Address',
|
|
134
|
+
emailPh: 'Enter your email',
|
|
135
|
+
phoneNumber: 'Phone Number',
|
|
136
|
+
phonePh: 'Enter your phone number',
|
|
137
|
+
startChat: 'Start Chat',
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
fr: {
|
|
142
|
+
translation: {
|
|
143
|
+
home: {
|
|
144
|
+
greeting: 'Bonjour',
|
|
145
|
+
subCompany: 'Vous discutez avec {{company}}. ',
|
|
146
|
+
sub: 'Questions, commandes, tout — nous sommes là.',
|
|
147
|
+
startChat: 'Démarrer une discussion',
|
|
148
|
+
whatsapp: 'WhatsApp',
|
|
149
|
+
conversations: 'Conversations',
|
|
150
|
+
new: 'Nouveau',
|
|
151
|
+
newChat: 'Nouvelle discussion',
|
|
152
|
+
yourTickets: 'Vos tickets',
|
|
153
|
+
ticketN: 'Ticket n°{{id}}',
|
|
154
|
+
showLess: 'Voir moins',
|
|
155
|
+
showMore: 'Voir {{count}} de plus',
|
|
156
|
+
tapToContinue: 'Appuyez pour continuer',
|
|
157
|
+
conversationEnded: 'Conversation terminée',
|
|
158
|
+
newReplies_one: '{{count}} nouvelle réponse',
|
|
159
|
+
newReplies_other: '{{count}} nouvelles réponses',
|
|
160
|
+
closeChat: 'Fermer le chat',
|
|
161
|
+
online: 'En ligne',
|
|
162
|
+
language: 'Langue',
|
|
163
|
+
},
|
|
164
|
+
status: { Open: 'Ouvert', 'In Progress': 'En cours', Solved: 'Résolu' },
|
|
165
|
+
time: { justNow: "à l'instant", m: 'il y a {{n}} min', h: 'il y a {{n}} h', d: 'il y a {{n}} j' },
|
|
166
|
+
messenger: {
|
|
167
|
+
onlineNow: 'En ligne',
|
|
168
|
+
aiAssistant: 'Assistant IA',
|
|
169
|
+
talkToUs: 'Parlez-nous',
|
|
170
|
+
voiceMode: 'Mode vocal',
|
|
171
|
+
},
|
|
172
|
+
input: {
|
|
173
|
+
placeholder: 'Écrivez un message...',
|
|
174
|
+
dropImage: "Déposez l'image ici",
|
|
175
|
+
uploadImage: 'Importer une image',
|
|
176
|
+
sendMessage: 'Envoyer le message',
|
|
177
|
+
recordVoice: 'Enregistrer un vocal',
|
|
178
|
+
stopRecording: "Arrêter l'enregistrement",
|
|
179
|
+
cancelRecording: "Annuler l'enregistrement",
|
|
180
|
+
imageTooBig: "L'image doit faire moins de 5 Mo",
|
|
181
|
+
recordingUnsupported: "L'enregistrement vocal n'est pas pris en charge par votre navigateur",
|
|
182
|
+
micDenied: 'Autorisation du microphone refusée',
|
|
183
|
+
},
|
|
184
|
+
support: {
|
|
185
|
+
reaching: "Nous contactons l'équipe…",
|
|
186
|
+
requestSent: 'Votre demande a été envoyée',
|
|
187
|
+
connectingTo: 'Mise en relation avec {{name}}',
|
|
188
|
+
oneMoment: 'Un instant…',
|
|
189
|
+
chattingWith: 'Vous discutez avec {{name}}',
|
|
190
|
+
liveNow: 'En direct',
|
|
191
|
+
teamOffline: "L'équipe n'est pas en ligne pour le moment",
|
|
192
|
+
savedSpot:
|
|
193
|
+
'Votre place est réservée — nous vous enverrons un e-mail dès leur retour pour continuer cette discussion',
|
|
194
|
+
ticketSubmitted: 'Ticket envoyé',
|
|
195
|
+
teamMember: "un membre de l'équipe",
|
|
196
|
+
},
|
|
197
|
+
order: {
|
|
198
|
+
order: 'Commande',
|
|
199
|
+
placedOn: 'Passée le {{date}}',
|
|
200
|
+
progress: 'Suivi de commande',
|
|
201
|
+
stepPlaced: 'Passée',
|
|
202
|
+
stepPaid: 'Payée',
|
|
203
|
+
stepShipped: 'Expédiée',
|
|
204
|
+
stepDelivered: 'Livrée',
|
|
205
|
+
delivered: 'Livrée',
|
|
206
|
+
onItsWay: 'En route',
|
|
207
|
+
preparing: 'En préparation',
|
|
208
|
+
awaitingPayment: 'En attente de paiement',
|
|
209
|
+
trackPackage: 'Suivre le colis',
|
|
210
|
+
},
|
|
211
|
+
product: {
|
|
212
|
+
addToCart: 'Ajouter au panier',
|
|
213
|
+
added: 'Ajouté',
|
|
214
|
+
addedToCart: 'Ajouté au panier',
|
|
215
|
+
tryOn: 'Essayer',
|
|
216
|
+
view: 'Voir ↗',
|
|
217
|
+
recommended: 'Produits recommandés',
|
|
218
|
+
openProduct: 'Ouvrir la page produit',
|
|
219
|
+
chooseOption: 'Choisissez une option',
|
|
220
|
+
price: 'Prix',
|
|
221
|
+
qty: 'Contrôles de quantité',
|
|
222
|
+
decrease: 'Diminuer la quantité',
|
|
223
|
+
increase: 'Augmenter la quantité',
|
|
224
|
+
},
|
|
225
|
+
cart: {
|
|
226
|
+
yourCart: 'Votre panier ({{count}})',
|
|
227
|
+
empty: 'Votre panier est vide',
|
|
228
|
+
subtotal: 'Sous-total :',
|
|
229
|
+
qty: 'Qté : {{count}}',
|
|
230
|
+
checkout: 'PAYER',
|
|
231
|
+
checkoutFailed: 'Échec du paiement. Veuillez réessayer.',
|
|
232
|
+
addFailed: "Échec de l'ajout de l'article",
|
|
233
|
+
},
|
|
234
|
+
voice: {
|
|
235
|
+
listening: 'À votre écoute — parlez',
|
|
236
|
+
thinking: 'Réflexion…',
|
|
237
|
+
speaking: '{{name}} parle — touchez pour interrompre',
|
|
238
|
+
tapToTalk: 'Touchez les barres quand vous voulez parler',
|
|
239
|
+
micNeeded: "L'accès au microphone est requis pour le mode vocal",
|
|
240
|
+
didntCatch: "Nous n'avons pas compris — réessayez",
|
|
241
|
+
wentWrong: "Une erreur s'est produite — touchez les barres pour réessayer",
|
|
242
|
+
title: 'Mode vocal',
|
|
243
|
+
exit: 'Quitter le mode vocal',
|
|
244
|
+
},
|
|
245
|
+
form: {
|
|
246
|
+
back: 'Retour',
|
|
247
|
+
welcome: 'Bienvenue',
|
|
248
|
+
shareDetails: 'Partagez quelques informations pour commencer.',
|
|
249
|
+
yourName: 'Votre nom',
|
|
250
|
+
namePh: 'Saisissez votre nom complet',
|
|
251
|
+
emailAddress: 'Adresse e-mail',
|
|
252
|
+
emailPh: 'Saisissez votre e-mail',
|
|
253
|
+
phoneNumber: 'Numéro de téléphone',
|
|
254
|
+
phonePh: 'Saisissez votre numéro',
|
|
255
|
+
startChat: 'Démarrer la discussion',
|
|
256
|
+
},
|
|
10
257
|
},
|
|
11
258
|
},
|
|
12
259
|
ar: {
|
|
13
260
|
translation: {
|
|
14
|
-
|
|
15
|
-
|
|
261
|
+
home: {
|
|
262
|
+
greeting: 'مرحبًا',
|
|
263
|
+
subCompany: 'أنت تتحدث مع {{company}}. ',
|
|
264
|
+
sub: 'أسئلة، طلبات، أي شيء — نحن هنا.',
|
|
265
|
+
startChat: 'ابدأ محادثة',
|
|
266
|
+
whatsapp: 'واتساب',
|
|
267
|
+
conversations: 'المحادثات',
|
|
268
|
+
new: 'جديد',
|
|
269
|
+
newChat: 'محادثة جديدة',
|
|
270
|
+
yourTickets: 'تذاكرك',
|
|
271
|
+
ticketN: 'تذكرة رقم {{id}}',
|
|
272
|
+
showLess: 'عرض أقل',
|
|
273
|
+
showMore: 'عرض {{count}} أكثر',
|
|
274
|
+
tapToContinue: 'اضغط للمتابعة',
|
|
275
|
+
conversationEnded: 'انتهت المحادثة',
|
|
276
|
+
newReplies_zero: 'لا ردود جديدة',
|
|
277
|
+
newReplies_one: 'رد جديد واحد',
|
|
278
|
+
newReplies_two: 'ردان جديدان',
|
|
279
|
+
newReplies_few: '{{count}} ردود جديدة',
|
|
280
|
+
newReplies_many: '{{count}} ردًا جديدًا',
|
|
281
|
+
newReplies_other: '{{count}} رد جديد',
|
|
282
|
+
closeChat: 'إغلاق المحادثة',
|
|
283
|
+
online: 'متصل',
|
|
284
|
+
language: 'اللغة',
|
|
285
|
+
},
|
|
286
|
+
status: { Open: 'مفتوحة', 'In Progress': 'قيد المعالجة', Solved: 'محلولة' },
|
|
287
|
+
time: { justNow: 'الآن', m: 'منذ {{n}} د', h: 'منذ {{n}} س', d: 'منذ {{n}} ي' },
|
|
288
|
+
messenger: {
|
|
289
|
+
onlineNow: 'متصل الآن',
|
|
290
|
+
aiAssistant: 'المساعد الذكي',
|
|
291
|
+
talkToUs: 'تحدث معنا',
|
|
292
|
+
voiceMode: 'الوضع الصوتي',
|
|
293
|
+
},
|
|
294
|
+
input: {
|
|
295
|
+
placeholder: 'اكتب رسالة...',
|
|
296
|
+
dropImage: 'أفلت الصورة هنا',
|
|
297
|
+
uploadImage: 'رفع صورة',
|
|
298
|
+
sendMessage: 'إرسال الرسالة',
|
|
299
|
+
recordVoice: 'تسجيل صوتي',
|
|
300
|
+
stopRecording: 'إيقاف التسجيل',
|
|
301
|
+
cancelRecording: 'إلغاء التسجيل',
|
|
302
|
+
imageTooBig: 'يجب أن يكون حجم الصورة أقل من 5 ميجابايت',
|
|
303
|
+
recordingUnsupported: 'التسجيل الصوتي غير مدعوم في متصفحك',
|
|
304
|
+
micDenied: 'تم رفض إذن الميكروفون',
|
|
305
|
+
},
|
|
306
|
+
support: {
|
|
307
|
+
reaching: 'جارٍ الوصول إلى الفريق…',
|
|
308
|
+
requestSent: 'تم إرسال طلبك',
|
|
309
|
+
connectingTo: 'جارٍ توصيلك بـ {{name}}',
|
|
310
|
+
oneMoment: 'لحظة واحدة…',
|
|
311
|
+
chattingWith: 'أنت تتحدث مع {{name}}',
|
|
312
|
+
liveNow: 'مباشر الآن',
|
|
313
|
+
teamOffline: 'الفريق غير متصل حاليًا',
|
|
314
|
+
savedSpot: 'حفظنا مكانك — سنراسلك عبر البريد فور عودتهم لتكمل هذه المحادثة',
|
|
315
|
+
ticketSubmitted: 'تم إرسال التذكرة',
|
|
316
|
+
teamMember: 'أحد أعضاء الفريق',
|
|
317
|
+
},
|
|
318
|
+
order: {
|
|
319
|
+
order: 'الطلب',
|
|
320
|
+
placedOn: 'أُنشئ {{date}}',
|
|
321
|
+
progress: 'تتبع الطلب',
|
|
322
|
+
stepPlaced: 'أُنشئ',
|
|
323
|
+
stepPaid: 'مدفوع',
|
|
324
|
+
stepShipped: 'شُحن',
|
|
325
|
+
stepDelivered: 'وصل',
|
|
326
|
+
delivered: 'تم التوصيل',
|
|
327
|
+
onItsWay: 'في الطريق',
|
|
328
|
+
preparing: 'قيد التجهيز',
|
|
329
|
+
awaitingPayment: 'بانتظار الدفع',
|
|
330
|
+
trackPackage: 'تتبع الشحنة',
|
|
331
|
+
},
|
|
332
|
+
product: {
|
|
333
|
+
addToCart: 'أضف إلى السلة',
|
|
334
|
+
added: 'أُضيف',
|
|
335
|
+
addedToCart: 'أُضيف إلى السلة',
|
|
336
|
+
tryOn: 'جرّبه',
|
|
337
|
+
view: 'عرض ↗',
|
|
338
|
+
recommended: 'منتجات مقترحة',
|
|
339
|
+
openProduct: 'فتح صفحة المنتج',
|
|
340
|
+
chooseOption: 'اختر خيارًا',
|
|
341
|
+
price: 'السعر',
|
|
342
|
+
qty: 'التحكم بالكمية',
|
|
343
|
+
decrease: 'إنقاص الكمية',
|
|
344
|
+
increase: 'زيادة الكمية',
|
|
345
|
+
},
|
|
346
|
+
cart: {
|
|
347
|
+
yourCart: 'سلتك ({{count}})',
|
|
348
|
+
empty: 'سلتك فارغة',
|
|
349
|
+
subtotal: 'المجموع الفرعي:',
|
|
350
|
+
qty: 'الكمية: {{count}}',
|
|
351
|
+
checkout: 'إتمام الشراء',
|
|
352
|
+
checkoutFailed: 'فشل إتمام الشراء. حاول مجددًا.',
|
|
353
|
+
addFailed: 'فشلت إضافة المنتج',
|
|
354
|
+
},
|
|
355
|
+
voice: {
|
|
356
|
+
listening: 'نستمع إليك — تحدث',
|
|
357
|
+
thinking: 'جارٍ التفكير…',
|
|
358
|
+
speaking: '{{name}} يتحدث — اضغط للمقاطعة',
|
|
359
|
+
tapToTalk: 'اضغط على الأشرطة عندما تريد التحدث',
|
|
360
|
+
micNeeded: 'الوصول إلى الميكروفون مطلوب للوضع الصوتي',
|
|
361
|
+
didntCatch: 'لم نفهم ذلك — حاول مجددًا',
|
|
362
|
+
wentWrong: 'حدث خطأ — اضغط على الأشرطة لإعادة المحاولة',
|
|
363
|
+
title: 'الوضع الصوتي',
|
|
364
|
+
exit: 'الخروج من الوضع الصوتي',
|
|
365
|
+
},
|
|
366
|
+
form: {
|
|
367
|
+
back: 'رجوع',
|
|
368
|
+
welcome: 'مرحبًا',
|
|
369
|
+
shareDetails: 'شاركنا بعض البيانات لنبدأ.',
|
|
370
|
+
yourName: 'اسمك',
|
|
371
|
+
namePh: 'أدخل اسمك الكامل',
|
|
372
|
+
emailAddress: 'البريد الإلكتروني',
|
|
373
|
+
emailPh: 'أدخل بريدك الإلكتروني',
|
|
374
|
+
phoneNumber: 'رقم الهاتف',
|
|
375
|
+
phonePh: 'أدخل رقم هاتفك',
|
|
376
|
+
startChat: 'ابدأ المحادثة',
|
|
377
|
+
},
|
|
16
378
|
},
|
|
17
379
|
},
|
|
18
380
|
};
|
|
19
381
|
|
|
20
382
|
i18n.use(initReactI18next).init({
|
|
21
383
|
resources,
|
|
22
|
-
lng:
|
|
384
|
+
lng: detect(),
|
|
23
385
|
fallbackLng: 'en',
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
escapeValue: false, // React already escapes
|
|
27
|
-
},
|
|
386
|
+
interpolation: { escapeValue: false },
|
|
387
|
+
returnEmptyString: false,
|
|
28
388
|
});
|
|
29
389
|
|
|
390
|
+
export const setWidgetLang = (lng: WidgetLang) => {
|
|
391
|
+
void i18n.changeLanguage(lng);
|
|
392
|
+
try {
|
|
393
|
+
localStorage.setItem(LANG_KEY, lng);
|
|
394
|
+
} catch {
|
|
395
|
+
/* ignore */
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
/** Direction for the current widget language — applied on the widget
|
|
400
|
+
* containers only (never the host page). */
|
|
401
|
+
export const widgetDir = (lng: string): 'rtl' | 'ltr' =>
|
|
402
|
+
RTL_LANGS.includes(lng) ? 'rtl' : 'ltr';
|
|
403
|
+
|
|
30
404
|
export default i18n;
|
|
@@ -1,187 +0,0 @@
|
|
|
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
|
-
import './SallaSwiper.css';
|
|
6
|
-
import { Button } from 'antd';
|
|
7
|
-
|
|
8
|
-
interface ProductVariant {
|
|
9
|
-
variant_id: string;
|
|
10
|
-
name: string;
|
|
11
|
-
image: string;
|
|
12
|
-
link: string;
|
|
13
|
-
price: string;
|
|
14
|
-
option?: string; // e.g. "Red - XL" or "500ml"
|
|
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,variantImage:string,price:string, qty: number) => Promise<void>;
|
|
24
|
-
handleRemoveFromCart: (variantId: string, qty: number) => Promise<void>;
|
|
25
|
-
colorCode?: string; // accent
|
|
26
|
-
shortenName: (name: string) => string;
|
|
27
|
-
|
|
28
|
-
// slider
|
|
29
|
-
sliderRef: React.Ref<HTMLDivElement>;
|
|
30
|
-
sliderInstance: React.MutableRefObject<KeenSliderInstance | null>;
|
|
31
|
-
currentSlide: number;
|
|
32
|
-
totalSlides: number;
|
|
33
|
-
onTryOnSelect: (product: ProductVariant) => void;
|
|
34
|
-
enableTry:boolean;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const SallaProductCarousel: FC<Props> = ({
|
|
38
|
-
groupedProducts,
|
|
39
|
-
selectedVariants,
|
|
40
|
-
setSelectedVariants,
|
|
41
|
-
colorCode = '#7F28F8',
|
|
42
|
-
shortenName,
|
|
43
|
-
sliderRef,
|
|
44
|
-
totalSlides,
|
|
45
|
-
onTryOnSelect,
|
|
46
|
-
enableTry
|
|
47
|
-
}) => {
|
|
48
|
-
if (!totalSlides || totalSlides === 0) return null;
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<section
|
|
52
|
-
className="sallaX"
|
|
53
|
-
style={
|
|
54
|
-
{
|
|
55
|
-
// allow live accent theming from prop
|
|
56
|
-
// @ts-ignore
|
|
57
|
-
'--sallaX-accent': colorCode,
|
|
58
|
-
} as React.CSSProperties
|
|
59
|
-
}
|
|
60
|
-
aria-label="Recommended products"
|
|
61
|
-
>
|
|
62
|
-
<div ref={sliderRef} className="keen-slider sallaX-slider">
|
|
63
|
-
{Object.entries(groupedProducts).map(([tag, group]) => {
|
|
64
|
-
const currentVariantId =
|
|
65
|
-
selectedVariants[tag] || (group.length > 0 ? group[0].variant_id : '');
|
|
66
|
-
const currentVariant =
|
|
67
|
-
group.find(v => v.variant_id === currentVariantId) || group[0];
|
|
68
|
-
|
|
69
|
-
if (!currentVariant) return null;
|
|
70
|
-
|
|
71
|
-
// Build unique, cleaned list of options (exclude empty/'none')
|
|
72
|
-
const options = Array.from(
|
|
73
|
-
new Set(
|
|
74
|
-
group
|
|
75
|
-
.map(v => (v.option?.trim() ?? ''))
|
|
76
|
-
.filter(o => o && o.toLowerCase() !== 'none')
|
|
77
|
-
)
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
// Helper to update active variant by option
|
|
81
|
-
const setActiveVariant = (option?: string) => {
|
|
82
|
-
const match = group.find(v => v.option === option);
|
|
83
|
-
if (match) {
|
|
84
|
-
setSelectedVariants(prev => ({
|
|
85
|
-
...prev,
|
|
86
|
-
[tag]: match.variant_id,
|
|
87
|
-
}));
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// Ensure the select has a stable value (fallback to first option)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const selectValue = (currentVariant.option ?? options[0] ?? '');
|
|
96
|
-
|
|
97
|
-
return (
|
|
98
|
-
<article key={tag} className="keen-slider__slide sallaX-card" role="group" aria-roledescription="slide">
|
|
99
|
-
<div className="sallaX-media" onClick={() => window.open(currentVariant.link, '_blank')}>
|
|
100
|
-
<img
|
|
101
|
-
src={currentVariant.image}
|
|
102
|
-
alt={currentVariant.name}
|
|
103
|
-
className="sallaX-img"
|
|
104
|
-
loading="lazy"
|
|
105
|
-
/>
|
|
106
|
-
<a
|
|
107
|
-
href={currentVariant.link}
|
|
108
|
-
target="_blank"
|
|
109
|
-
rel="noopener noreferrer"
|
|
110
|
-
className="sallaX-visit"
|
|
111
|
-
aria-label="Open product page"
|
|
112
|
-
onClick={(e) => e.stopPropagation()}
|
|
113
|
-
>
|
|
114
|
-
View
|
|
115
|
-
</a>
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
<div className="sallaX-body">
|
|
119
|
-
<a
|
|
120
|
-
href={currentVariant.link}
|
|
121
|
-
target="_blank"
|
|
122
|
-
rel="noopener noreferrer"
|
|
123
|
-
className="sallaX-name"
|
|
124
|
-
title={currentVariant.name}
|
|
125
|
-
>
|
|
126
|
-
{shortenName(currentVariant.name)}
|
|
127
|
-
</a>
|
|
128
|
-
|
|
129
|
-
<div className="sallaX-row">
|
|
130
|
-
<span className="sallaX-price" aria-label="Price">
|
|
131
|
-
{currentVariant.price}
|
|
132
|
-
</span>
|
|
133
|
-
</div>
|
|
134
|
-
|
|
135
|
-
{/* NEW: Select-based options */}
|
|
136
|
-
{options.length > 0 && (
|
|
137
|
-
<div className="sallaX-selectWrap">
|
|
138
|
-
<label htmlFor={`sallaX-select-${tag}`} className="sallaX-selectLabel">
|
|
139
|
-
Choose an option
|
|
140
|
-
</label>
|
|
141
|
-
<select
|
|
142
|
-
id={`sallaX-select-${tag}`}
|
|
143
|
-
className="sallaX-select"
|
|
144
|
-
aria-label="Choose an option"
|
|
145
|
-
value={selectValue}
|
|
146
|
-
onChange={(e) => setActiveVariant(e.target.value)}
|
|
147
|
-
>
|
|
148
|
-
{options.map((option) => (
|
|
149
|
-
<option key={option} value={option}>
|
|
150
|
-
{option}
|
|
151
|
-
</option>
|
|
152
|
-
))}
|
|
153
|
-
</select>
|
|
154
|
-
</div>
|
|
155
|
-
)}
|
|
156
|
-
|
|
157
|
-
<div className="flex items-center justify-between w-full space-x-4">
|
|
158
|
-
|
|
159
|
-
{enableTry?
|
|
160
|
-
<Button
|
|
161
|
-
style={{ backgroundColor: colorCode, color: "white" }}
|
|
162
|
-
className="px-6 py-2 rounded-lg shadow-md transition"
|
|
163
|
-
onClick={() => {
|
|
164
|
-
onTryOnSelect(currentVariant);
|
|
165
|
-
}}
|
|
166
|
-
>
|
|
167
|
-
Try On
|
|
168
|
-
</Button>
|
|
169
|
-
:<></>
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
</div>
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
</div>
|
|
179
|
-
</article>
|
|
180
|
-
);
|
|
181
|
-
})}
|
|
182
|
-
</div>
|
|
183
|
-
</section>
|
|
184
|
-
);
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
export default SallaProductCarousel;
|