vue-hotel-search-widget 1.0.0

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.
@@ -0,0 +1,1410 @@
1
+ <template>
2
+ <div class="thsw-root sw-container" :style="themeStyle">
3
+ <div class="sw-card-box">
4
+ <form class="sw-srh-h-grid" @submit.prevent="handleSubmit">
5
+ <!-- Destination Field -->
6
+ <div
7
+ :class="[
8
+ 'sw-form-field',
9
+ 'sw-pos-rel',
10
+ 'br-left',
11
+ { 'thsw-field-error': fieldErrors.destination }
12
+ ]"
13
+ >
14
+ <label for="destination">Destination</label>
15
+ <div class="sw-custom-dropdown" ref="destinationDropdownRef">
16
+ <div class="sw-custom-dropdown-trigger">
17
+ <input
18
+ class="hotel-destination-input"
19
+ :value="destinationQuery"
20
+ @input="handleDestinationQueryChange"
21
+ @focus="setIsDestinationDropdownOpen(true)"
22
+ placeholder="Where are you going?"
23
+ />
24
+ </div>
25
+ <div v-if="isDestinationDropdownOpen" class="sw-dropdown">
26
+ <ul class="sw-dropdown-list">
27
+ <li class="sw-srh-loc" @click="handleAroundCurrentLocation">
28
+ <div class="sw-flex sw-align-center sw-gap-5 sw-justify-between">
29
+ <div class="sw-flex sw-align-center sw-gap-15">
30
+ <span class="sw-hotel-search-form-icon sw-fill">
31
+ <svg
32
+ width="26px"
33
+ height="26px"
34
+ fill="currentColor"
35
+ viewBox="0 0 24 24"
36
+ xmlns="http://www.w3.org/2000/svg"
37
+ data-testid="MyLocationIcon"
38
+ class="sw-bg-color-loc"
39
+ >
40
+ <path
41
+ fillRule="evenodd"
42
+ d="M12.0283 3a.7614.7614 0 0 1 .7614.7614v1.1544c3.328.3533 5.9726 2.9958 6.329 6.3228h1.1482a.7614.7614 0 0 1 0 1.5228h-1.1467c-.3505 3.3337-2.9978 5.9831-6.3305 6.3368v1.1404a.7614.7614 0 0 1-1.5228 0v-1.1404c-3.3327-.3537-5.98-3.0031-6.3306-6.3368H3.7897a.7614.7614 0 0 1 0-1.5228h1.1481c.3565-3.327 3.001-5.9696 6.3291-6.3228V3.7614A.7614.7614 0 0 1 12.0283 3m5.6086 9.007c0 3.0976-2.5111 5.6086-5.6086 5.6086s-5.6086-2.511-5.6086-5.6086 2.511-5.6085 5.6086-5.6085 5.6086 2.511 5.6086 5.6085m-3.5479-.0001c0 1.1381-.9226 2.0607-2.0607 2.0607-1.138 0-2.0606-.9226-2.0606-2.0607 0-1.138.9226-2.0606 2.0606-2.0606s2.0607.9226 2.0607 2.0606m1.5228 0c0 1.9791-1.6044 3.5835-3.5835 3.5835s-3.5834-1.6044-3.5834-3.5835 1.6044-3.5834 3.5834-3.5834 3.5835 1.6044 3.5835 3.5834"
43
+ clipRule="evenodd"
44
+ ></path>
45
+ </svg>
46
+ </span>
47
+ <h5 class="sw-fw500">Around Current Location</h5>
48
+ </div>
49
+ </div>
50
+ </li>
51
+ <li v-if="isDestinationLoading" class="sw-dropdown-list-item">
52
+ <div class="sw-dropdown-list-card">
53
+ <h6 class="sw-text-label">Searching...</h6>
54
+ </div>
55
+ </li>
56
+ <template
57
+ v-if="
58
+ !isDestinationLoading &&
59
+ destinationQuery.trim().length < MIN_SEARCH_CHARS
60
+ "
61
+ >
62
+ <li
63
+ v-for="option in STATIC_LOCATIONS"
64
+ :key="option.geoId"
65
+ class="sw-dropdown-list-item"
66
+ @click="handleDestinationSelect(option)"
67
+ >
68
+ <div class="sw-dropdown-list-card">
69
+ <span class="sw-hotel-search-form-icon">
70
+ <svg
71
+ width="30px"
72
+ height="30px"
73
+ fill="currentColor"
74
+ viewBox="0 0 24 24"
75
+ xmlns="http://www.w3.org/2000/svg"
76
+ data-testid="LocationIcon"
77
+ >
78
+ <path
79
+ d="M13.5 8.0056c0 .8294-.6716 1.5017-1.5 1.5017s-1.5-.6723-1.5-1.5017.6716-1.5017 1.5-1.5017 1.5.6723 1.5 1.5017"
80
+ ></path>
81
+ <path
82
+ fillRule="evenodd"
83
+ d="M12 17.0359c2.7614 0 5-6.2457 5-9.0102C17 5.261 14.7614 3.02 12 3.02S7 5.2611 7 8.0257c0 2.7645 2.2386 9.0102 5 9.0102m2.1803-4.1114C15.0205 11.1538 15.5 9.1236 15.5 8.0257 15.5 6.088 13.9314 4.52 12 4.52S8.5 6.088 8.5 8.0257c0 1.098.4795 3.1281 1.3197 4.8988.414.8726.8717 1.5863 1.3208 2.0583.4658.4896.7566.5531.8595.5531s.3937-.0635.8595-.5531c.4491-.472.9068-1.1857 1.3208-2.0583"
84
+ clipRule="evenodd"
85
+ ></path>
86
+ <path
87
+ d="M16.8904 14.2118c-.1965.4142.0122.9154.4396 1.0813q.2178.0845.4184.175c1.5348.6914 1.7516 1.3369 1.7516 1.4976 0 .1606-.2168.8061-1.7516 1.4975-1.394.628-3.425 1.0553-5.7484 1.0553s-4.3544-.4273-5.7484-1.0553C4.7168 17.7718 4.5 17.1263 4.5 16.9657s.2168-.8062 1.7516-1.4976a8.5 8.5 0 0 1 .4184-.175c.4274-.1659.6361-.6671.4396-1.0813-.1528-.322-.5183-.493-.8522-.3682C4.2674 14.5873 3 15.7098 3 16.9657c0 2.2392 4.0294 4.0545 9 4.0545s9-1.8153 9-4.0545c0-1.2559-1.2674-2.3784-3.2574-3.1221-.3339-.1248-.6994.0462-.8522.3682"
88
+ ></path>
89
+ </svg>
90
+ </span>
91
+ <div>
92
+ <h5 class="sw-fw500 sw-text-ellipsis">{{ option.geoTitle }}</h5>
93
+ <h6 class="sw-text-label sw-text-ellipsis">
94
+ {{ option.displayName }}
95
+ </h6>
96
+ </div>
97
+ </div>
98
+ </li>
99
+ </template>
100
+ <li
101
+ v-if="
102
+ !isDestinationLoading &&
103
+ destinationQuery.trim().length >= MIN_SEARCH_CHARS &&
104
+ destinationResults.length === 0
105
+ "
106
+ class="sw-dropdown-list-item"
107
+ >
108
+ <div class="sw-dropdown-list-card">
109
+ <h6 class="sw-text-label">No results found</h6>
110
+ </div>
111
+ </li>
112
+ <li
113
+ v-for="option in destinationResults"
114
+ :key="option.geoId"
115
+ class="sw-dropdown-list-item"
116
+ @click="handleDestinationSelect(option)"
117
+ >
118
+ <div class="sw-dropdown-list-card">
119
+ <span class="sw-hotel-search-form-icon">
120
+ <svg
121
+ width="30px"
122
+ height="30px"
123
+ fill="currentColor"
124
+ viewBox="0 0 24 24"
125
+ xmlns="http://www.w3.org/2000/svg"
126
+ data-testid="LocationIcon"
127
+ >
128
+ <path
129
+ d="M13.5 8.0056c0 .8294-.6716 1.5017-1.5 1.5017s-1.5-.6723-1.5-1.5017.6716-1.5017 1.5-1.5017 1.5.6723 1.5 1.5017"
130
+ ></path>
131
+ <path
132
+ fillRule="evenodd"
133
+ d="M12 17.0359c2.7614 0 5-6.2457 5-9.0102C17 5.261 14.7614 3.02 12 3.02S7 5.2611 7 8.0257c0 2.7645 2.2386 9.0102 5 9.0102m2.1803-4.1114C15.0205 11.1538 15.5 9.1236 15.5 8.0257 15.5 6.088 13.9314 4.52 12 4.52S8.5 6.088 8.5 8.0257c0 1.098.4795 3.1281 1.3197 4.8988.414.8726.8717 1.5863 1.3208 2.0583.4658.4896.7566.5531.8595.5531s.3937-.0635.8595-.5531c.4491-.472.9068-1.1857 1.3208-2.0583"
134
+ clipRule="evenodd"
135
+ ></path>
136
+ <path
137
+ d="M16.8904 14.2118c-.1965.4142.0122.9154.4396 1.0813q.2178.0845.4184.175c1.5348.6914 1.7516 1.3369 1.7516 1.4976 0 .1606-.2168.8061-1.7516 1.4975-1.394.628-3.425 1.0553-5.7484 1.0553s-4.3544-.4273-5.7484-1.0553C4.7168 17.7718 4.5 17.1263 4.5 16.9657s.2168-.8062 1.7516-1.4976a8.5 8.5 0 0 1 .4184-.175c.4274-.1659.6361-.6671.4396-1.0813-.1528-.322-.5183-.493-.8522-.3682C4.2674 14.5873 3 15.7098 3 16.9657c0 2.2392 4.0294 4.0545 9 4.0545s9-1.8153 9-4.0545c0-1.2559-1.2674-2.3784-3.2574-3.1221-.3339-.1248-.6994.0462-.8522.3682"
138
+ ></path>
139
+ </svg>
140
+ </span>
141
+ <div>
142
+ <h5 class="sw-fw500 sw-text-ellipsis">{{ option.geoTitle }}</h5>
143
+ <h6 class="sw-text-label sw-text-ellipsis">
144
+ {{ option.displayName }}
145
+ </h6>
146
+ </div>
147
+ </div>
148
+ </li>
149
+ </ul>
150
+ </div>
151
+ </div>
152
+ </div>
153
+
154
+ <!-- Nationality Field -->
155
+ <div
156
+ :class="[
157
+ 'sw-form-field',
158
+ 'sw-pos-rel',
159
+ { 'thsw-field-error': fieldErrors.nationality }
160
+ ]"
161
+ >
162
+ <label for="nationality">Nationality</label>
163
+ <div class="sw-custom-dropdown" ref="nationalityDropdownRef">
164
+ <div class="sw-custom-dropdown-trigger">
165
+ <input
166
+ class="hotel-destination-input"
167
+ :value="nationalityQuery"
168
+ @input="handleNationalityQueryChange"
169
+ @focus="setIsNationalityDropdownOpen(true)"
170
+ :placeholder="formData.nationality"
171
+ />
172
+ </div>
173
+ <div v-if="isNationalityDropdownOpen" class="sw-dropdown">
174
+ <ul class="sw-dropdown-list">
175
+ <li v-if="isCountryLoading" class="sw-dropdown-list-item">
176
+ <div class="sw-dropdown-list-box">
177
+ <div>
178
+ <h5 class="sw-fw500 sw-text-ellipsis">Searching...</h5>
179
+ </div>
180
+ </div>
181
+ </li>
182
+ <template
183
+ v-if="
184
+ !isCountryLoading &&
185
+ nationalityQuery.trim().length < MIN_SEARCH_CHARS
186
+ "
187
+ >
188
+ <li
189
+ v-for="option in STATIC_COUNTRIES"
190
+ :key="option.country_id"
191
+ class="sw-dropdown-list-item"
192
+ @click="handleNationalitySelect(option)"
193
+ >
194
+ <div class="sw-dropdown-list-box">
195
+ <div>
196
+ <h5 class="sw-fw500 sw-text-ellipsis">
197
+ {{ option.country_name }}
198
+ </h5>
199
+ </div>
200
+ <h6 class="sw-dropdown-item-badge sw-en-font">
201
+ {{ option.country_code }}
202
+ </h6>
203
+ </div>
204
+ </li>
205
+ </template>
206
+ <li
207
+ v-if="
208
+ !isCountryLoading &&
209
+ nationalityQuery.trim().length >= MIN_SEARCH_CHARS &&
210
+ countryResults.length === 0
211
+ "
212
+ class="sw-dropdown-list-item"
213
+ >
214
+ <div class="sw-dropdown-list-box">
215
+ <div>
216
+ <h5 class="sw-fw500 sw-text-ellipsis">No results found</h5>
217
+ </div>
218
+ </div>
219
+ </li>
220
+ <template
221
+ v-if="
222
+ !isCountryLoading &&
223
+ nationalityQuery.trim().length >= MIN_SEARCH_CHARS
224
+ "
225
+ >
226
+ <li
227
+ v-for="option in filteredNationalityOptions"
228
+ :key="option.country_id"
229
+ class="sw-dropdown-list-item"
230
+ @click="handleNationalitySelect(option)"
231
+ >
232
+ <div class="sw-dropdown-list-box">
233
+ <div>
234
+ <h5 class="sw-fw500 sw-text-ellipsis">
235
+ {{ option.country_name }}
236
+ </h5>
237
+ </div>
238
+ <h6 class="sw-dropdown-item-badge sw-en-font">
239
+ {{ option.country_code }}
240
+ </h6>
241
+ </div>
242
+ </li>
243
+ </template>
244
+ </ul>
245
+ </div>
246
+ </div>
247
+ </div>
248
+
249
+ <!-- Check-in/Check-out Dates -->
250
+ <div class="sw-srh-wrap">
251
+ <div
252
+ :class="[
253
+ 'sw-form-field',
254
+ 'sw-pos-rel',
255
+ 'sw-com-cal',
256
+ { 'thsw-field-error': fieldErrors.checkIn }
257
+ ]"
258
+ >
259
+ <label for="checkIn">Check-in</label>
260
+ <input
261
+ id="checkIn"
262
+ class="hotel-destination-input"
263
+ readonly
264
+ :value="formatDate(formData.checkIn)"
265
+ placeholder="Select date"
266
+ @click="showCalendar"
267
+ />
268
+ <Calendar
269
+ ref="calendarRef"
270
+ v-model="dateRange"
271
+ selectionMode="range"
272
+ :numberOfMonths="2"
273
+ :minDate="new Date()"
274
+ dateFormat="dd M yy"
275
+ class="thsw-range-calendar"
276
+ @update:modelValue="handleDateRangeChange"
277
+ />
278
+ </div>
279
+ <div
280
+ :class="[
281
+ 'sw-form-field',
282
+ 'sw-pos-rel',
283
+ 'sw-com-cal',
284
+ { 'thsw-field-error': fieldErrors.checkOut }
285
+ ]"
286
+ >
287
+ <label for="checkOut">Check-out</label>
288
+ <input
289
+ id="checkOut"
290
+ class="hotel-destination-input"
291
+ readonly
292
+ :value="formatDate(formData.checkOut)"
293
+ placeholder="Select date"
294
+ @click="showCalendar"
295
+ />
296
+ </div>
297
+ </div>
298
+
299
+ <!-- Rooms & Guests -->
300
+ <div class="sw-form-field">
301
+ <label for="rooms">Rooms & Guests</label>
302
+ <div class="sw-custom-dropdown" ref="roomsGuestsMenuRef">
303
+ <div
304
+ class="sw-custom-dropdown-trigger"
305
+ @click="setIsRoomsGuestsMenuOpen(!isRoomsGuestsMenuOpen)"
306
+ >
307
+ <span>{{ formData.roomsGuests }}</span>
308
+ </div>
309
+ <div v-if="isRoomsGuestsMenuOpen" class="sw-rooms-guests-menu">
310
+ <h3 class="sw-rooms-guests-title">Number of Travellers</h3>
311
+ <div
312
+ v-for="(room, index) in rooms"
313
+ :key="index"
314
+ class="sw-room-section"
315
+ >
316
+ <div class="sw-room-header">
317
+ <svg
318
+ width="24"
319
+ height="24"
320
+ viewBox="0 0 24 24"
321
+ version="1.1"
322
+ xmlns="http://www.w3.org/2000/svg"
323
+ >
324
+ <g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
325
+ <g transform="translate(-4092.000000, -824.000000)">
326
+ <g transform="translate(4092.000000, 824.000000)">
327
+ <rect
328
+ fill="#FFFFFF"
329
+ opacity="0"
330
+ x="0"
331
+ y="0"
332
+ width="24"
333
+ height="24"
334
+ ></rect>
335
+ <g
336
+ transform="translate(3.000000, 6.000000)"
337
+ fill="var(--primary)"
338
+ fillRule="nonzero"
339
+ >
340
+ <path
341
+ d="M18.011072,5.62815523 L18.011072,1.13048375 C18.011072,0.514180505 17.4892314,0.012765343 16.8478226,0.012765343 L2.14333676,0.012765343 C1.50192802,0.012765343 0.980087404,0.514180505 0.980087404,1.13048375 L0.980087404,5.62820217 C0.42596144,5.70920578 0,6.17002527 0,6.72475451 L0,9.50863177 C0,9.70302166 0.164015424,9.86061733 0.366323907,9.86061733 L0.75877892,9.86061733 L0.75877892,11.5970794 C0.75877892,11.7914693 0.922794344,11.949065 1.12510283,11.949065 L1.78639075,11.949065 L1.78639075,12.6268014 C1.78639075,12.8211913 1.95040617,12.978787 2.15271465,12.978787 C2.35502314,12.978787 2.51903856,12.8211913 2.51903856,12.6268014 L2.51903856,11.949065 L16.4721697,11.949065 L16.4721697,12.6268014 C16.4721697,12.8211913 16.6361362,12.978787 16.8384936,12.978787 C17.0408509,12.978787 17.2048175,12.8211913 17.2048175,12.6268014 L17.2048175,11.949065 L17.8661054,11.949065 C18.0684627,11.949065 18.2324293,11.7914693 18.2324293,11.5970794 L18.2324293,9.86061733 L18.6248355,9.86061733 C18.8271928,9.86061733 18.9911594,9.70302166 18.9911594,9.50863177 L18.9911594,6.72475451 C18.9911594,6.17002527 18.5651979,5.70915884 18.011072,5.62815523 Z M1.71268638,1.13048375 C1.71268638,0.902350181 1.90586118,0.716736462 2.14328792,0.716736462 L16.8477738,0.716736462 C17.0852005,0.716736462 17.2783753,0.902350181 17.2783753,1.13048375 L17.2783753,5.61553069 L15.99209,5.61553069 C16.2480283,5.35079061 16.5083625,5.0723935 16.7779769,4.77451986 C16.8978869,4.64203249 16.897838,4.44492058 16.777928,4.31248014 C15.7181285,3.14210469 14.8307943,2.28950181 13.6130848,1.27151264 C13.4752494,1.1562491 13.2700591,1.1562491 13.1321748,1.27151264 C11.9144165,2.28954874 11.0271311,3.14215162 9.96738046,4.31248014 C9.84747044,4.44492058 9.84742159,4.64203249 9.96733162,4.77451986 C10.2369949,5.07244043 10.4972802,5.35083755 10.7532185,5.61553069 L8.23764781,5.61553069 C8.49358612,5.35079061 8.75396915,5.0723935 9.02358355,4.77451986 C9.14349357,4.64203249 9.14349357,4.44492058 9.0235347,4.31248014 C7.96388175,3.14229242 7.07654756,2.28968953 5.85869152,1.27151264 C5.72085604,1.1562491 5.51566581,1.1562491 5.37778149,1.27151264 C4.15992545,2.28968953 3.27259126,3.14229242 2.2129383,4.31248014 C2.09297943,4.44492058 2.09297943,4.64203249 2.21288946,4.77451986 C2.4825527,5.0723935 2.74288689,5.35079061 2.99882519,5.61553069 L1.71268638,5.61553069 L1.71268638,1.13048375 Z M11.7640283,5.61553069 C11.4269614,5.28306859 11.0864756,4.92981588 10.7309949,4.54352347 C11.6067044,3.59142599 12.3820411,2.84643682 13.3726787,2.00519134 C14.3633162,2.84638989 15.138653,3.59142599 16.0144113,4.54352347 C15.6589794,4.92981588 15.3184447,5.28306859 14.9813779,5.61553069 L11.7640283,5.61553069 Z M4.0096838,5.61553069 C3.67261697,5.28302166 3.33208226,4.92981588 2.97660154,4.54352347 C3.85216452,3.59161372 4.62764781,2.84648375 5.61833419,2.00519134 C6.60902057,2.84648375 7.38450386,3.59161372 8.26006684,4.54352347 C7.90463496,4.92981588 7.56410026,5.28302166 7.22698458,5.61553069 L4.0096838,5.61553069 Z M17.4997815,11.2450939 L1.49142674,11.2450939 L1.49142674,9.86061733 L17.4997815,9.86061733 L17.4997815,11.2450939 Z M18.2585116,9.15664621 L0.732647815,9.15664621 L0.732647815,6.72475451 C0.732647815,6.50131408 0.921866324,6.31950181 1.15445758,6.31950181 L17.8367018,6.31950181 C18.0692931,6.31950181 18.2585116,6.50131408 18.2585116,6.72475451 L18.2585116,9.15664621 Z"
342
+ ></path>
343
+ </g>
344
+ </g>
345
+ </g>
346
+ </g>
347
+ </svg>
348
+ Room {{ index + 1 }}
349
+ </div>
350
+ <div class="sw-room-controls-group">
351
+ <div class="sw-room-control-item">
352
+ <div class="sw-room-control-label">
353
+ <span>Adult (18+ years)</span>
354
+ </div>
355
+ <div class="sw-room-control-buttons">
356
+ <button
357
+ type="button"
358
+ :class="[
359
+ 'sw-count-button',
360
+ room.adults === 1 ? 'sw-disabled' : 'sw-active'
361
+ ]"
362
+ @click="decrementAdults(index)"
363
+ :disabled="room.adults === 1"
364
+ >
365
+ <svg
366
+ xmlns="http://www.w3.org/2000/svg"
367
+ height="20"
368
+ fill="var(--sw-white)"
369
+ viewBox="0 -960 960 960"
370
+ >
371
+ <path d="M200-440v-80h560v80H200Z"></path>
372
+ </svg>
373
+ </button>
374
+ <span class="sw-count-value">{{ room.adults }}</span>
375
+ <button
376
+ type="button"
377
+ :class="[
378
+ 'sw-count-button',
379
+ room.adults >= MAX_ADULTS_PER_ROOM
380
+ ? 'sw-disabled'
381
+ : 'sw-active'
382
+ ]"
383
+ @click="incrementAdults(index)"
384
+ :disabled="room.adults >= MAX_ADULTS_PER_ROOM"
385
+ >
386
+ <svg
387
+ xmlns="http://www.w3.org/2000/svg"
388
+ height="20"
389
+ fill="var(--sw-white)"
390
+ viewBox="0 -960 960 960"
391
+ >
392
+ <path
393
+ d="M440-440H200v-80h240v-240h80v240h240v80H520v240h-80v-240Z"
394
+ ></path>
395
+ </svg>
396
+ </button>
397
+ </div>
398
+ </div>
399
+ <div class="sw-room-control-item">
400
+ <div class="sw-room-control-label">
401
+ <span>Child (0-17 years)</span>
402
+ </div>
403
+ <div class="sw-room-control-buttons">
404
+ <button
405
+ type="button"
406
+ :class="[
407
+ 'sw-count-button',
408
+ room.children === 0 ? 'sw-disabled' : 'sw-active'
409
+ ]"
410
+ @click="decrementChildren(index)"
411
+ :disabled="room.children === 0"
412
+ >
413
+ <svg
414
+ xmlns="http://www.w3.org/2000/svg"
415
+ height="20"
416
+ fill="var(--sw-white)"
417
+ viewBox="0 -960 960 960"
418
+ >
419
+ <path d="M200-440v-80h560v80H200Z"></path>
420
+ </svg>
421
+ </button>
422
+ <span class="sw-count-value">{{ room.children }}</span>
423
+ <button
424
+ type="button"
425
+ :class="[
426
+ 'sw-count-button',
427
+ room.children >= 4 ? 'sw-disabled' : ''
428
+ ]"
429
+ @click="incrementChildren(index)"
430
+ :disabled="room.children >= 4"
431
+ >
432
+ <svg
433
+ xmlns="http://www.w3.org/2000/svg"
434
+ height="20"
435
+ fill="var(--sw-white)"
436
+ viewBox="0 -960 960 960"
437
+ >
438
+ <path
439
+ d="M440-440H200v-80h240v-240h80v240h240v80H520v240h-80v-240Z"
440
+ ></path>
441
+ </svg>
442
+ </button>
443
+ </div>
444
+ </div>
445
+ </div>
446
+ <div class="">
447
+ <div v-if="room.children > 0" class="sw-room-controls-child">
448
+ <span
449
+ v-for="(_, childIndex) in Array.from({ length: room.children })"
450
+ :key="childIndex"
451
+ class="sw-room-control-label"
452
+ >
453
+ Child {{ childIndex + 1 }}
454
+ </span>
455
+ </div>
456
+ <div class="sw-room-controls-child">
457
+ <div
458
+ v-for="(_, childIndex) in Array.from({ length: room.children })"
459
+ :key="childIndex"
460
+ class="sw-child-age-item"
461
+ >
462
+ <Dropdown
463
+ :modelValue="room.childAges[childIndex] ?? 0"
464
+ :options="childAgeOptions"
465
+ @update:modelValue="
466
+ (value) => updateChildAge(index, childIndex, value)
467
+ "
468
+ placeholder="Age"
469
+ class="child-age-dropdown"
470
+ />
471
+ </div>
472
+ </div>
473
+ </div>
474
+ <button
475
+ v-if="rooms.length > 1 && index > 0"
476
+ type="button"
477
+ class="sw-remove-room-button"
478
+ @click="handleRemoveRoom(index)"
479
+ :aria-label="`Remove Room ${index + 1}`"
480
+ >
481
+ ×
482
+ </button>
483
+ </div>
484
+ <div class="sw-rooms-guests-footer">
485
+ <button
486
+ v-if="rooms.length < 6"
487
+ type="button"
488
+ class="sw-add-room-button"
489
+ @click="handleAddRoom"
490
+ >
491
+ <svg
492
+ width="14"
493
+ height="14"
494
+ viewBox="0 0 16 16"
495
+ xmlns="http://www.w3.org/2000/svg"
496
+ >
497
+ <path fill="none" d="M0 0h16v16H0z"></path>
498
+ <path d="M15 7H9V1H7v6H1v2h6v6h2V9h6z"></path>
499
+ </svg>
500
+ Add another room
501
+ </button>
502
+ <div class="sw-rooms-guests-cta">
503
+ <button
504
+ type="button"
505
+ class="sw-reset-button"
506
+ @click="resetRooms"
507
+ >
508
+ Reset
509
+ </button>
510
+ <button
511
+ type="button"
512
+ class="sw-apply-button"
513
+ @click="setIsRoomsGuestsMenuOpen(false)"
514
+ >
515
+ Apply
516
+ </button>
517
+ </div>
518
+ </div>
519
+ </div>
520
+ </div>
521
+ </div>
522
+
523
+ <!-- Search Button -->
524
+ <Button
525
+ type="submit"
526
+ label="Search"
527
+ icon="pi pi-search"
528
+ class="sw-search-button br-right"
529
+ />
530
+ </form>
531
+
532
+ <!-- Advanced Search Options -->
533
+ <div class="sw-advanced-search-options">
534
+ <Accordion>
535
+ <AccordionTab header="Advanced Search options">
536
+ <div class="sw-hsrh-advan-cont">
537
+ <h5>Star Rating:</h5>
538
+ <div class="sw-hsrh-chk-main">
539
+ <label
540
+ v-for="rating in [5, 4, 3, 2, 1]"
541
+ :key="rating"
542
+ class="sw-star-checkbox-wrap sw-checkbox-wrap"
543
+ >
544
+ <input
545
+ type="checkbox"
546
+ :checked="selectedStarRatings.includes(rating)"
547
+ @change="handleStarRatingToggle(rating)"
548
+ />
549
+ <div class="sw-star-checkbox">
550
+ <span class="sw-en-font">{{ rating }}</span>
551
+ <svg
552
+ width="14"
553
+ height="14"
554
+ viewBox="0 0 18 17"
555
+ xmlns="http://www.w3.org/2000/svg"
556
+ >
557
+ <path
558
+ d="M3.35375 16.6825L4.7865 10.5133L0 6.36525L6.31525 5.81725L8.77875 0L11.2422 5.81725L17.5575 6.36525L12.771 10.5133L14.2038 16.6825L8.77875 13.4095L3.35375 16.6825Z"
559
+ fill="#ffae43"
560
+ />
561
+ </svg>
562
+ </div>
563
+ </label>
564
+ </div>
565
+ </div>
566
+ <div class="sw-hsrh-advan-cont">
567
+ <h5>Meal Type:</h5>
568
+ <div class="sw-hsrh-chk-main">
569
+ <label
570
+ v-for="mealType in mealTypeOptions"
571
+ :key="mealType.value"
572
+ class="sw-checkbox-wrap"
573
+ >
574
+ <input
575
+ type="checkbox"
576
+ :checked="selectedMealTypes.includes(mealType.value)"
577
+ @change="handleMealTypeToggle(mealType.value)"
578
+ />
579
+ <div class="sw-star-checkbox">{{ mealType.label }}</div>
580
+ </label>
581
+ </div>
582
+ </div>
583
+ </AccordionTab>
584
+ </Accordion>
585
+ </div>
586
+ </div>
587
+ </div>
588
+ </template>
589
+
590
+ <script setup>
591
+ import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
592
+ import Button from 'primevue/button'
593
+ import Dropdown from 'primevue/dropdown'
594
+ import Calendar from 'primevue/calendar'
595
+ import Accordion from 'primevue/accordion'
596
+ import AccordionTab from 'primevue/accordiontab'
597
+ import { JSEncrypt } from 'jsencrypt'
598
+ import { hotelSearchWidgetStyles } from './hotel-search-widget.styles.js'
599
+
600
+ const props = defineProps({
601
+ config: {
602
+ type: Object,
603
+ default: () => ({})
604
+ },
605
+ minWidth: {
606
+ type: [Number, String],
607
+ default: undefined
608
+ }
609
+ })
610
+
611
+ const STYLE_ELEMENT_ID = 'thsw-hotel-styles'
612
+ const PRIMEICONS_LINK_ID = 'thsw-primeicons'
613
+ const MIN_SEARCH_CHARS = 3
614
+ const MAX_ADULTS_PER_ROOM = 6
615
+
616
+ const STATIC_LOCATIONS = [
617
+ {
618
+ geoId: '618704',
619
+ geoTitle: 'Dubai',
620
+ geoName: 'United Arab Emirates',
621
+ displayName: 'Dubai, United Arab Emirates',
622
+ type: 'city',
623
+ countryCode: 'AE',
624
+ lattitude: 25.27063,
625
+ longitude: 55.30037
626
+ },
627
+ {
628
+ geoId: '804926',
629
+ geoTitle: 'Dubai Hills',
630
+ geoName: 'Dubai, United Arab Emirates',
631
+ displayName: 'Dubai Hills, Dubai, United Arab Emirates',
632
+ type: 'neighborhood',
633
+ countryCode: 'AE',
634
+ lattitude: 25.103307,
635
+ longitude: 55.247966
636
+ },
637
+ {
638
+ geoId: '618818',
639
+ geoTitle: 'Dubai Media City',
640
+ geoName: 'Dubai, United Arab Emirates',
641
+ displayName: 'Dubai Media City, Dubai, United Arab Emirates',
642
+ type: 'neighborhood',
643
+ countryCode: 'AE',
644
+ lattitude: 25.094279,
645
+ longitude: 55.154095
646
+ },
647
+ {
648
+ geoId: '618493',
649
+ geoTitle: 'Dubai Internet City',
650
+ geoName: 'Dubai, United Arab Emirates',
651
+ displayName: 'Dubai Internet City, Dubai, United Arab Emirates',
652
+ type: 'neighborhood',
653
+ countryCode: 'AE',
654
+ lattitude: 25.097792,
655
+ longitude: 55.163026
656
+ },
657
+ {
658
+ geoId: '618494',
659
+ geoTitle: 'Dubai Studio City',
660
+ geoName: 'Dubai, United Arab Emirates',
661
+ displayName: 'Dubai Studio City, Dubai, United Arab Emirates',
662
+ type: 'neighborhood',
663
+ countryCode: 'AE',
664
+ lattitude: 25.041623,
665
+ longitude: 55.251897
666
+ },
667
+ {
668
+ geoId: '618670',
669
+ geoTitle: 'Dubai Sports City',
670
+ geoName: 'Dubai, United Arab Emirates',
671
+ displayName: 'Dubai Sports City, Dubai, United Arab Emirates',
672
+ type: 'neighborhood',
673
+ countryCode: 'AE',
674
+ lattitude: 25.036236,
675
+ longitude: 55.217676
676
+ },
677
+ {
678
+ geoId: '618631',
679
+ geoTitle: 'Dubai Production City',
680
+ geoName: 'Dubai, United Arab Emirates',
681
+ displayName: 'Dubai Production City, Dubai, United Arab Emirates',
682
+ type: 'neighborhood',
683
+ countryCode: 'AE',
684
+ lattitude: 25.032507,
685
+ longitude: 55.189725
686
+ },
687
+ {
688
+ geoId: '618733',
689
+ geoTitle: 'Ski Dubai',
690
+ geoName: 'Dubai, United Arab Emirates',
691
+ displayName: 'Ski Dubai, Dubai, United Arab Emirates',
692
+ type: 'point_of_interest',
693
+ countryCode: 'AE',
694
+ lattitude: 25.117395,
695
+ longitude: 55.198021
696
+ },
697
+ {
698
+ geoId: '2442684',
699
+ geoTitle: 'Dubai',
700
+ geoName: 'Dubai, United Arab Emirates',
701
+ displayName: 'Dubai, Dubai, United Arab Emirates',
702
+ type: 'hotel',
703
+ countryCode: '',
704
+ lattitude: 25.273935,
705
+ longitude: 55.32799
706
+ },
707
+ {
708
+ geoId: '1487637',
709
+ geoTitle: 'Dubai Luxury Stay - Downtown Dubai',
710
+ geoName: 'Dubai, United Arab Emirates',
711
+ displayName: 'Dubai Luxury Stay - Downtown Dubai, Dubai, United Arab Emirates',
712
+ type: 'hotel',
713
+ countryCode: '',
714
+ lattitude: 25.076485,
715
+ longitude: 55.144405
716
+ },
717
+ {
718
+ geoId: '1910103',
719
+ geoTitle: 'Dubai Arch',
720
+ geoName: 'Dubai, United Arab Emirates',
721
+ displayName: 'Dubai Arch, Dubai, United Arab Emirates',
722
+ type: 'hotel',
723
+ countryCode: '',
724
+ lattitude: 25.071701,
725
+ longitude: 55.143578
726
+ },
727
+ {
728
+ geoId: '737327',
729
+ geoTitle: 'Angsana Dubai',
730
+ geoName: 'Dubai, United Arab Emirates',
731
+ displayName: 'Angsana Dubai, Dubai, United Arab Emirates',
732
+ type: 'hotel',
733
+ countryCode: '',
734
+ lattitude: 25.215,
735
+ longitude: 55.279
736
+ },
737
+ {
738
+ geoId: '2231257',
739
+ geoTitle: 'Dubai Backpackers',
740
+ geoName: 'Dubai, United Arab Emirates',
741
+ displayName: 'Dubai Backpackers, Dubai, United Arab Emirates',
742
+ type: 'hotel',
743
+ countryCode: '',
744
+ lattitude: 25.09714,
745
+ longitude: 55.17065
746
+ },
747
+ {
748
+ geoId: '2322561',
749
+ geoTitle: 'Dubai Hostel',
750
+ geoName: 'Dubai, United Arab Emirates',
751
+ displayName: 'Dubai Hostel, Dubai, United Arab Emirates',
752
+ type: 'hotel',
753
+ countryCode: '',
754
+ lattitude: 25.09696,
755
+ longitude: 55.1776
756
+ },
757
+ {
758
+ geoId: '2323808',
759
+ geoTitle: 'Hostel Dubai',
760
+ geoName: 'Dubai, United Arab Emirates',
761
+ displayName: 'Hostel Dubai, Dubai, United Arab Emirates',
762
+ type: 'hotel',
763
+ countryCode: '',
764
+ lattitude: 25.19179,
765
+ longitude: 55.26195
766
+ },
767
+ {
768
+ geoId: '2442649',
769
+ geoTitle: 'Dubai 360',
770
+ geoName: 'Dubai, United Arab Emirates',
771
+ displayName: 'Dubai 360, Dubai, United Arab Emirates',
772
+ type: 'hotel',
773
+ countryCode: '',
774
+ lattitude: 25.088923,
775
+ longitude: 55.146818
776
+ }
777
+ ]
778
+
779
+ const RSA_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
780
+ MIICITANBgkqhkiG9w0BAQEFAAOCAg4AMIICCQKCAgB0rRSHGZK/FtK15IRfQxtl
781
+ dN5ToPtfXGzTNBe1vGhow5xqp6BLubMKKPwCj3zOJiAm1Ip1VNqqYxWnaL8ZErBF
782
+ rErtE0r7kn50QgGgJCpdrWJ4unhaoe1su207d9oY6D6OBH90v1aXA6ZjN7zTJmiq
783
+ FJZOOTQE6t/XwNAOphq/B6Lcr1iQSQmSqaMozy4OQ4NvIzNTQn5tcs9qJBMsKFeU
784
+ cSp8VK76JBNEqSuEHNmbV2d/IQ5XAqVUiAs34kH2xWRSbBmy0WK3c3QKGzC1MRWM
785
+ 9JauLN56TKUFOmUSjfWY3caJru6HxsiMTGKnvflXHfgA2aLAPxu72pIHoCTy5Hxj
786
+ AUoKM7qXUn/kQj4Ci0M/0LRF4boqd37wpbyVNeXNvqm+dLjxklf/+NHMfNL2Wvb4
787
+ ekHUVI8nMVPgswIcQ7q/Njf9DC+LA+/duf8m9JDjI3J6r9y1gQugdkLETGzF2Gj1
788
+ ToU0Pocrg6CFHImqq2z+cOeWnURrV0f0I1UWWGCb79OZxZ5oDjM9uCLRsoe8+Npg
789
+ ACZNf4AbnM7OUNj5TEYs6t/tUYouvTB7gH452I3EH6KWBl+ZTgCg0RFkOMazNOjn
790
+ 4J0yfbaPvpVgKzF2WRyn63mTO2tdLRl5s9uqMFhXmCs6IzEqBpxMKL1aS6ONZLDG
791
+ 5qPtpCPJnUFQEBTVlSO15QIDAQAB
792
+ -----END PUBLIC KEY-----`
793
+
794
+ const HOTEL_SEARCH_BASE_URL = 'https://hotelapihub.dev.futuretravelplatform.com'
795
+ const HOTEL_SEARCH_PATH = '/api/v1/hotels/search-form'
796
+ const HOTEL_SEARCH_URL = `${HOTEL_SEARCH_BASE_URL}${HOTEL_SEARCH_PATH}`
797
+ const COUNTRY_SEARCH_BASE_URL = 'https://adminapi.dev.futuretravelplatform.com'
798
+ const COUNTRY_SEARCH_PATH = '/api/MasterSearch/GetAllCountry/EN'
799
+
800
+ const STATIC_COUNTRIES = [
801
+ {
802
+ country_code: 'KW',
803
+ country_name: 'Kuwait',
804
+ country_id: '2108231234183749619'
805
+ },
806
+ {
807
+ country_code: 'BH',
808
+ country_name: 'Bahrain',
809
+ country_id: '2108231234168861036'
810
+ },
811
+ {
812
+ country_code: 'QA',
813
+ country_name: 'Qatar',
814
+ country_id: '2108231234193188713'
815
+ },
816
+ {
817
+ country_code: 'OM',
818
+ country_name: 'Oman',
819
+ country_id: '2108231234191323057'
820
+ }
821
+ ]
822
+
823
+ const mealTypeOptions = [
824
+ { label: 'Breakfast Only', value: 2 },
825
+ { label: 'Breakfast or Lunch or Dinner', value: 3 },
826
+ { label: 'Breakfast or Lunch and Dinner', value: 4 },
827
+ { label: 'All Inclusive', value: 5 },
828
+ { label: 'Room Only', value: 1 }
829
+ ]
830
+
831
+ const childAgeOptions = [
832
+ { label: 'Under 1', value: 0 },
833
+ ...Array.from({ length: 17 }, (_, i) => ({
834
+ label: `${i + 1}`,
835
+ value: i + 1
836
+ }))
837
+ ]
838
+
839
+ const theme = computed(() => props.config?.theme || {})
840
+ const primaryColor = computed(() => theme.value.primary ?? '#2c0a82')
841
+ const secondaryColor = computed(() => theme.value.secondary ?? '#2c0a82')
842
+ const primaryLightColor = computed(() => theme.value.primaryLight ?? '#f3e2ff')
843
+ const fontName = computed(() => props.config?.fontName)
844
+ const redirectionDomain = computed(() => props.config?.redirectionDomain)
845
+
846
+ const getInitialDates = () => {
847
+ const today = new Date()
848
+ today.setHours(0, 0, 0, 0)
849
+ const tomorrow = new Date(today)
850
+ tomorrow.setDate(today.getDate() + 1)
851
+ return { today, tomorrow }
852
+ }
853
+
854
+ const { today, tomorrow } = getInitialDates()
855
+
856
+ const formData = ref({
857
+ destination: 'Destination',
858
+ nationality: 'Select Nationality',
859
+ checkIn: today,
860
+ checkOut: tomorrow,
861
+ roomsGuests: '1 Room, 2 Adult'
862
+ })
863
+
864
+ const destinationQuery = ref('')
865
+ const nationalityQuery = ref('')
866
+ const isDestinationDropdownOpen = ref(false)
867
+ const isNationalityDropdownOpen = ref(false)
868
+ const isRoomsGuestsMenuOpen = ref(false)
869
+
870
+ const destinationDropdownRef = ref(null)
871
+ const nationalityDropdownRef = ref(null)
872
+ const roomsGuestsMenuRef = ref(null)
873
+ const calendarRef = ref(null)
874
+
875
+ const rooms = ref([{ adults: 2, children: 0, childAges: [] }])
876
+
877
+ const destinationResults = ref([])
878
+ const isDestinationLoading = ref(false)
879
+ const countryResults = ref([])
880
+ const isCountryLoading = ref(false)
881
+ const selectedDestination = ref(null)
882
+ const selectedNationality = ref(null)
883
+ const isAroundCurrentLocation = ref(false)
884
+ const currentLocationCoords = ref(null)
885
+
886
+ const fieldErrors = ref({
887
+ destination: false,
888
+ nationality: false,
889
+ checkIn: false,
890
+ checkOut: false
891
+ })
892
+
893
+ const selectedStarRatings = ref([])
894
+ const selectedMealTypes = ref([])
895
+ const dateRange = ref([today, tomorrow])
896
+
897
+ const filteredNationalityOptions = computed(() => {
898
+ return nationalityQuery.value.trim().length === 0
899
+ ? STATIC_COUNTRIES
900
+ : countryResults.value
901
+ })
902
+
903
+ const themeStyle = computed(() => {
904
+ const style = {
905
+ '--primary': primaryColor.value,
906
+ '--secondary': secondaryColor.value,
907
+ '--primary-light': primaryLightColor.value
908
+ }
909
+
910
+ if (fontName.value) {
911
+ style['--font-family'] = fontName.value
912
+ }
913
+
914
+ if (props.minWidth !== undefined) {
915
+ style.minWidth =
916
+ typeof props.minWidth === 'number' ? `${props.minWidth}px` : props.minWidth
917
+ }
918
+
919
+ return style
920
+ })
921
+
922
+ const ensureStylesInjected = () => {
923
+ if (typeof document === 'undefined') {
924
+ return
925
+ }
926
+
927
+ if (document.getElementById(STYLE_ELEMENT_ID)) {
928
+ return
929
+ }
930
+
931
+ const style = document.createElement('style')
932
+ style.id = STYLE_ELEMENT_ID
933
+ style.textContent = hotelSearchWidgetStyles
934
+ document.head.appendChild(style)
935
+ }
936
+
937
+ const ensurePrimeIconsLoaded = () => {
938
+ if (typeof document === 'undefined') {
939
+ return
940
+ }
941
+
942
+ if (document.getElementById(PRIMEICONS_LINK_ID)) {
943
+ return
944
+ }
945
+
946
+ const link = document.createElement('link')
947
+ link.id = PRIMEICONS_LINK_ID
948
+ link.rel = 'stylesheet'
949
+ link.href = 'https://unpkg.com/primeicons@7.0.0/primeicons.css'
950
+ document.head.appendChild(link)
951
+ }
952
+
953
+ const buildRequestToken = () => {
954
+ if (!redirectionDomain.value) {
955
+ return ''
956
+ }
957
+
958
+ const domainKey = `${redirectionDomain.value}~${Date.now()}`
959
+ const encryptor = new JSEncrypt()
960
+ encryptor.setPublicKey(RSA_PUBLIC_KEY)
961
+ return encryptor.encrypt(domainKey) || ''
962
+ }
963
+
964
+ const handleInputChange = (field, value) => {
965
+ formData.value[field] = value
966
+ }
967
+
968
+ const handleDestinationQueryChange = (event) => {
969
+ const value = event.target.value
970
+ destinationQuery.value = value
971
+ selectedDestination.value = null
972
+ isAroundCurrentLocation.value = false
973
+ currentLocationCoords.value = null
974
+ if (value.trim().length > 0) {
975
+ fieldErrors.value.destination = false
976
+ }
977
+ }
978
+
979
+ const handleNationalityQueryChange = (event) => {
980
+ const value = event.target.value
981
+ nationalityQuery.value = value
982
+ selectedNationality.value = null
983
+ if (value.trim().length > 0) {
984
+ fieldErrors.value.nationality = false
985
+ }
986
+ }
987
+
988
+ const handleDestinationSelect = (option) => {
989
+ handleInputChange('destination', option.displayName)
990
+ destinationQuery.value = option.displayName
991
+ selectedDestination.value = option
992
+ isAroundCurrentLocation.value = false
993
+ currentLocationCoords.value = null
994
+ isDestinationDropdownOpen.value = false
995
+ fieldErrors.value.destination = false
996
+ }
997
+
998
+ const handleNationalitySelect = (option) => {
999
+ handleInputChange('nationality', option.country_name)
1000
+ nationalityQuery.value = option.country_name
1001
+ selectedNationality.value = option
1002
+ isNationalityDropdownOpen.value = false
1003
+ fieldErrors.value.nationality = false
1004
+ }
1005
+
1006
+ const handleAroundCurrentLocation = () => {
1007
+ const label = 'Around Current Location'
1008
+ handleInputChange('destination', label)
1009
+ destinationQuery.value = label
1010
+ selectedDestination.value = null
1011
+ isAroundCurrentLocation.value = true
1012
+ isDestinationDropdownOpen.value = false
1013
+ fieldErrors.value.destination = false
1014
+
1015
+ if (!navigator.geolocation) {
1016
+ currentLocationCoords.value = null
1017
+ return
1018
+ }
1019
+
1020
+ navigator.geolocation.getCurrentPosition(
1021
+ (position) => {
1022
+ currentLocationCoords.value = {
1023
+ lat: position.coords.latitude,
1024
+ lng: position.coords.longitude
1025
+ }
1026
+ },
1027
+ () => {
1028
+ currentLocationCoords.value = null
1029
+ }
1030
+ )
1031
+ }
1032
+
1033
+ const handleStarRatingToggle = (rating) => {
1034
+ if (selectedStarRatings.value.includes(rating)) {
1035
+ selectedStarRatings.value = selectedStarRatings.value.filter(
1036
+ (value) => value !== rating
1037
+ )
1038
+ } else {
1039
+ selectedStarRatings.value = [...selectedStarRatings.value, rating].sort(
1040
+ (a, b) => a - b
1041
+ )
1042
+ }
1043
+ }
1044
+
1045
+ const handleMealTypeToggle = (mealType) => {
1046
+ if (selectedMealTypes.value.includes(mealType)) {
1047
+ selectedMealTypes.value = selectedMealTypes.value.filter(
1048
+ (value) => value !== mealType
1049
+ )
1050
+ } else {
1051
+ selectedMealTypes.value = [...selectedMealTypes.value, mealType]
1052
+ }
1053
+ }
1054
+
1055
+ const handleRemoveRoom = (roomIndex) => {
1056
+ if (rooms.value.length <= 1) {
1057
+ return
1058
+ }
1059
+ rooms.value = rooms.value.filter((_, index) => index !== roomIndex)
1060
+ }
1061
+
1062
+ const handleAddRoom = () => {
1063
+ if (rooms.value.length >= 6) {
1064
+ return
1065
+ }
1066
+ rooms.value = [...rooms.value, { adults: 1, children: 0, childAges: [] }]
1067
+ }
1068
+
1069
+ const resetRooms = () => {
1070
+ rooms.value = [{ adults: 2, children: 0, childAges: [] }]
1071
+ }
1072
+
1073
+ const incrementAdults = (index) => {
1074
+ if (rooms.value[index].adults >= MAX_ADULTS_PER_ROOM) {
1075
+ return
1076
+ }
1077
+ const newRooms = [...rooms.value]
1078
+ newRooms[index].adults += 1
1079
+ rooms.value = newRooms
1080
+ }
1081
+
1082
+ const decrementAdults = (index) => {
1083
+ const newRooms = [...rooms.value]
1084
+ newRooms[index].adults = Math.max(1, newRooms[index].adults - 1)
1085
+ rooms.value = newRooms
1086
+ }
1087
+
1088
+ const incrementChildren = (index) => {
1089
+ if (rooms.value[index].children >= 4) {
1090
+ return
1091
+ }
1092
+ const newRooms = [...rooms.value]
1093
+ newRooms[index].children += 1
1094
+ newRooms[index].childAges = [...newRooms[index].childAges, 0]
1095
+ rooms.value = newRooms
1096
+ }
1097
+
1098
+ const decrementChildren = (index) => {
1099
+ const newRooms = [...rooms.value]
1100
+ newRooms[index].children = Math.max(0, newRooms[index].children - 1)
1101
+ newRooms[index].childAges = newRooms[index].childAges.slice(0, -1)
1102
+ rooms.value = newRooms
1103
+ }
1104
+
1105
+ const updateChildAge = (roomIndex, childIndex, value) => {
1106
+ const newRooms = [...rooms.value]
1107
+ newRooms[roomIndex].childAges[childIndex] = value
1108
+ rooms.value = newRooms
1109
+ }
1110
+
1111
+ const formatDate = (date) => {
1112
+ if (!date) {
1113
+ return ''
1114
+ }
1115
+ return date.toLocaleDateString('en-GB', {
1116
+ weekday: 'short',
1117
+ day: '2-digit',
1118
+ month: 'short'
1119
+ })
1120
+ }
1121
+
1122
+ const formatQueryDate = (date) => {
1123
+ if (!date) {
1124
+ return ''
1125
+ }
1126
+ return date.toLocaleDateString('en-GB', {
1127
+ day: '2-digit',
1128
+ month: 'short',
1129
+ year: 'numeric'
1130
+ })
1131
+ }
1132
+
1133
+ const handleDateRangeChange = (value) => {
1134
+ dateRange.value = value
1135
+ const start = value?.[0] ?? null
1136
+ const end = value?.[1] ?? null
1137
+ handleInputChange('checkIn', start)
1138
+ handleInputChange('checkOut', end)
1139
+
1140
+ if (start && end) {
1141
+ if (calendarRef.value) {
1142
+ calendarRef.value.hide()
1143
+ }
1144
+ fieldErrors.value.checkIn = false
1145
+ fieldErrors.value.checkOut = false
1146
+ }
1147
+ }
1148
+
1149
+ const showCalendar = () => {
1150
+ if (calendarRef.value) {
1151
+ calendarRef.value.show()
1152
+ }
1153
+ }
1154
+
1155
+ const validateRequiredFields = () => {
1156
+ const destinationValid = destinationQuery.value.trim().length > 0
1157
+ const nationalityValid = nationalityQuery.value.trim().length > 0
1158
+ const checkInValid = !!formData.value.checkIn
1159
+ const checkOutValid = !!formData.value.checkOut
1160
+
1161
+ fieldErrors.value = {
1162
+ destination: !destinationValid,
1163
+ nationality: !nationalityValid,
1164
+ checkIn: !checkInValid,
1165
+ checkOut: !checkOutValid
1166
+ }
1167
+
1168
+ return destinationValid && nationalityValid && checkInValid && checkOutValid
1169
+ }
1170
+
1171
+ const handleSearch = () => {
1172
+ const destination = selectedDestination.value
1173
+ const nationality = selectedNationality.value
1174
+ const roomParams = rooms.value.map((room, index) => {
1175
+ const parts = [room.adults, room.children, ...room.childAges]
1176
+ return `room${index + 1}=${parts.join('-')}`
1177
+ })
1178
+ const latitudeValue = isAroundCurrentLocation.value
1179
+ ? (currentLocationCoords.value?.lat?.toString() ?? '0')
1180
+ : (destination?.lattitude?.toString() ?? '0')
1181
+ const longitudeValue = isAroundCurrentLocation.value
1182
+ ? (currentLocationCoords.value?.lng?.toString() ?? '0')
1183
+ : (destination?.longitude?.toString() ?? '0')
1184
+ const queryParams = {
1185
+ curr: 'INR',
1186
+ geoId: isAroundCurrentLocation.value ? '' : (destination?.geoId ?? ''),
1187
+ Country: isAroundCurrentLocation.value ? '' : (destination?.countryCode ?? ''),
1188
+ nationality: nationality?.country_code ?? '',
1189
+ checkinDate: formData.value.checkIn ? formatQueryDate(formData.value.checkIn) : '',
1190
+ checkoutDate: formData.value.checkOut ? formatQueryDate(formData.value.checkOut) : '',
1191
+ lc: 'EN',
1192
+ StarRatings:
1193
+ selectedStarRatings.value.length > 0
1194
+ ? selectedStarRatings.value.join(',')
1195
+ : '',
1196
+ MealTypes:
1197
+ selectedMealTypes.value.length > 0 ? selectedMealTypes.value.join(',') : '',
1198
+ FreeCancellation: 'false',
1199
+ geoName: isAroundCurrentLocation.value
1200
+ ? 'Around Current Location'
1201
+ : (destination?.displayName ?? ''),
1202
+ Lat: latitudeValue,
1203
+ Long: longitudeValue,
1204
+ is_around_current_location: isAroundCurrentLocation.value ? 'true' : 'false',
1205
+ currtime: Date.now().toString(),
1206
+ mgcc: '',
1207
+ type: isAroundCurrentLocation.value ? '' : (destination?.type ?? '')
1208
+ }
1209
+ const encodedPairs = Object.entries(queryParams).map(
1210
+ ([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
1211
+ )
1212
+ const queryString = `/hotel/result?${[...encodedPairs, ...roomParams].join('&')}`
1213
+ console.log('Query string:', queryString)
1214
+ if (redirectionDomain.value) {
1215
+ const domainOnly = redirectionDomain.value
1216
+ .replace(/^https?:\/\//i, '')
1217
+ .split('/')[0]
1218
+ const normalizedDomain = `https://${domainOnly}`
1219
+ const redirectUrl = `${normalizedDomain}${queryString}`
1220
+ window.open(redirectUrl, '_blank', 'noopener,noreferrer')
1221
+ }
1222
+ }
1223
+
1224
+ const handleSubmit = (e) => {
1225
+ e.preventDefault()
1226
+ if (!validateRequiredFields()) {
1227
+ return
1228
+ }
1229
+ handleSearch()
1230
+ }
1231
+
1232
+ // Watch for destination query changes
1233
+ let destinationSearchTimeout = null
1234
+ watch(destinationQuery, (query) => {
1235
+ const trimmedQuery = query.trim()
1236
+ if (trimmedQuery.length < MIN_SEARCH_CHARS) {
1237
+ destinationResults.value = []
1238
+ return
1239
+ }
1240
+
1241
+ if (destinationSearchTimeout) {
1242
+ clearTimeout(destinationSearchTimeout)
1243
+ }
1244
+
1245
+ destinationSearchTimeout = setTimeout(async () => {
1246
+ const controller = new AbortController()
1247
+
1248
+ try {
1249
+ isDestinationLoading.value = true
1250
+ const headers = {
1251
+ 'Content-Type': 'application/json'
1252
+ }
1253
+ const token = buildRequestToken()
1254
+ if (token) {
1255
+ headers.token = token
1256
+ }
1257
+
1258
+ const response = await fetch(HOTEL_SEARCH_URL, {
1259
+ method: 'POST',
1260
+ headers,
1261
+ body: JSON.stringify({
1262
+ searchTerm: trimmedQuery,
1263
+ languageCode: 'EN',
1264
+ lattitude: '',
1265
+ longitude: ''
1266
+ }),
1267
+ signal: controller.signal
1268
+ })
1269
+
1270
+ if (!response.ok) {
1271
+ throw new Error('Destination search failed')
1272
+ }
1273
+
1274
+ const data = await response.json()
1275
+ destinationResults.value = data.data?.locations ?? data.locations ?? []
1276
+ } catch (error) {
1277
+ if (!(error instanceof DOMException && error.name === 'AbortError')) {
1278
+ destinationResults.value = []
1279
+ }
1280
+ } finally {
1281
+ isDestinationLoading.value = false
1282
+ }
1283
+ }, 300)
1284
+ })
1285
+
1286
+ // Watch for nationality query changes
1287
+ let nationalitySearchTimeout = null
1288
+ watch(nationalityQuery, (query) => {
1289
+ const trimmedQuery = query.trim()
1290
+ if (trimmedQuery.length < MIN_SEARCH_CHARS) {
1291
+ countryResults.value = []
1292
+ return
1293
+ }
1294
+
1295
+ if (nationalitySearchTimeout) {
1296
+ clearTimeout(nationalitySearchTimeout)
1297
+ }
1298
+
1299
+ nationalitySearchTimeout = setTimeout(async () => {
1300
+ const controller = new AbortController()
1301
+
1302
+ try {
1303
+ isCountryLoading.value = true
1304
+ const headers = {
1305
+ 'Content-Type': 'application/json'
1306
+ }
1307
+ const token = buildRequestToken()
1308
+ if (token) {
1309
+ headers.token = token
1310
+ }
1311
+
1312
+ const response = await fetch(
1313
+ `${COUNTRY_SEARCH_BASE_URL}${COUNTRY_SEARCH_PATH}/${encodeURIComponent(trimmedQuery)}`,
1314
+ {
1315
+ method: 'GET',
1316
+ headers,
1317
+ signal: controller.signal
1318
+ }
1319
+ )
1320
+
1321
+ if (!response.ok) {
1322
+ throw new Error('Nationality search failed')
1323
+ }
1324
+
1325
+ const data = await response.json()
1326
+ countryResults.value = data ?? []
1327
+ } catch (error) {
1328
+ if (!(error instanceof DOMException && error.name === 'AbortError')) {
1329
+ countryResults.value = []
1330
+ }
1331
+ } finally {
1332
+ isCountryLoading.value = false
1333
+ }
1334
+ }, 300)
1335
+ })
1336
+
1337
+ // Watch for rooms changes to update display text
1338
+ watch(
1339
+ rooms,
1340
+ () => {
1341
+ const totalAdults = rooms.value.reduce((sum, room) => sum + room.adults, 0)
1342
+ const totalChildren = rooms.value.reduce((sum, room) => sum + room.children, 0)
1343
+ const roomsText = rooms.value.length === 1 ? 'Room' : 'Rooms'
1344
+ const adultsText = totalAdults === 1 ? 'Adult' : 'Adults'
1345
+ const childrenText =
1346
+ totalChildren === 0
1347
+ ? ''
1348
+ : totalChildren === 1
1349
+ ? `, ${totalChildren} Child`
1350
+ : `, ${totalChildren} Children`
1351
+ const displayText = `${rooms.value.length} ${roomsText}, ${totalAdults} ${adultsText}${childrenText}`
1352
+ handleInputChange('roomsGuests', displayText)
1353
+ },
1354
+ { deep: true }
1355
+ )
1356
+
1357
+ // Handle click outside
1358
+ const handleClickOutside = (event) => {
1359
+ const target = event.target
1360
+
1361
+ const isInsidePrimeDropdown =
1362
+ target.closest('.p-dropdown-panel') !== null ||
1363
+ target.closest('.p-dropdown-items-wrapper') !== null ||
1364
+ target.closest('.p-dropdown-item') !== null ||
1365
+ target.closest('.p-component')?.classList.contains('p-dropdown') !== false ||
1366
+ target.closest('[data-pc-section="panel"]') !== null ||
1367
+ target.closest('[data-pc-name="dropdown"]') !== null
1368
+
1369
+ if (
1370
+ destinationDropdownRef.value &&
1371
+ !destinationDropdownRef.value.contains(target)
1372
+ ) {
1373
+ isDestinationDropdownOpen.value = false
1374
+ }
1375
+ if (
1376
+ nationalityDropdownRef.value &&
1377
+ !nationalityDropdownRef.value.contains(target)
1378
+ ) {
1379
+ isNationalityDropdownOpen.value = false
1380
+ }
1381
+
1382
+ if (
1383
+ !isInsidePrimeDropdown &&
1384
+ roomsGuestsMenuRef.value &&
1385
+ !roomsGuestsMenuRef.value.contains(target)
1386
+ ) {
1387
+ isRoomsGuestsMenuOpen.value = false
1388
+ }
1389
+ }
1390
+
1391
+ onMounted(() => {
1392
+ ensureStylesInjected()
1393
+ ensurePrimeIconsLoaded()
1394
+ document.addEventListener('mousedown', handleClickOutside)
1395
+ })
1396
+
1397
+ onUnmounted(() => {
1398
+ document.removeEventListener('mousedown', handleClickOutside)
1399
+ if (destinationSearchTimeout) {
1400
+ clearTimeout(destinationSearchTimeout)
1401
+ }
1402
+ if (nationalitySearchTimeout) {
1403
+ clearTimeout(nationalitySearchTimeout)
1404
+ }
1405
+ })
1406
+ </script>
1407
+
1408
+ <style>
1409
+ /* Styles are injected via JavaScript */
1410
+ </style>