1<!DOCTYPE html> 2<html lang="en"> 3 4<head> 5 <meta charset="UTF-8" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 <meta name="apple-itunes-app" content="app-id=6736680726, app-argument=goodresto://goodresto.app/resto/405036819902/reserve" /> 8 <meta name="google-play-app" content="app-id=app.goodresto.android" /> 9 10 <title>Casa Italia - Table Reservation - GoodResto</title> 11 <meta name="title" content="Casa Italia - Table Reservation - GoodResto"> 12 <meta name="description" content="Reserve your table at Casa Italia. Choose a date, find available time slots, and provide additional info for the restaurant."> 13 <meta name="keywords" content="Casa Italia, table reservation, book a table, schedule, restaurant, dining, GoodResto"> 14 15 <meta property="og:type" content="website"> 16 <meta property="og:title" content="Casa Italia - Table Reservation - GoodResto"> 17 <meta property="og:url" content="https://casa-italia.pl/reserve"> 18 <meta property="og:description" content="Reserve your table at Casa Italia. Choose a date, find available time slots, and provide additional info for the restaurant."> 19 <meta property="og:image" content="https://cdn.goodresto.app/id/382549298295_md"> 20 <meta property="og:site_name" content="GoodResto"> 21 22 <meta name="google" content="notranslate"> 23 <meta name="google" content="max-image-preview:standard"> 24 25 <link rel="canonical" href="https://casa-italia.pl/reserve"> 26 <link rel="icon" href="/favicon.ico"> 27 <link rel="apple-touch-icon" href="/GoodRestoLogo-apple.png"> 28 29
29<script src="https://widget.goodresto.app/minialert.js"></script>
29 30
30<script src="/tailwind-v3.4.16.js"></script>
30 31
31<script src="https://goodresto.app/index.js"></script>
31 32 <link rel="preconnect" href="https://fonts.googleapis.com"> 33 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 34 35 <link href="https://fonts.googleapis.com/css2?family=Courgette:wght@400;700&display=swap" rel="stylesheet"> 36 37 <link href="https://fonts.googleapis.com/css2?family=Courgette:wght@400;700&display=swap" rel="stylesheet"> 38
38<script> 39 tailwind.config = { 40 darkMode: 'class', 41 theme: { 42 extend: {}, 43 } 44 } 45 </script>
45 46 <style> 47 * { 48 transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease; 49 } 50 51 *, 52 *:before, 53 *:after { 54 box-sizing: border-box; 55 } 56 57 [hidden] { 58 display: none !important; 59 } 60 61 62 @media (min-width: 640px) { 63 body { 64 background-image: url('https://cdn.goodresto.app/id/382871171812_lg'); 65 background-size: cover; 66 background-position: center; 67 background-attachment: fixed; 68 background-repeat: no-repeat; 69 } 70 } 71 72 73 .gdrst-bkgd { 74 background-color: #f2d24b; 75 } 76 77 .gdrst-bkgd-button { 78 background-color: #4e8275; 79 } 80 81 .gdrst-button-text { 82 color: #fffbfb; 83 } 84 85 .gdrst-nav { 86 background-color: #f2d24b; 87 } 88 89 .gdrst-nav-alt { 90 background-color: #F4DB6F; 91 } 92 93 .gdrst-nav-fill { 94 fill: #f2d24b; 95 } 96 97 .gdrst-nav-text { 98 color: #d43a33; 99 font-family: Courgette, serif; 100 } 101 102 .gdrst-nav-text-alt { 103 color: color-contrast(var(--base-color) vs white, black); 104 } 105 106 .gdrst-nav-text svg { 107 stroke: currentColor; 108 fill: currentColor; 109 } 110 111 .gdrst-body { 112 background-color: #f3f4f6; 113 opacity: 89%; 114 } 115 116 .gdrst-body-alt { 117 background-color: #F5F6F7; 118 } 119 120 .gdrst-body-text { 121 color: #374151; 122 font-family: Courgette, serif; 123 } 124 125 .gdrst-body-divider { 126 color: #374151; 127 } 128 129 .gdrst-social-logo svg { 130 color: #f2d24b; 131 stroke: currentColor; 132 fill: currentColor; 133 }</style> 134 <style> 135 @media (max-width: 639px) { 136 .step-indicator.is-active .step-label { 137 display: inline; 138 } 139 } 140 141 .step-dot { 142 width: 14px; 143 height: 14px; 144 border-radius: 999px; 145 background: #dc2626; 146 } 147 148 .step-line { 149 flex: 1; 150 height: 2px; 151 background: #fecaca; 152 } 153 154 .time-slot.selected { 155 @apply gdrst-nav gdrst-nav-text; 156 } 157 158 .people-option.selected { 159 @apply gdrst-nav gdrst-nav-text; 160 } 161 </style> 162</head> 163 164<body class="gdrst-bkgd overflow-x-hidden overflow-y-scroll font-sans flex flex-col min-h-screen"> 165 <nav class="shadow-lg sticky top-0 z-40"> 166 <div id="header-container"></div> 167 </nav> 168 169 <main class="flex-grow md:p-4"> 170 <div class="gdrst-body w-full max-w-4xl rounded-lg mx-auto p-4 mt-2"> 171 172 173 <div class="flex items-center mb-2 px-1"> 174 <div data-step="0" class="step-indicator flex items-center gap-2 text-sm font-medium"> 175 <span class="step-icon flex h-8 w-8 items-center justify-center rounded-full border border-gray-300 gdrst-body-text"> 176 <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 177 <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" /> 178 <circle cx="9" cy="7" r="4" /> 179 <path d="M22 21v-2a4 4 0 0 0-3-3.87" /> 180 <path d="M16 3.13a4 4 0 0 1 0 7.75" /> 181 </svg> 182 </span> 183 <span class="hidden sm:inline step-label gdrst-body-text">Guests</span> 184 </div> 185 186 <div class="mx-3 h-px flex-1 gdrst-body-alt"></div> 187 188 <div data-step="1" class="step-indicator flex items-center gap-2 text-sm font-medium text-gray-400"> 189 <span class="step-icon flex h-8 w-8 items-center justify-center rounded-full border border-gray-300 gdrst-body-text"> 190 <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 191 <rect x="3" y="4" width="18" height="18" rx="2" /> 192 <line x1="16" y1="2" x2="16" y2="6" /> 193 <line x1="8" y1="2" x2="8" y2="6" /> 194 <line x1="3" y1="10" x2="21" y2="10" /> 195 <circle cx="16" cy="16" r="3" /> 196 <line x1="16" y1="14.5" x2="16" y2="16" /> 197 <line x1="16" y1="16" x2="17" y2="17" /> 198 </svg> 199 </span> 200 <span class="hidden sm:inline step-label gdrst-body-text">Date & time</span> 201 </div> 202 203 <div class="mx-3 h-px flex-1 gdrst-body-alt"></div> 204 205 <div data-step="2" class="step-indicator flex items-center gap-2 text-sm font-medium text-gray-400"> 206 <span class="step-icon flex h-8 w-8 items-center justify-center rounded-full border border-gray-300 gdrst-body-text"> 207 <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 208 <circle cx="12" cy="7" r="4" /> 209 <path d="M5.5 21a6.5 6.5 0 0 1 13 0" /> 210 </svg> 211 </span> 212 <span class="hidden sm:inline step-label gdrst-body-text">Contact</span> 213 </div> 214 215 <div class="mx-3 h-px flex-1 gdrst-body-alt"></div> 216 217 <div data-step="3" class="step-indicator flex items-center gap-2 text-sm font-medium text-gray-400"> 218 <span class="step-icon flex h-8 w-8 items-center justify-center rounded-full border border-gray-300 gdrst-body-text"> 219 <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> 220 <circle cx="12" cy="12" r="10" /> 221 <path d="m8 12 2.5 2.5L16 9" /> 222 </svg> 223 </span> 224 <span class="hidden sm:inline step-label gdrst-body-text">Confirm</span> 225 </div> 226 </div> 227 228 <div class="gdrst-body-alt rounded-lg shadow-lg p-4"> 229 230 231 232 <div class="step" data-step="0"> 233 <div class="w-full max-w-xl mx-auto"> 234 235 <div id="noticeContainer" class="hidden restaurant-note bg-[#fff8e1] rounded-lg p-4 flex gap-3 items-start mb-2"> 236 <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-yellow-600 flex-shrink-0" viewB
236ox="0 0 24 24" fill="currentColor"> 237 <path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"></path> 238 </svg> 239 240 <div class="note-content"> 241 <strong class="block mb-1">Message from restaurant</strong> 242 243 <p id="noticeIndividual" class="text-md leading-relaxed whitespace-pre-line"></p> 244 <p id="noticeGroup" class="hidden text-md leading-relaxed whitespace-pre-line"></p> 245 246 <button id="noticeMore" type="button" class="hidden mx-auto mt-2 text-md font-semibold underline">Expand</button> 247 </div> 248 </div> 249 250 <h2 class="gdrst-body-text text-xl font-bold mb-2 text-center">Choose the number of guests</h2> 251 252 <div id="peopleGrid" class="grid grid-cols-3 sm:grid-cols-6 gap-3 mb-2"> 253 254 <button type="button" data-people="1" class="people-option gdrst-body-text font-medium rounded-lg border px-4 py-3"> 255 1 256 </button> 257 258 <button type="button" data-people="2" class="people-option gdrst-body-text font-medium rounded-lg border px-4 py-3"> 259 2 260 </button> 261 262 <button type="button" data-people="3" class="people-option gdrst-body-text font-medium rounded-lg border px-4 py-3"> 263 3 264 </button> 265 266 <button type="button" data-people="4" class="people-option gdrst-body-text font-medium rounded-lg border px-4 py-3"> 267 4 268 </button> 269 270 <button type="button" data-people="5" class="people-option gdrst-body-text font-medium rounded-lg border px-4 py-3"> 271 5 272 </button> 273 274 <button type="button" data-people="6" class="people-option gdrst-body-text font-medium rounded-lg border px-4 py-3"> 275 6 276 </button> 277 278 <button type="button" data-people="7" class="people-option gdrst-body-text font-medium rounded-lg border px-4 py-3"> 279 7 280 </button> 281 282 <button type="button" data-people="8" class="people-option gdrst-body-text font-medium rounded-lg border px-4 py-3"> 283 8 284 </button> 285 286 <button type="button" data-people="9" class="people-option gdrst-body-text font-medium rounded-lg border px-4 py-3"> 287 9 288 </button> 289 290 <button type="button" data-people="10" class="people-option gdrst-body-text font-medium rounded-lg border px-4 py-3"> 291 10 292 </button> 293 294 <button type="button" data-people="11" class="people-option gdrst-body-text font-medium rounded-lg border px-4 py-3"> 295 11 296 </button> 297 298 <button type="button" data-people="12" class="people-option gdrst-body-text font-medium rounded-lg border px-4 py-3"> 299 12 300 </button> 301 302 </div> 303 304 305 <div class="mb-2 text-center"> 306 <input id="peopleCustom" type="number" min="16" max="100" class="hidden w-full rounded-lg border mb-3 px-3 py-2" placeholder="Number of people"> 307 308 <button id="peopleMore" type="button" class="font-medium gdrst-body-text border rounded-lg p-2 hover:underline">More guests?</button> 309 </div> 310 311 312 <div class="flex justify-end mt-2"> 313 <button id="continueBtn" disabled class="btn-next gdrst-nav gdrst-nav-text px-6 py-3 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed">Continue â</button> 314 </div> 315 </div> 316 </div> 317 318 319 320 <div class="step hidden" data-step="1"> 321 322 <div class="flex justify-between items-center mb-2"> 323 324 <button class="btn-back text-red-600 font-semibold">â Back</button> 325 326 </div> 327 328 <div id="date-time-picker" class="w-full max-w-xl mx-auto"> 329 330 331 <div class="flex items-center justify-between mb-4"> 332 <button type="button" id="calendar-prev" class="gdrst-body-text w-10 h-10 rounded-lg border">â</button> 333 334 <h2 id="calendar-title" class="text-lg font-bold gdrst-body-text"></h2> 335 336 <button type="button" id="calendar-next" class="gdrst-body-text w-10 h-10 rounded-lg border">â</button> 337 </div> 338 339 340 <div class="grid grid-cols-7 gap-2 mb-2 text-center"> 341 <div class="text-xs font-semibold gdrst-body-text">Mon</div> 342 <div class="text-xs font-semibold gdrst-body-text">Tue</div> 343 <div class="text-xs font-semibold gdrst-body-text">Wed</div> 344 <div class="text-xs font-semibold gdrst-body-text">Thu</div> 345 <div class="text-xs font-semibold gdrst-body-text">Fri</div> 346 <div class="text-xs font-semibold gdrst-body-text">Sat</div> 347 <div class="text-xs font-semibold gdrst-body-text">Sun</div> 348 </div> 349 350 351 <div id="calendar" class="grid grid-cols-7 grid-rows-6 gap-2 mb-2"></div> 352 353 <div id="noSlotsNoticeContainer" class="hidden bg-[#fff8e1] rounded-lg p-4 flex gap-3 items-start mb-2"> 354 <svg xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 text-yellow-600 flex-shrink-0" viewB
354ox="0 0 24 24" fill="currentColor"> 355 <path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"></path> 356 </svg> 357 358 <div> 359 <strong id="noSlotsNotice" class="block mb-1">No slots for this day, try another day.</strong> 360 </div> 361 </div> 362 363 <a id="eventCard" href="#" target="_blank" rel="noopener" class="hidden mb-3 flex items-center gap-3 rounded-xl border border-amber-200 bg-amber-50 p-4 transition hover:bg-amber-100"> 364 <div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-amber-100 text-xl">â¨</div> 365 366 <div class="min-w-0 flex-1"> 367 <div id="eventCardTitle" class="text-sm font-semibold text-amber-900"></div> 368 <p id="eventCardDescription" class="mt-1 text-sm text-amber-800"></p> 369 </div> 370 371 <svg class="h-5 w-5 shrink-0 text-amber-700" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"> 372 <path d="M5 12h14M13 6l6 6-6 6"></path> 373 </svg> 374 </a> 375 376 377 378 379 <div id="durationContainer" class="border rounded-lg p-[10px_18px] text-base w-full relative cursor-pointer mb-2 flex justify-between items-center"> 380 <div class="gdrst-body-text">Duration</div> 381 382 <div class="relative"> 383 <select id="duration" class="appearance-none border rounded-lg px-[15px] py-3 text-base w-full pr-10 gdrst-body-alt gdrst-body-text"> 384 <option value="2h">2 hours</option> 385 </select> 386 387 <svg class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 -960 960 960" fill="currentColor"> 388 <path d="M480-360 280-560h400L480-360Z" /> 389 </svg> 390 </div> 391 </div> 392 393 394 <div id="slotsGrid" class="grid grid-cols-3 sm:grid-cols-6 gap-2 mb-2"></div> 395 396 <div id="priceSlotInfo" class="hidden flex items-start mt-2 gap-3 rounded-lg border border-green-200 bg-green-50 p-3"> 397 <span class="text-green-600 text-md font-bold leading-none">$</span> 398 399 <p class="text-sm text-green-700">The dollar sign indicates that reservations for this party size at this time require a deposit or advance payment, depending on the restaurant's policy.</p> 400 </div> 401 402 <div id="autoSlotInfo" class="hidden flex items-start mt-2 gap-3 rounded-lg border border-green-200 bg-green-50 p-3"> 403 <span class="mt-1 w-2 h-2 bg-green-500 rounded-full shrink-0"></span> 404 405 <p class="text-sm text-green-700">The green dot indicates an available table for the selected criteria â reservations for this time are confirmed automatically.</p> 406 </div> 407 408 <div class="flex justify-end mt-2"> 409 <button id="continueBtn" disabled class="btn-next gdrst-nav gdrst-nav-text px-6 py-3 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed">Continue â</button> 410 </div> 411 </div> 412 413 </div> 414 415 416 417 <div class="step hidden" data-step="2"> 418 419 <div class="flex justify-between items-center mb-2"> 420 <button class="btn-back text-red-600 font-semibold">â Back</button> 421 422 <h2 class="hidden md:block absolute left-1/2 -translate-x-1/2 text-xl font-bold gdrst-body-text">Contact details</h2> 423 </div> 424 425 <div class="w-full max-w-xl mx-auto"> 426 <h2 class="block md:hidden gdrst-body-text text-xl font-bold mb-2 text-center">Contact details</h2> 427 428 <div id="main-form" class="form-section space-y-4"> 429 430 <div class="form-group"> 431 <input type="text" id="first-name" autocomplete="given-name" required placeholder="First name" data-i18n-hint="" class="w-full gdrst-body gdrst-body-text border rounded-lg px-3 py-3 placeholder:opacity-70"> 432 </div> 433 434 <div class="form-group"> 435 <input type="text" id="last-name" autocomplete="family-name" required placeholder="Last name" data-i18n-hint="" class="w-full gdrst-body gdrst-body-text border rounded-lg px-3 py-3 placeholder:opacity-70"> 436 </div> 437 438 <div class="form-group"> 439 <input type="email" id="email" autocomplete="email" required placeholder="Email" data-i18n-hint="email" class="w-full gdrst-body gdrst-body-text border rounded-lg px-3 py-3 placeholder:opacity-70"> 440 </div> 441 442 <div class="form-group flex items-center gap-1"> 443 <div id="phonePrefix" class="w-80"></div> 444 <input type="tel" id="phone" autocomplete="tel" required placeholder="Phone" data-i18n-hint="phone" class="w-full gdrst-body gdrst-body-text border rounded-lg px-3 py-3 placeholder:opacity-70"> 445 </div> 446 </div> 447 448 <div class="flex justify-end mt-2"> 449 <button id="continueBtn" disabled class="btn-next gdrst-nav gdrst-nav-text px-6 py-3 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed">Continue â</button> 450 </div> 451 </div> 452
453 </div> 454 455 456 457 <div class="step hidden" data-step="3"> 458 <div class="flex justify-between items-center mb-2"> 459 <button class="btn-back text-red-600 font-semibold">â Back</button> 460 461 <h2 class="hidden md:block absolute left-1/2 -translate-x-1/2 text-xl font-bold gdrst-body-text">Details</h2> 462 </div> 463 464 <div class="w-full max-w-xl mx-auto"> 465 <h2 class="block md:hidden gdrst-body-text text-xl font-bold mb-2 text-center">Details</h2> 466 467 468 <div class="border rounded-lg p-[10px_18px] text-base w-full relative cursor-pointer mb-2 flex justify-between items-center"> 469 470 <label for="visitKind" class="block gdrst-body-text">Type of visit</label> 471 472 <div class="relative"> 473 <select id="visitKind" class="appearance-none border rounded-lg px-[15px] py-3 text-base w-full pr-10 gdrst-body-alt gdrst-body-text"> 474 <option value="other">Other</option> 475 <option value="business">Business</option> 476 <option value="traveler">Traveler</option> 477 <option value="romantic">Romantic</option> 478 <option value="family">Family</option> 479 <option value="birthday">Birthday</option> 480 <option value="colleagues"></option> 481 <option value="anniversary">Anniversary</option> 482 <option value="friends">Friends</option> 483 </select> 484 485 <svg class="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 -960 960 960" fill="currentColor"> 486 <path d="M480-360 280-560h400L480-360Z" /> 487 </svg> 488 </div> 489 </div> 490 491 492 493 <div class="form-group mb-4"> 494 <label for="specialNote" class="block gdrst-body-text mb-2">Comment (optional)</label> 495 496 <textarea id="specialNote" name="specialNote" rows="3" class="placeholder:italic border gdrst-body gdrst-body-text rounded-lg px-[15px] py-3 text-base w-full resize-none" placeholder="E.g. special seating requests, accessibility needs, or other preferencesâ¦"></textarea> 497 </div> 498 499 500 501 <div class="mb-4"> 502 503 <label class="flex items-center gap-2 cursor-pointer"> 504 <input type="checkbox" id="taxInvoice" name="tax_invoice" class="size-4"> 505 506 <span class="gdrst-body-text">I want an invoice</span> 507 </label> 508 509 510 <div id="vatDetails" class="hidden mt-3 space-y-3"> 511 512 <input type="text" id="taxNumber" name="vat_id" autocomplete="organization" data-i18n-hint="taxNumber" class="border placeholder:italic rounded-lg px-[15px] py-3 text-base w-full" placeholder="VAT identification number"> 513 514 <input type="text" id="companyName" name="company_name" autocomplete="organization" data-i18n-hint="companyName" class="border placeholder:italic rounded-lg px-[15px] py-3 text-base w-full" placeholder="Company name"> 515 </div> 516 517 </div> 518 519 520 <div class="mb-4 border-b"></div> 521 522 523 <div class="mt-5 space-y-4"> 524 525 526 <label class="flex items-center gap-3 cursor-pointer"> 527 <input type="checkbox" id="selectAllAgreements" class="size-4 shrink-0"> 528 529 <span class="gdrst-body-text font-medium">Select all agreements</span> 530 </label> 531 532 533 <div class="flex-1"> 534 <input type="checkbox" id="termsAgreement" class="mt-1 size-4 shrink-0"> 535 536 <label class="gdrst-body-text" for="termsAgreement"> 537 * I accept terms & privacy policy 538 </label> 539 540 <details class="inline"> 541 <summary class="gdrst-body-text inline cursor-pointer text-sm uppercase"> 542 Expand 543 </summary> 544 545 <div class="block"> 546 <p class="gdrst-body-text mt-2 ml-6 text-sm leading-relaxed"> 547 <a href="https://cdn.goodresto.app/static/web-terms" target="_blank" rel="noopener noreferrer" class="terms-link"> 548 terms 549 </a> 550 <span> & </span> 551 <a href="https://cdn.goodresto.app/static/web-privacy" target="_blank" rel="noopener noreferrer" class="terms-link"> 552 privacy policy 553 </a> 554 </p> 555 </div> 556 </details> 557 </div> 558 559 560 <div class="flex-1"> 561 562 <input type="checkbox" id="marketingRestoAgreement" class="mt-1 size-4 shrink-0"> 563 564 <label for="marketingRestoAgreement" class="gdrst-body-text cursor-pointer">
565 I agree to receive marketing information from the restaurant. 566 </label> 567 568 <details class="inline"> 569 <summary class="gdrst-body-text inline cursor-pointer text-sm uppercase"> 570 Expand 571 </summary> 572 573 <div class="block"> 574 <p class="gdrst-body-text mt-2 ml-6 text-sm leading-relaxed"> 575 I consent to receiving marketing information from Restauracja Casa Italia by electronic means (e-mail, SMS, phone). I can withdraw my consent at any time. 576 </p> 577 </div> 578 </details> 579 580 </div> 581 582 583 584 <div class="flex-1"> 585 586 <input type="checkbox" id="marketingGdrstAgreement" class="mt-1 size-4 shrink-0"> 587 588 <label for="marketingGdrstAgreement" class="gdrst-body-text cursor-pointer"> 589 I agree to receive marketing information from GoodResto. 590 </label> 591 592 <details class="inline"> 593 <summary class="gdrst-body-text inline cursor-pointer text-sm uppercase"> 594 Expand 595 </summary> 596 597 <div class="block"> 598 <p class="gdrst-body-text mt-2 ml-6 text-sm leading-relaxed"> 599 I consent to receiving from GoodResto sp. z o.o. marketing information including promotions, offers, and news regarding the platform and partner restaurants by electronic means (email, SMS, phone). I can withdraw my consent at any time. 600 </p> 601 </div> 602 </details> 603 604 605 </div> 606 607 </div> 608 609 610 <div class="flex justify-center mt-6"> 611 <button id="submitButton" type="submit" onclick="submitReservation()" class="gdrst-nav gdrst-nav-text reserve-button rounded-lg flex w-full h-16 items-center justify-center gap-2 sm:w-1/2"> 612 <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="currentColor" height="20" width="20" viewBox="0 0 24 24" aria-hidden="true"> 613 <path d="M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M16.2,16.2L11,13V7H12.5V12.2L17,14.7L16.2,16.2Z"></path> 614 </svg> 615 616 <span class="gdrst-nav-text">Reserve</span> 617 </button> 618 </div> 619 620 <div class="text-center text-sm gdrst-body-text mt-4">Your best restaurant experience with GoodResto â¤ï¸</div> 621 </div> 622 </div> 623 624 </div> 625 </div> 626 </main> 627 628 <footer class="gdrst-nav px-4 py-4 sm:px-6"> 629 <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"> 630 <div class="gdrst-nav-text order-2 sm:order-1 text-center sm:text-left"> 631 <a href="https://goodresto.app" class="hover:underline">© 2026 GoodResto</a> 632 </div> 633 634 <div class="flex flex-wrap justify-center sm:justify-start gap-x-4 gap-y-2 order-1 sm:order-2"> 635 <a href="https://goodresto.app/privacy" class="gdrst-nav-text">Privacy</a> 636 <a href="https://goodresto.app/terms" class="gdrst-nav-text">Terms</a> 637 </div> 638 639 <div id="darkModeContainer" class="hidden order-3 flex justify-center sm:justify-end"></div> 640 </div> 641 </footer> 642 643
643<script id="page-data" type="application/json">{"groupMax":75,"groupMin":13,"groupNotice":"","hasGroups":true,"i18nDays":["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],"i18nMonths":["January","February","March","April","May","June","July","August","September","October","November","December"],"idempotencyKey":"1YofF1J1nItRctfMGQrzPrbGtRqTjFQp","individualNotice":"","maxDays":180,"peopleMax":12,"peopleMin":1}</script>
643 644
644<script id="lang-data" type="application/json">{"cs":"ÄeÅ¡tina","da":"Dansk","de":"Deutsch","el":"Îλληνικά","en":"English","es":"Español","fr":"Français","hr":"Hrvatski","hu":"Magyar","it":"Italiano","nl":"Nederlands","no":"Norsk","pl":"Polski","pt":"Português","ro":"RomânÄ","sk":"SlovenÄina","sl":"SlovenÅ¡Äina","sv":"Svenska","uk":"УкÑаÑнÑÑка"}</script>
644 645
645<script> 646 const PAGEDATA = JSON.parse(document.getElementById("page-data").textContent); 647 const pageLangs = JSON.parse(document.getElementById("lang-data").textContent); 648 const urlParams = new URLSearchParams(window.location.search); 649 const sessionId = 'zXyFs1b4DkVBl9De'; 650 const restoId = '405036819902'; 651 652 const hostname = location.hostname === "localhost" 653 ? "http://localhost:8080" 654 : location.hostname === "canary-web.goodresto.app" 655 ? "https://canary-web.goodresto.app" 656 : "https://goodresto.app"; 657 658 let currentLang = getLang(); 659 660 661 function formatDate(date) { 662 const year = date.getFullYear(); 663 const month = String(date.getMonth() + 1).padStart(2, "0"); 664 const day = String(date.getDate()).padStart(2, "0"); 665 666 return `${year}-${month}-${day}`; 667 } 668 669 function parseDate(value) { 670 const [year, month, day] = value.split("-").map(Number); 671 672 return new Date(year, month - 1, day); 673 } 674 675 function unixSecondsToDate(unixSeconds) { 676 return new Date(unixSeconds * 1000); 677 } 678 679 function startOfDay(date) { 680 return new Date(date.getFullYear(), date.getMonth(), date.getDate()); 681 } 682 683 function addDays(date, days) { 684 const result = new Date(date); 685 result.setDate(result.getDate() + days); 686 return startOfDay(result); 687 } 688 689 function showError(err) { 690 window.MiniAlert.fire({ 691 title: "Error", 692 text: err, 693 694 buttons: [ 695 { 696 text: "OK" 697 } 698 ] 699 }); 700 } 701 702 703 704 let reserveData = { 705 people: 0, 706 isIndividual: true, 707 date: formatDate(startOfDay(new Date())), 708 duration: "", 709 slotTime: "", 710 guest: {}, 711 signedSlots: "", 712 signedSlots2: "", 713 }; 714 let dateTimePicker; 715 716 class Steps { 717 constructor() { 718 this.steps = [...document.querySelectorAll(".step")]; 719 this.indicators = [...document.querySelectorAll(".step-indicator")]; 720 this.current = 0; 721 722 document.querySelectorAll(".btn-next").forEach(btn => { 723 btn.addEventListener("click", () => this.next()); 724 }); 725 726 document.querySelectorAll(".btn-back").forEach(btn => { 727 btn.addEventListener("click", () => this.back()); 728 }); 729 730 this.update(true); 731 } 732 733 get activeStep() { 734 return this.steps[this.current]; 735 } 736 737 setNextEnabled() { 738 this.activeStep.querySelector(".btn-next").disabled = false; 739 } 740 741 setNextDisabled() { 742 this.activeStep.querySelector(".btn-next").disabled = true; 743 } 744 745 async next() { 746 if (this.current >= this.steps.length - 1) return; 747 748 const btn = document.getElementById("continueBtn"); 749 btn.disabled = true; 750 751 try { 752 this.current++; 753 754 if (this.current == 1) { 755 const res = await fetchSlots( 756 reserveData.date, 757 "_", 758 reserveData.people, 759 ); 760 761 const { durDefault, durations, slots, signedSlots, signedSlots2 } = res; 762 dateTimePicker.setDurations(durations, durDefault); 763 dateTimePicker.setSlots(slots); 764 reserveData.duration = durDefault; 765 reserveData.signedSlots = signedSlots; 766 reserveData.signedSlots2 = signedSlots2; 767 } 768 769 this.update(true); 770 771 if (this.current == 2) { 772 const allSet = 773 reserveData.guest.firstName && 774 reserveData.guest.lastName && 775 reserveData.guest.email && 776 reserveData.guest.phone; 777 778 if (allSet) { 779 this.setNextEnabled(); 780 } 781 } 782 } finally { 783 btn.disabled = false; 784 window.scrollTo(0, 0); 785 } 786 } 787 788 back() { 789 if (this.current > 0) { 790 this.current--; 791 this.update(false); 792 } 793 } 794 795 update(isNext) { 796 this.steps.forEach((step, i) => { 797 step.classList.toggle("hidden", i !== this.current); 798 }); 799 800 this.indicators.forEach((indicator, i) => { 801 const active = i === this.current; 802 const completed = i < this.current; 803 804 indicator.classList.toggle("is-active", active); 805 indicator.classList.toggle("text-gray-900", active || completed); 806 indicator.classList.toggle("text-gray-400", !active && !completed); 807 808 const icon = indicator.querySelector(".step-icon"); 809 810 811 812 icon.classList.toggle("border", active); 813 icon.classList.toggle("border-red-700", active); 814 815 816 icon.classList.toggle("border-gray-300", !active); 817 }); 818 819 const nextBtn = this.activeStep.querySelector(".btn-next"); 820 if (nextBtn) { 821 nextBtn.disabled = isNext; 822 } 823 } 824 } 825 826 const steps = new Steps(); 827 828 829 830 if (urlParams.get("guests")) { 831 reserveData.people = parseInt(urlParams.get("guests"), 10); 832 } 833 if (urlParams.get("date")) {
834 reserveData.date = urlParams.get("date"); 835 } 836 if (urlParams.get("menuKind")) { 837 reserveData.menuKind = urlParams.get("menuKind"); 838 } 839 840 if (reserveData.people) { 841 steps.next(); 842 } 843 844 function detectReferrerSource(urlParams) { 845 const ref = urlParams.get("ref"); 846 const utmSource = urlParams.get("utm_source"); 847 848 if (urlParams.get("rwg_token")) { 849 return 'rwg'; 850 } 851 if (ref) { 852 return ref; 853 } 854 if (utmSource) { 855 return utmSource; 856 } 857 858 const referrer = document.referrer; 859 if (!referrer) return null; 860 861 const sources = [ 862 { domain: 'apple.com/maps', name: 'applemaps' }, 863 { domain: 'facebook.com', name: 'facebook' }, 864 { domain: 'glovoapp.com', name: 'glovo' }, 865 { domain: 'goo.gl/maps', name: 'gmaps' }, 866 { domain: 'google.com', name: 'google' }, 867 { domain: 'google.com/maps', name: 'gmaps' }, 868 { domain: 'instagram.com', name: 'instagram' }, 869 { domain: 'messenger.com', name: 'messenger' }, 870 { domain: 'opentable.com', name: 'opentable' }, 871 { domain: 't.me', name: 'telegram' }, 872 { domain: 'telegram.org', name: 'telegram' }, 873 { domain: 'thefork.com', name: 'thefork' }, 874 { domain: 'tiktok.com', name: 'tiktok' }, 875 { domain: 'tripadvisor.com', name: 'tripadvisor' }, 876 { domain: 'twitter.com', name: 'twitter' }, 877 { domain: 'ubereats.com', name: 'ubereats' }, 878 { domain: 'waze.com', name: 'waze' }, 879 { domain: 'whatsapp.com', name: 'whatsapp' }, 880 { domain: 'x.com', name: 'twitter' }, 881 { domain: 'yahoo.com', name: 'yahoo' }, 882 { domain: 'yelp.com', name: 'yelp' }, 883 ]; 884 885 for (const { domain, name } of sources) { 886 if (referrer.includes(domain)) { 887 return name; 888 } 889 } 890 return null; 891 } 892 893 const ref = detectReferrerSource(urlParams); 894 895 const rwgStartsAt = urlParams.get("starts_at"); 896 const rwgDuration = urlParams.get("duration"); 897 const rwgPartySize = urlParams.get("party_size"); 898 899 if (rwgStartsAt && rwgDuration && rwgPartySize) { 900 901 reserveData.people = parseInt(rwgPartySize, 10); 902 reserveData.date = formatDate(unixSecondsToDate(rwgStartsAt)); 903 reserveData.dur = parseInt(rwgDuration, 10); 904 905 steps.next(); 906 } 907 908 909 910 let isIndividual = true; 911 const noticeContainer = document.getElementById("noticeContainer");
912 const noticeIndividual = document.getElementById("noticeIndividual"); 913 const noticeGroup = document.getElementById("noticeGroup"); 914 const noticeMore = document.getElementById("noticeMore"); 915 916 function activeNotice() { 917 return isIndividual ? noticeIndividual : noticeGroup; 918 } 919 920 function resetNotice() { 921 for (const notice of [noticeIndividual, noticeGroup]) { 922 notice.style.maxHeight = ""; 923 notice.style.overflow = ""; 924 } 925 926 const notice = activeNotice(); 927 const maxHeight = parseFloat(getComputedStyle(notice).lineHeight) * 5; 928 const needsMore = notice.scrollHeight > maxHeight + 1; 929 930 notice.style.maxHeight = needsMore ? `${maxHeight}px` : ""; 931 notice.style.overflow = needsMore ? "hidden" : ""; 932 933 noticeMore.classList.toggle("hidden", !needsMore); 934 noticeMore.classList.toggle("block", needsMore); 935 } 936 937 noticeMore.addEventListener("click", () => { 938 const notice = activeNotice(); 939 notice.style.maxHeight = ""; 940 notice.style.overflow = ""; 941 942 noticeMore.classList.add("hidden"); 943 noticeMore.classList.remove("block"); 944 }); 945 946 const noticeObserver = new MutationObserver(resetNotice); 947 948 for (const notice of [noticeIndividual, noticeGroup]) { 949 noticeObserver.observe(notice, { 950 attributes: true, 951 attributeFilter: ["class"], 952 }); 953 } 954 955 requestAnimationFrame(resetNotice); 956 957 const peopleGrid = document.getElementById("peopleGrid"); 958 const peopleMoreLess = document.getElementById("peopleMore"); 959 const peopleCustom = document.getElementById("peopleCustom"); 960 961 const peopleButtons = [ 962 ...peopleGrid.querySelectorAll(".people-option") 963 ]; 964 965 function peopleUnselect() { 966 peopleButtons.forEach(b => { 967 b.classList.remove("gdrst-nav", "gdrst-nav-text", "font-black"); 968 b.classList.add("gdrst-body-text"); 969 }); 970 } 971 972 function peopleChanged(isIndividual, people) { 973 if (!isIndividual && people > PAGEDATA.groupMax) { 974 steps.setNextDisabled(); 975 showError("The group is too large. This restaurant accepts reservations for up to {max} guests.".replace("{max}", PAGEDATA.groupMax)); 976 return; 977 } 978 979 reserveData.isIndividual = isIndividual; 980 reserveData.people = people; 981 982 if (people) { 983 steps.setNextEnabled(); 984 } else { 985 steps.setNextDisabled(); 986 987 peopleUnselect(); 988 } 989 } 990 991 peopleButtons.forEach(button => { 992 button.addEventListener("click", () => { 993 peopleUnselect(); 994 995 button.classList.add("gdrst-nav", "gdrst-nav-text", "font-black"); 996 button.classList.remove("gdrst-body-text", "font-medium"); 997 998 peopleChanged(isIndividual, Number(button.dataset.people)); 999 }); 1000 }); 1001 1002 if (peopleMoreLess) { 1003 peopleMoreLess.addEventListener("click", () => { 1004 isIndividual = !isIndividual; 1005
1006 noticeIndividual.classList.toggle("hidden"); 1007 noticeGroup.classList.toggle("hidden"); 1008 peopleGrid.classList.toggle("hidden"); 1009 peopleCustom.classList.toggle("hidden"); 1010 peopleChanged(isIndividual, null); 1011 1012 let notice; 1013 if (isIndividual) { 1014 notice = noticeIndividual.innerText; 1015 peopleMoreLess.textContent = "More guests?"; 1016 peopleCustom.value = ''; 1017 } else { 1018 notice = noticeGroup.innerText; 1019 peopleMoreLess.textContent = "Less guests?"; 1020 peopleCustom.focus(); 1021 } 1022 1023 if (!notice) { 1024 noticeContainer.classList.add("hidden"); 1025 } else { 1026 noticeContainer.classList.remove("hidden"); 1027 } 1028 }); 1029 } 1030 1031 if (peopleCustom) { 1032 peopleCustom.addEventListener("input", () => { 1033 const value = peopleCustom.value ? Number(peopleCustom.value) : null; 1034 peopleChanged(isIndividual, value); 1035 }); 1036 } 1037 1038 1039 1040 const autoSlotInfo = document.getElementById("autoSlotInfo"); 1041 const priceSlotInfo = document.getElementById("priceSlotInfo"); 1042 1043 async function fetchSlots(date, dur, people) { 1044 if (!date || !dur || !people) { 1045 return; 1046 } 1047 1048 try { 1049 const params = new URLSearchParams({ date, dur, people, }); 1050 1051 const resp = await fetch( 1052 `${hostname}/api/v1/resto/405036819902/prereserve?${params}` 1053 ); 1054 1055 if (!resp.ok) { 1056 throw new Error(`HTTP ${resp.status}`); 1057 } 1058 1059 return await resp.json(); 1060 } catch (err) { 1061 console.error("fetchSlots:", err); 1062 } 1063 } 1064 1065 1066 1067 const PHONE_COUNTRIES = [ 1068 { iso: "AF", code: "+93", name: "Afghanistan", flag: "ð¦ð«" }, 1069 { iso: "AL", code: "+355", name: "Albania", flag: "ð¦ð±" }, 1070 { iso: "DZ", code: "+213", name: "Algeria", flag: "ð©ð¿" }, 1071 { iso: "AS", code: "+1684", name: "American Samoa", flag: "ð¦ð¸" }, 1072 { iso: "AD", code: "+376", name: "Andorra", flag: "ð¦ð©" }, 1073 { iso: "AO", code: "+244", name: "Angola", flag: "ð¦ð´" }, 1074 { iso: "AI", code: "+1264", name: "Anguilla", flag: "ð¦ð®" }, 1075 { iso: "AQ", code: "+672", name: "Antarctica", flag: "ð¦ð¶" }, 1076 { iso: "AG", code: "+1268", name: "Antigua and Barbuda", flag: "ð¦ð¬" }, 1077 { iso: "AR", code: "+54", name: "Argentina", flag: "ð¦ð·" }, 1078 { iso: "AM", code: "+374", name: "Armenia", flag: "ð¦ð²" }, 1079 { iso: "AW", code: "+297", name: "Aruba", flag: "ð¦ð¼" }, 1080 { iso: "AU", code: "+61", name: "Australia", flag: "ð¦ðº" },
1081 { iso: "AT", code: "+43", name: "Austria", flag: "ð¦ð¹" }, 1082 { iso: "AZ", code: "+994", name: "Azerbaijan", flag: "ð¦ð¿" }, 1083 { iso: "BS", code: "+1242", name: "Bahamas", flag: "ð§ð¸" }, 1084 { iso: "BH", code: "+973", name: "Bahrain", flag: "ð§ð" }, 1085 { iso: "BD", code: "+880", name: "Bangladesh", flag: "ð§ð©" }, 1086 { iso: "BB", code: "+1246", name: "Barbados", flag: "ð§ð§" }, 1087 { iso: "BY", code: "+375", name: "Belarus", flag: "ð§ð¾" }, 1088 { iso: "BE", code: "+32", name: "Belgium", flag: "ð§ðª" }, 1089 { iso: "BZ", code: "+501", name: "Belize", flag: "ð§ð¿" }, 1090 { iso: "BJ", code: "+229", name: "Benin", flag: "ð§ð¯" }, 1091 { iso: "BM", code: "+1441", name: "Bermuda", flag: "ð§ð²" }, 1092 { iso: "BT", code: "+975", name: "Bhutan", flag: "ð§ð¹" }, 1093 { iso: "BO", code: "+591", name: "Bolivia", flag: "ð§ð´" }, 1094 { iso: "BA", code: "+387", name: "Bosnia and Herzegovina", flag: "ð§ð¦" }, 1095 { iso: "BW", code: "+267", name: "Botswana", flag: "ð§ð¼" }, 1096 { iso: "BR", code: "+55", name: "Brazil", flag: "ð§ð·" }, 1097 { iso: "IO", code: "+246", name: "British Indian Ocean Territory", flag: "ð®ð´" }, 1098 { iso: "VG", code: "+1284", name: "British Virgin Islands", flag: "ð»ð¬" }, 1099 { iso: "BN", code: "+673", name: "Brunei", flag: "ð§ð³" }, 1100 { iso: "BG", code: "+359", name: "Bulgaria", flag: "ð§ð¬" }, 1101 { iso: "BF", code: "+226", name: "Burkina Faso", flag: "ð§ð«" }, 1102 { iso: "BI", code: "+257", name: "Burundi", flag: "ð§ð®" }, 1103 { iso: "KH", code: "+855", name: "Cambodia", flag: "ð°ð" }, 1104 { iso: "CM", code: "+237", name: "Cameroon", flag: "ð¨ð²" }, 1105 { iso: "CA", code: "+1", name: "Canada", flag: "ð¨ð¦" }, 1106 { iso: "CV", code: "+238", name: "Cape Verde", flag: "ð¨ð»" }, 1107 { iso: "KY", code: "+1345", name: "Cayman Islands", flag: "ð°ð¾" }, 1108 { iso: "CF", code: "+236", name: "Central African Republic", flag: "ð¨ð«" }, 1109 { iso: "TD", code: "+235", name: "Chad", flag: "ð¹ð©" }, 1110 { iso: "CL", code: "+56", name: "Chile", flag: "ð¨ð±" }, 1111 { iso: "CN", code: "+86", name: "China", flag: "ð¨ð³" }, 1112 { iso: "CX", code: "+61", name: "Christmas Island", flag: "ð¨ð½" }, 1113 { iso: "CC", code: "+61", name: "Cocos Islands", flag: "ð¨ð¨" }, 1114 { iso: "CO", code: "+57", name: "Colombia", flag: "ð¨ð´" }, 1115 { iso: "KM", code: "+269", name: "Comoros", flag: "ð°ð²" }, 1116 { iso: "CK", code: "+682", name: "Cook Islands", flag: "ð¨ð°" }, 1117 { iso: "CR", code: "+506", name: "Costa Rica", flag: "ð¨ð·" }, 1118 { iso: "HR", code: "+385", name: "Croatia", flag: "ðð·" }, 1119 { iso: "CU", code: "+53", name: "Cuba", flag: "ð¨ðº" }, 1120 { iso: "CW", code: "+599", name: "Curacao", flag: "ð¨ð¼" }, 1121 { iso: "CY", code: "+357", name: "Cyprus", flag: "ð¨ð¾" }, 1122 { iso: "CZ", code: "+420", name: "Czech Republic", flag: "ð¨ð¿" }, 1123 { iso: "CD", code: "+243", name: "DR Congo", flag: "ð¨ð©" }, 1124 { iso: "DK", code: "+45", name: "Denmark", flag: "ð©ð°" }, 1125 { iso: "DJ", code: "+253", name: "Djibouti", flag: "ð©ð¯" }, 1126 { iso: "DM", code: "+1767", name: "Dominica", flag: "ð©ð²" }, 1127 { iso: "DO", code: "+1849", name: "Dominican Republic", flag: "ð©ð´" }, 1128 { iso: "TL", code: "+670", name: "East Timor", flag: "ð¹ð±" }, 1129 { iso: "EC", code: "+593", name: "Ecuador", flag: "ðªð¨" }, 1130 { iso: "EG", code: "+20", name: "Egypt", flag: "ðªð¬" }, 1131 { iso: "SV", code: "+503", name: "El Salvador", flag: "ð¸ð»" }, 1132 { iso: "GQ", code: "+240", name: "Equatorial Guinea", flag: "ð¬ð¶" }, 1133 { iso: "ER", code: "+291", name: "Eritrea", flag: "ðªð·" }, 1134 { iso: "EE", code: "+372", name: "Estonia", flag: "ðªðª" }, 1135 { iso: "SZ", code: "+268", name: "Eswatini", flag: "ð¸ð¿" }, 1136 { iso: "ET", code: "+251", name: "Ethiopia", flag: "ðªð¹" }, 1137 { iso: "FK", code: "+500", name: "Falkland Islands", flag: "ð«ð°" }, 1138 { iso: "FO", code: "+298", name: "Faroe Islands", flag: "ð«ð´" }, 1139 { iso: "FJ", code: "+679", name: "Fiji", flag: "ð«ð¯" }, 1140 { iso: "FI", code: "+358", name: "Finland", flag: "ð«ð®" }, 1141 { iso: "FR", code: "+33", name: "France", flag: "ð«ð·" }, 1142 { iso: "GF", code: "+594", name: "French Guiana", flag: "ð¬ð«" }, 1143 { iso: "PF", code: "+689", name: "French Polynesia", flag: "ðµð«" }, 1144 { iso: "GA", code: "+241", name: "Gabon", flag: "ð¬ð¦" }, 1145 { iso: "GM", code: "+220", name: "Gambia", flag: "ð¬ð²" }, 1146 { iso: "GE", code: "+995", name: "Georgia", flag: "ð¬ðª" }, 1147 { iso: "DE", code: "+49", name: "Germany", flag: "ð©ðª" }, 1148 { iso: "GH", code: "+233", name: "Ghana", flag: "ð¬ð" }, 1149 { iso: "GI", code: "+350", name: "Gibraltar", flag: "ð¬ð®" }, 1150 { iso: "GR", code: "+30", name: "Greece", flag: "ð¬ð·" }, 1151 { iso: "GL", code: "+299", name: "Greenland", flag: "ð¬ð±" }, 1152 { iso: "GD", code: "+1473", name: "Grenada", flag: "ð¬ð©" }, 1153 { iso: "GP", code: "+590", name: "Guadeloupe", flag: "ð¬ðµ" }, 1154 { iso: "GU", code: "+1671", name: "Guam", flag: "ð¬ðº" }, 1155 { iso: "GT", code: "+502", name: "Guatemala", flag: "ð¬ð¹" }, 1156 { iso: "GG", code: "+44", name: "Guernsey", flag: "ð¬ð¬" }, 1157 { iso: "GN", code: "+224", name: "Guinea", flag: "ð¬ð³" }, 1158 { iso: "GW", code: "+245", name: "Guinea-Bissau", flag: "ð¬ð¼" }, 1159 { iso: "GY", code: "+592", name: "Guyana", flag: "ð¬ð¾" }, 1160 { iso: "HT", code: "+509", name: "Haiti", flag: "ðð¹" }, 1161 { iso: "HN", code: "+504", name: "Honduras", flag: "ðð³" }, 1162 { iso: "HK", code: "+852", name: "Hong Kong", flag: "ðð°" }, 1163 { iso: "HU", code: "+36", name: "Hungary", flag: "ððº" }, 1164 { iso: "IS", code: "+354", name: "Iceland", flag: "ð®ð¸" }, 1165 { iso: "IN", code: "+91", name: "India", flag: "ð®ð³" }, 1166 { iso: "ID", code: "+62", name: "Indonesia", flag: "ð®ð©" }, 1167 { iso: "IR", code: "+98", name: "Iran", flag: "ð®ð·" }, 1168 { iso: "IQ", code: "+964", name: "Iraq", flag: "ð®ð¶" }, 1169 { iso: "IE", code: "+353", name: "Ireland", flag: "ð®ðª" }, 1170 { iso: "IM", code: "+44", name: "Isle of Man", flag: "ð®ð²" }, 1171 { iso: "IL", code: "+972", name: "Israel", flag: "ð®ð±" }, 1172 { iso: "IT", code: "+39", name: "Italy", flag: "ð®ð¹" }, 1173 { iso: "CI", code: "+225", name: "Ivory Coast", flag: "ð¨ð®" }, 1174 { iso: "JM", code: "+1876", name: "Jamaica", flag: "ð¯ð²" }, 1175 { iso: "JP", code: "+81", name: "Japan", flag: "ð¯ðµ" }, 1176 { iso: "JE", code: "+44", name: "Jersey", flag: "ð¯ðª" }, 1177 { iso: "JO", code: "+962", name: "Jordan", flag: "ð¯ð´" }, 1178 { iso: "KZ", code: "+7", name: "Kazakhstan", flag: "ð°ð¿" }, 1179 { iso: "KE", code: "+254", name: "Kenya", flag: "ð°ðª" }, 1180 { iso: "KI", code: "+686", name: "Kiribati", flag: "ð°ð®" }, 1181 { iso: "XK", code: "+383", name: "Kosovo", flag: "ð½ð°" }, 1182 { iso: "KW", code: "+965", name: "Kuwait", flag: "ð°ð¼" }, 1183 { iso: "KG", code: "+996", name: "Kyrgyzstan", flag: "ð°ð¬" }, 1184 { iso: "LA", code: "+856", name: "Laos", flag: "ð±ð¦" }, 1185 { iso: "LV", code: "+371", name: "Latvia", flag: "ð±ð»" }, 1186 { iso: "LB", code: "+961", name: "Lebanon", flag: "ð±ð§" }, 1187 { iso: "LS", code: "+266", name: "Lesotho", flag: "ð±ð¸" }, 1188 { iso: "LR", code: "+231", name: "Liberia", flag: "ð±ð·" }, 1189 { iso: "LY", code: "+218", name: "Libya", flag: "ð±ð¾" }, 1190 { iso: "LI", code: "+423", name: "Liechtenstein", flag: "ð±ð®" }, 1191 { iso: "LT", code: "+370", name: "Lithuania", flag: "ð±ð¹" }, 1192 { iso: "LU", code: "+352", name: "Luxembourg", flag: "ð±ðº" }, 1193 { iso: "MO", code: "+853", name: "Macau", flag: "ð²ð´" }, 1194 { iso: "MG", code: "+261", name: "Madagascar", flag: "ð²ð¬" },
1195 { iso: "MW", code: "+265", name: "Malawi", flag: "ð²ð¼" }, 1196 { iso: "MY", code: "+60", name: "Malaysia", flag: "ð²ð¾" }, 1197 { iso: "MV", code: "+960", name: "Maldives", flag: "ð²ð»" }, 1198 { iso: "ML", code: "+223", name: "Mali", flag: "ð²ð±" }, 1199 { iso: "MT", code: "+356", name: "Malta", flag: "ð²ð¹" }, 1200 { iso: "MH", code: "+692", name: "Marshall Islands", flag: "ð²ð" }, 1201 { iso: "MQ", code: "+596", name: "Martinique", flag: "ð²ð¶" }, 1202 { iso: "MR", code: "+222", name: "Mauritania", flag: "ð²ð·" }, 1203 { iso: "MU", code: "+230", name: "Mauritius", flag: "ð²ðº" }, 1204 { iso: "YT", code: "+262", name: "Mayotte", flag: "ð¾ð¹" }, 1205 { iso: "MX", code: "+52", name: "Mexico", flag: "ð²ð½" }, 1206 { iso: "FM", code: "+691", name: "Micronesia", flag: "ð«ð²" }, 1207 { iso: "MD", code: "+373", name: "Moldova", flag: "ð²ð©" }, 1208 { iso: "MC", code: "+377", name: "Monaco", flag: "ð²ð¨" }, 1209 { iso: "MN", code: "+976", name: "Mongolia", flag: "ð²ð³" }, 1210 { iso: "ME", code: "+382", name: "Montenegro", flag: "ð²ðª" }, 1211 { iso: "MS", code: "+1664", name: "Montserrat", flag: "ð²ð¸" }, 1212 { iso: "MA", code: "+212", name: "Morocco", flag: "ð²ð¦" }, 1213 { iso: "MZ", code: "+258", name: "Mozambique", flag: "ð²ð¿" }, 1214 { iso: "MM", code: "+95", name: "Myanmar", flag: "ð²ð²" }, 1215 { iso: "NA", code: "+264", name: "Namibia", flag: "ð³ð¦" }, 1216 { iso: "NR", code: "+674", name: "Nauru", flag: "ð³ð·" }, 1217 { iso: "NP", code: "+977", name: "Nepal", flag: "ð³ðµ" }, 1218 { iso: "NL", code: "+31", name: "Netherlands", flag: "ð³ð±" }, 1219 { iso: "NC", code: "+687", name: "New Caledonia", flag: "ð³ð¨" }, 1220 { iso: "NZ", code: "+64", name: "New Zealand", flag: "ð³ð¿" }, 1221 { iso: "NI", code: "+505", name: "Nicaragua", flag: "ð³ð®" }, 1222 { iso: "NE", code: "+227", name: "Niger", flag: "ð³ðª" }, 1223 { iso: "NG", code: "+234", name: "Nigeria", flag: "ð³ð¬" }, 1224 { iso: "NU", code: "+683", name: "Niue", flag: "ð³ðº" }, 1225 { iso: "NF", code: "+672", name: "Norfolk Island", flag: "ð³ð«" }, 1226 { iso: "KP", code: "+850", name: "North Korea", flag: "ð°ðµ" }, 1227 { iso: "MK", code: "+389", name: "North Macedonia", flag: "ð²ð°" }, 1228 { iso: "MP", code: "+1670", name: "Northern Mariana Islands", flag: "ð²ðµ" }, 1229 { iso: "NO", code: "+47", name: "Norway", flag: "ð³ð´" }, 1230 { iso: "OM", code: "+968", name: "Oman", flag: "ð´ð²" }, 1231 { iso: "PK", code: "+92", name: "Pakistan", flag: "ðµð°" }, 1232 { iso: "PW", code: "+680", name: "Palau", flag: "ðµð¼" }, 1233 { iso: "PS", code: "+970", name: "Palestine", flag: "ðµð¸" }, 1234 { iso: "PA", code: "+507", name: "Panama", flag: "ðµð¦" }, 1235 { iso: "PG", code: "+675", name: "Papua New Guinea", flag: "ðµð¬" }, 1236 { iso: "PY", code: "+595", name: "Paraguay", flag: "ðµð¾" }, 1237 { iso: "PE", code: "+51", name: "Peru", flag: "ðµðª" }, 1238 { iso: "PH", code: "+63", name: "Philippines", flag: "ðµð" }, 1239 { iso: "PN", code: "+64", name: "Pitcairn Islands", flag: "ðµð³" }, 1240 { iso: "PL", code: "+48", name: "Poland", flag: "ðµð±" }, 1241 { iso: "PT", code: "+351", name: "Portugal", flag: "ðµð¹" }, 1242 { iso: "PR", code: "+1787", name: "Puerto Rico", flag: "ðµð·" }, 1243 { iso: "QA", code: "+974", name: "Qatar", flag: "ð¶ð¦" }, 1244 { iso: "CG", code: "+242", name: "Republic of the Congo", flag: "ð¨ð¬" }, 1245 { iso: "RE", code: "+262", name: "Reunion", flag: "ð·ðª" }, 1246 { iso: "RO", code: "+40", name: "Romania", flag: "ð·ð´" }, 1247 { iso: "RU", code: "+7", name: "Russia", flag: "ð·ðº" }, 1248 { iso: "RW", code: "+250", name: "Rwanda", flag: "ð·ð¼" }, 1249 { iso: "SH", code: "+290", name: "Saint Helena", flag: "ð¸ð" }, 1250 { iso: "KN", code: "+1869", name: "Saint Kitts and Nevis", flag: "ð°ð³" }, 1251 { iso: "LC", code: "+1758", name: "Saint Lucia", flag: "ð±ð¨" }, 1252 { iso: "PM", code: "+508", name: "Saint Pierre and Miquelon", flag: "ðµð²" }, 1253 { iso: "VC", code: "+1784", name: "Saint Vincent and the Grenadines", flag: "ð»ð¨" }, 1254 { iso: "WS", code: "+685", name: "Samoa", flag: "ð¼ð¸" }, 1255 { iso: "SM", code: "+378", name: "San Marino", flag: "ð¸ð²" }, 1256 { iso: "ST", code: "+239", name: "Sao Tome and Principe", flag: "ð¸ð¹" }, 1257 { iso: "SA", code: "+966", name: "Saudi Arabia", flag: "ð¸ð¦" }, 1258 { iso: "SN", code: "+221", name: "Senegal", flag: "ð¸ð³" }, 1259 { iso: "RS", code: "+381", name: "Serbia", flag: "ð·ð¸" }, 1260 { iso: "SC", code: "+248", name: "Seychelles", flag: "ð¸ð¨" }, 1261 { iso: "SL", code: "+232", name: "Sierra Leone", flag: "ð¸ð±" }, 1262 { iso: "SG", code: "+65", name: "Singapore", flag: "ð¸ð¬" }, 1263 { iso: "SX", code: "+1721", name: "Sint Maarten", flag: "ð¸ð½" }, 1264 { iso: "SK", code: "+421", name: "Slovakia", flag: "ð¸ð°" }, 1265 { iso: "SI", code: "+386", name: "Slovenia", flag: "ð¸ð®" }, 1266 { iso: "SB", code: "+677", name: "Solomon Islands", flag: "ð¸ð§" }, 1267 { iso: "SO", code: "+252", name: "Somalia", flag: "ð¸ð´" }, 1268 { iso: "ZA", code: "+27", name: "South Africa", flag: "ð¿ð¦" }, 1269 { iso: "KR", code: "+82", name: "South Korea", flag: "ð°ð·" }, 1270 { iso: "SS", code: "+211", name: "South Sudan", flag: "ð¸ð¸" }, 1271 { iso: "ES", code: "+34", name: "Spain", flag: "ðªð¸" }, 1272 { iso: "LK", code: "+94", name: "Sri Lanka", flag: "ð±ð°" }, 1273 { iso: "SD", code: "+249", name: "Sudan", flag: "ð¸ð©" }, 1274 { iso: "SR", code: "+597", name: "Suriname", flag: "ð¸ð·" }, 1275 { iso: "SJ", code: "+47", name: "Svalbard and Jan Mayen", flag: "ð¸ð¯" },
1276 { iso: "SE", code: "+46", name: "Sweden", flag: "ð¸ðª" }, 1277 { iso: "CH", code: "+41", name: "Switzerland", flag: "ð¨ð" }, 1278 { iso: "SY", code: "+963", name: "Syria", flag: "ð¸ð¾" }, 1279 { iso: "TW", code: "+886", name: "Taiwan", flag: "ð¹ð¼" }, 1280 { iso: "TJ", code: "+992", name: "Tajikistan", flag: "ð¹ð¯" }, 1281 { iso: "TZ", code: "+255", name: "Tanzania", flag: "ð¹ð¿" }, 1282 { iso: "TH", code: "+66", name: "Thailand", flag: "ð¹ð" }, 1283 { iso: "TG", code: "+228", name: "Togo", flag: "ð¹ð¬" }, 1284 { iso: "TK", code: "+690", name: "Tokelau", flag: "ð¹ð°" }, 1285 { iso: "TO", code: "+676", name: "Tonga", flag: "ð¹ð´" }, 1286 { iso: "TT", code: "+1868", name: "Trinidad and Tobago", flag: "ð¹ð¹" }, 1287 { iso: "TN", code: "+216", name: "Tunisia", flag: "ð¹ð³" }, 1288 { iso: "TR", code: "+90", name: "Turkey", flag: "ð¹ð·" }, 1289 { iso: "TM", code: "+993", name: "Turkmenistan", flag: "ð¹ð²" }, 1290 { iso: "TC", code: "+1649", name: "Turks and Caicos Islands", flag: "ð¹ð¨" }, 1291 { iso: "TV", code: "+688", name: "Tuvalu", flag: "ð¹ð»" }, 1292 { iso: "VI", code: "+1340", name: "US Virgin Islands", flag: "ð»ð®" }, 1293 { iso: "UG", code: "+256", name: "Uganda", flag: "ðºð¬" }, 1294 { iso: "UA", code: "+380", name: "Ukraine", flag: "ðºð¦" }, 1295 { iso: "AE", code: "+971", name: "United Arab Emirates", flag: "ð¦ðª" }, 1296 { iso: "GB", code: "+44", name: "United Kingdom", flag: "ð¬ð§" }, 1297 { iso: "US", code: "+1", name: "United States", flag: "ðºð¸" }, 1298 { iso: "UY", code: "+598", name: "Uruguay", flag: "ðºð¾" }, 1299 { iso: "UZ", code: "+998", name: "Uzbekistan", flag: "ðºð¿" }, 1300 { iso: "VU", code: "+678", name: "Vanuatu", flag: "ð»ðº" }, 1301 { iso: "VA", code: "+379", name: "Vatican City", flag: "ð»ð¦" }, 1302 { iso: "VE", code: "+58", name: "Venezuela", flag: "ð»ðª" }, 1303 { iso: "VN", code: "+84", name: "Vietnam", flag: "ð»ð³" }, 1304 { iso: "WF", code: "+681", name: "Wallis and Futuna", flag: "ð¼ð«" }, 1305 { iso: "EH", code: "+212", name: "Western Sahara", flag: "ðªð" }, 1306 { iso: "YE", code: "+967", name: "Yemen", flag: "ð¾ðª" }, 1307 { iso: "ZM", code: "+260", name: "Zambia", flag: "ð¿ð²" }, 1308 { iso: "ZW", code: "+263", name: "Zimbabwe", flag: "ð¿ð¼" }, 1309 ]; 1310 1311 class PhonePrefixDropdown { 1312 constructor({ 1313 el, 1314 countries = PHONE_COUNTRIES, 1315 defaultCode = "+48", 1316 onChange = null, 1317 } = {}) { 1318 if (!el) { 1319 throw new Error("PhonePrefixDropdown: el is required"); 1320 } 1321 1322 this.el = el; 1323 this.countries = countries; 1324 this.onChange = onChange; 1325 1326 1327 const defaultCountry = countries.find(c => c.code === defaultCode) || 1328 countries[0]; 1329 1330 this.selected = defaultCountry; 1331 this.filtered = [...countries]; 1332 1333 this.render(); 1334 this.cacheElements(); 1335 this.attachEvents(); 1336 this.renderList(); 1337 1338 this.selectedEl.textContent = this.format(defaultCountry); 1339 } 1340 1341 format(country) { 1342 return `${country.code} ${country.flag} ${country.name}`; 1343 } 1344 1345 render() { 1346 this.el.classList.add("relative"); 1347 1348 this.el.innerHTML = ` 1349 <button 1350 type="button" class="prefix-trigger gdrst-body-text rounded-lg p-[12px_18px] text-base w-full flex justify-between items-center border text-left"> 1351 <span class="selected-prefix truncate"></span> 1352 <span class="text-xs opacity-60">â¼</span> 1353 </button> 1354 1355 <div class="dropdown-list hidden absolute z-10 mt-1 w-full bg-white border rounded-lg shadow max-h-80 overflow-hidden flex flex-col"> 1356 <div class="p-2 border-b"> 1357 <input 1358 type="text" 1359 placeholder="Search..." 1360 class="search-input w-full px-3 py-2 border rounded-lg 1361 text-sm outline-none focus:ring-2 focus:ring-gray-200" 1362 /> 1363 </div> 1364 1365 <div class="list-container overflow-y-auto max-h-64"></div> 1366 </div> 1367 `; 1368 } 1369 1370 cacheElements() { 1371 this.trigger = this.el.querySelector(".prefix-trigger"); 1372 this.dropdown = this.el.querySelector(".dropdown-list"); 1373 this.selectedEl = this.el.querySelector(".selected-prefix"); 1374 this.searchInput = this.el.querySelector(".search-input"); 1375 this.listContainer = this.el.querySelector(".list-container"); 1376 } 1377 1378 attachEvents() { 1379 this.trigger.addEventListener("click", () => { 1380 this.toggle(); 1381 }); 1382 1383 this.searchInput.addEventListener("input", () => { 1384 this.filter(this.searchInput.value); 1385 }); 1386 1387 this.listContainer.addEventListener("click", e => { 1388 const item = e.target.closest("[data-iso]"); 1389 if (!item) return; 1390 1391 this.select(item.dataset.iso); 1392 }); 1393 1394 document.addEventListener("click", this.handleOutsideClick); 1395 } 1396 1397 handleOutsideClick = e => { 1398 if (!this.el.contains(e.target)) { 1399 this.close(); 1400 } 1401 }; 1402 1403 filter(query) { 1404 query = query.trim().toLowerCase(); 1405 1406 this.filtered = query 1407 ? this.countries.filter(country => 1408 country.name.toLowerCase().includes(query) || 1409 country.code.includes(query) || 1410 country.iso.toLowerCase().includes(query) 1411 ) 1412 : [...this.countries]; 1413 1414 this.renderList(); 1415 } 1416 1417 renderList() { 1418 this.listContainer.innerHTML = this.filtered 1419 .map(country => ` 1420 <button 1421 type="button" 1422 class="block w-full px-3 py-2 text-left 1423 hover:bg-gray-100 cursor-pointer"
1424 data-iso="${country.iso}" 1425 > 1426 ${this.format(country)} 1427 </button> 1428 `) 1429 .join(""); 1430 } 1431 1432 select(iso) { 1433 const country = this.countries.find(c => c.iso === iso); 1434 if (!country) return; 1435 1436 this.selected = country; 1437 this.selectedEl.textContent = this.format(country); 1438 1439 this.searchInput.value = ""; 1440 this.filtered = [...this.countries]; 1441 1442 this.renderList(); 1443 this.close(); 1444 1445 this.onChange?.(country); 1446 } 1447 1448 toggle() { 1449 this.dropdown.classList.contains("hidden") 1450 ? this.open() 1451 : this.close(); 1452 } 1453 1454 open() { 1455 this.dropdown.classList.remove("hidden"); 1456 this.searchInput.focus(); 1457 } 1458 1459 close() { 1460 this.dropdown.classList.add("hidden"); 1461 } 1462 1463 destroy() { 1464 document.removeEventListener("click", this.handleOutsideClick); 1465 } 1466 } 1467 1468 const phonePrefixDropdown = new PhonePrefixDropdown({ 1469 el: document.querySelector("#phonePrefix"), 1470 defaultCountry: "\u002b48", 1471 onChange: country => { 1472 reserveData.guest.phoneCode = country.code; 1473 } 1474 }); 1475 1476 function onGuestFieldsComplete(callback) { 1477 const firstNameField = document.getElementById("first-name"); 1478 const lastNameField = document.getElementById("last-name"); 1479 const emailField = document.getElementById("email"); 1480 const phoneField = document.getElementById("phone"); 1481 1482 const fields = [ 1483 firstNameField, 1484 lastNameField, 1485 emailField, 1486 phoneField, 1487 ]; 1488 1489 const isValidEmail = email => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); 1490 1491 const isValidPhone = phone => { 1492 const normalized = phone.replace(/[\s().-]/g, ""); 1493 return /^\+?[0-9]{7,15}$/.test(normalized); 1494 }; 1495 1496 const check = () => { 1497 if (steps.current !== 2) { 1498 return; 1499 } 1500 1501 const firstName = firstNameField.value.trim(); 1502 const lastName = lastNameField.value.trim(); 1503 const email = emailField.value.trim(); 1504 const phone = phoneField.value.trim(); 1505 1506 emailField.setCustomValidity( 1507 email && !isValidEmail(email) ? "Invalid email address" : "" 1508 ); 1509 1510 phoneField.setCustomValidity( 1511 phone && !isValidPhone(phone) ? "Invalid phone number" : "" 1512 ); 1513 1514 if ( 1515 !firstName || 1516 !lastName || 1517 !email || 1518 !phone || 1519 !isValidEmail(email) || 1520 !isValidPhone(phone) 1521 ) { 1522 return false; 1523 } 1524 1525 callback(firstName, lastName, email, phone); 1526 return true; 1527 }; 1528 1529 const validateFields = () => { 1530 if (steps.current === 2 && !check()) { 1531 steps.setNextDisabled(); 1532 } 1533 }; 1534 1535 fields.forEach(field => { 1536 ["input", "change", "blur"].forEach(event => { 1537 field.addEventListener(event, validateFields); 1538 }); 1539 }); 1540 1541 1542 const interval = setInterval(() => { 1543 if (steps.current !== 2) { 1544 return; 1545 } 1546 if (check()) { 1547 clearInterval(interval); 1548 } else { 1549 steps.setNextDisabled(); 1550 } 1551 }, 300); 1552 1553 check(); 1554 } 1555 1556 onGuestFieldsComplete((firstName, lastName, email, phone) => { 1557 reserveData.guest = { 1558 firstName, 1559 lastName, 1560 email, 1561 phone, 1562 }; 1563 1564 steps.setNextEnabled(); 1565 }); 1566 1567 1568 1569 const taxInvoice = document.getElementById('taxInvoice'); 1570 const vatDetails = document.getElementById('vatDetails'); 1571 1572 1573 if (taxInvoice) { 1574 taxInvoice.addEventListener('change', () => { 1575 vatDetails.classList.toggle('hidden', !taxInvoice.checked); 1576 1577 if (!taxInvoice.checked) { 1578 document.getElementById('taxNumber').value = ''; 1579 document.getElementById('companyName').value = ''; 1580 } 1581 }); 1582 } 1583 1584 const termsAgreement = document.getElementById('termsAgreement'); 1585 const selectAllAgreements = document.getElementById('selectAllAgreements'); 1586 1587 const agreements = [ 1588 document.getElementById('termsAgreement'), 1589 document.getElementById('marketingRestoAgreement'), 1590 document.getElementById('marketingGdrstAgreement') 1591 ]; 1592 1593 1594 selectAllAgreements.addEventListener('change', () => { 1595 agreements.forEach(checkbox => { 1596 checkbox.checked = selectAllAgreements.checked; 1597 }); 1598 }); 1599 1600 1601 agreements.forEach(checkbox => { 1602 checkbox.addEventListener('change', () => { 1603 selectAllAgreements.checked = agreements.every( 1604 checkbox => checkbox.checked 1605 ); 1606 1607 selectAllAgreements.indeterminate = 1608 !selectAllAgreements.checked && 1609 agreements.some(checkbox => checkbox.checked); 1610 }); 1611 }); 1612 1613 1614 function getDetailsData() { 1615 const data = { 1616 visitKind: document.getElementById('visitKind').value, 1617 specialNote: document.getElementById('specialNote').value.trim(), 1618 1619 taxInvoice: { 1620 enabled: taxInvoice?.checked ?? false, 1621 vatId: document.getElementById('taxNumber')?.value?.trim() ?? '', 1622 companyName: document.getElementById('companyName')?.value?.trim() ?? '', 1623 }, 1624 1625 marketing: { 1626 }, 1627 }; 1628 1629 return data; 1630 } 1631 1632 let isSubmitClicked = false;
1633 1634 async function submitReservation() { 1635 if (isSubmitClicked) return; 1636 isSubmitClicked = true; 1637 1638 if (!termsAgreement.checked) { 1639 isSubmitClicked = false; 1640 showError('Please accept the terms and conditions and privacy policy.'); 1641 return 1642 } 1643 1644 const rawParams = Object.fromEntries(urlParams); 1645 const selectedSlot = document.querySelector('.time-slot.selected'); 1646 const time = selectedSlot ? selectedSlot.firstElementChild.textContent.trim() : null; 1647 const price = selectedSlot.dataset.price; 1648 const menuKind = this.menuKind ? this.menuKind.value : ''; 1649 const details = getDetailsData(); 1650 const marketingResto = document.getElementById('marketingRestoAgreement').checked; 1651 const marketingGdrst = document.getElementById('marketingGdrstAgreement').checked; 1652 1653 const payload = { 1654 date: reserveData.date, 1655 time: reserveData.slotTime, 1656 dur: reserveData.duration, 1657 price: reserveData.slotPrice, 1658 people: reserveData.people, 1659 isGroup: !reserveData.isIndividual, 1660 1661 kind: details.visitKind, 1662 menuKind: menuKind, 1663 specialNote: details.specialNote, 1664 1665 wantInvoice: details.taxInvoice.enabled, 1666 taxNumber: details.taxInvoice.vatId, 1667 companyName: details.taxInvoice.companyName, 1668 1669 firstname: reserveData.guest.firstName, 1670 lastname: reserveData.guest.lastName, 1671 email: reserveData.guest.email, 1672 phone: reserveData.guest.phone, 1673 phoneCode: reserveData.guest.phoneCode, 1674 1675 marketingResto: marketingResto, 1676 marketingGdrst: marketingGdrst, 1677 1678 urlParams: rawParams, 1679 signedSlots: reserveData.signedSlots, 1680 signedSlots2: reserveData.signedSlots2, 1681 1682 idempotencyKey: PAGEDATA.idempotencyKey, 1683 }; 1684 1685 try { 1686 const { result, error } = await doSubmitReservation(payload); 1687 if (error) { 1688 showError(error.message); 1689 return; 1690 } 1691 1692 if (result.redirectLink) { 1693 window.location.href = result.redirectLink; 1694 return; 1695 } 1696 1697 showSubmitSuccess(result.message); 1698 } finally { 1699 isSubmitClicked = false; 1700 1701 const gtagConv = ''; 1702 if (gtagConv) { 1703 gtag('event', 'conversion', { 'send_to': gtagConv }); 1704 } 1705 } 1706 } 1707 1708 function showSubmitSuccess(msg) { 1709 window.MiniAlert.fire({ 1710 title: "Success", 1711 text: msg, 1712 1713 buttons: [ 1714 { 1715 id: "ok", 1716 text: "OK" 1717 } 1718 ], 1719 1720 onAction: async ({ id, values }) => { 1721 window.location.href = "/"; 1722 } 1723 }); 1724 } 1725 1726 async function doSubmitReservation(payload) { 1727 try { 1728 const resp = await fetch(`${hostname}/api/v1/resto/405036819902/reserve`, { 1729 method: 'POST', 1730 headers: { 1731 'Content-Type': 'application/json; charset=UTF-8', 1732 'Language': currentLang || 'en', 1733 'Ref': ref || 'web', 1734 }, 1735 body: JSON.stringify(payload), 1736 }); 1737 1738 const result = await resp.json(); 1739 1740 if (!resp.ok) { 1741 return { result: null, error: result }; 1742 } 1743 1744 return { result, error: null }; 1745 } catch (err) { 1746 return { 1747 result: null, 1748 error: { 1749 message: err instanceof Error ? err.message : String(err), 1750 }, 1751 }; 1752 } 1753 } 1754 1755 1756 class DateTimePicker { 1757 constructor(options = {}) { 1758 this.root = document.getElementById(options.id); 1759 1760 if (!this.root) { 1761 throw new Error(`DateTimePicker: #${options.id} not found`); 1762 } 1763 1764 this.maxFutureDays = options.maxFutureDays ?? 30; 1765 1766 this.selectedDate = options.selectedDate 1767 ? parseDate(options.selectedDate) 1768 : startOfDay(new Date()); 1769 1770 this.selectedDuration = options.selectedDuration ?? "02:00"; 1771 1772 this.selectedSlot = null; 1773 1774 1775 1776 this.events = options.events ?? {}; 1777 1778 this.availableDurs = []; 1779 this.availableSlots = []; 1780 1781 this.currentMonth = new Date( 1782 this.selectedDate.getFullYear(), 1783 this.selectedDate.getMonth(), 1784 1 1785 ); 1786 1787 this.onDateChange = options.onDateChange ?? (() => { }); 1788 this.onDurationChange = options.onDurationChange ?? (() => { }); 1789 this.onSlotChange = options.onSlotChange ?? (() => { }); 1790 1791 this.calendar = this.root.querySelector("#calendar"); 1792 this.calendarTitle = this.root.querySelector("#calendar-title"); 1793 this.calendarPrevButton = this.root.querySelector("#calendar-prev"); 1794 this.calendarNextButton = this.root.querySelector("#calendar-next"); 1795 1796 this.eventCard = this.root.querySelector("#eventCard"); 1797 this.eventCardTitle = this.root.querySelector("#eventCardTitle"); 1798 this.eventCardDescription = this.root.querySelector("#eventCardDescription"); 1799 1800 this.noSlotsNoticeContainer = this.root.querySelector("#noSlotsNoticeContainer"); 1801 this.noSlotsNotice = this.root.querySelector("#noSlotsNotice"); 1802 1803 this.menuKindContainer = this.root.querySelector("#menuKindContainer"); 1804 this.durationContainer = this.root.querySelector("#durationContainer"); 1805 1806 this.menuKind = this.root.querySelector("#menuKind"); 1807 this.duration = this.root.querySelector("#duration"); 1808 this.slots = this.root.querySelector("#slotsGrid"); 1809 1810 this.bindEvents(); 1811 1812 this.duration.value = this.selectedDuration; 1813 1814 this.render(); 1815 } 1816 1817 bindEvents() { 1818 this.calendarPrevButton.addEventListener("click", () => this.prevMonth()); 1819 this.calendarNextButton.addEventListener("click", () => this.nextMonth()); 1820 this.duration.addEventListener("change", event => { 1821 this.selectedDuration = event.target.value; 1822 this.selectedSlot = null; 1823 1824 this.renderSlots(); 1825 1826 this.onDurationChange(this.selectedDuration); 1827 }); 1828 } 1829 1830 render() {
1831 this.renderCalendar(); 1832 this.renderSlots(); 1833 } 1834 1835 1836 1837 1838 renderCalendar() { 1839 this.eventCard.classList.add("hidden"); 1840 1841 const year = this.currentMonth.getFullYear(); 1842 const month = this.currentMonth.getMonth(); 1843 1844 this.calendarTitle.textContent = 1845 new Intl.DateTimeFormat(undefined, { 1846 month: "long", 1847 year: "numeric" 1848 }).format(this.currentMonth); 1849 1850 this.calendar.innerHTML = ""; 1851 1852 const firstDay = new Date(year, month, 1).getDay(); 1853 const mondayIndex = (firstDay + 6) % 7; 1854 const daysInMonth = new Date(year, month + 1, 0).getDate(); 1855 1856 const today = startOfDay(new Date()); 1857 const maxDate = addDays(today, this.maxFutureDays); 1858 1859 1860 1861 for (let cell = 0; cell < 42; cell++) { 1862 const dayNumber = cell - mondayIndex + 1; 1863 const cellElement = document.createElement("div"); 1864 1865 if (dayNumber > daysInMonth) { 1866 break; 1867 } 1868 if (dayNumber < 1) { 1869 cellElement.className = "w-full h-11"; 1870 this.calendar.appendChild(cellElement); 1871 continue; 1872 } 1873 1874 const date = new Date(year, month, dayNumber); 1875 const dateString = formatDate(date); 1876 const isPast = date < today; 1877 const isTooFar = date > maxDate; 1878 const isDisabled = isPast || isTooFar; 1879 const isSelected = this.selectedDate && 1880 formatDate(this.selectedDate) === dateString; 1881 1882 const isToday = formatDate(today) === dateString; 1883 1884 1885 1886 const event = this.events[dateString]; 1887 1888 const button = document.createElement("button"); 1889 button.type = "button"; 1890 1891 button.className = [ 1892 "relative", 1893 "w-full", 1894 "h-11", 1895 "rounded-lg", 1896 "border", 1897 "transition", 1898 "font-medium" 1899 ].join(" "); 1900 1901 1902 1903 const day = document.createElement("span"); 1904 day.textContent = dayNumber; 1905 1906 1907 1908 day.className = "absolute inset-0 flex items-center justify-center gdrst-body-text font-medium"; 1909 1910 button.appendChild(day); 1911 1912 1913 1914 if (event) { 1915 const marker = document.createElement("span"); 1916 1917 marker.className = [ 1918 "absolute", 1919 "bottom-1", 1920 "left-1/2", 1921 "-translate-x-1/2", 1922 "w-1.5", 1923 "h-1.5", 1924 "rounded-full", 1925 "bg-red-500" 1926 ].join(" "); 1927 1928 1929 1930 marker.title = event.label ?? "Special event"; 1931 button.appendChild(marker); 1932 1933 if (formatDate(this.selectedDate) === dateString) { 1934 this.eventCard.classList.remove("hidden"); 1935 this.eventCard.href = event.link; 1936 this.eventCardTitle.textContent = event.label; 1937 this.eventCardDescription.textContent = event.descr; 1938 } 1939 } 1940 1941 1942 1943 if (isDisabled) { 1944 button.classList.add( 1945 "text-gray-500", 1946 "border-gray-500", 1947 "cursor-not-allowed", 1948 "relative", 1949 "w-full", 1950 "h-11", 1951 "rounded-lg", 1952 "border", 1953 "border-gray-500", 1954 "font-medium", 1955 "text-gray-500", 1956 "opacity-50", 1957 "cursor-not-allowed", 1958 "transition", 1959 ); 1960 1961 button.disabled = true; 1962 } else if (isSelected) { 1963 button.classList.add("gdrst-nav", "gdrst-nav-text", "font-black"); 1964 button.classList.remove("gdrst-body-text", "font-medium"); 1965 day.classList.add("font-black"); 1966 day.classList.remove("gdrst-body-text", "font-medium"); 1967 1968 1969 1970 if (event) { 1971 const marker = button.querySelector("span:last-child"); 1972 marker.classList.remove("bg-red-500"); 1973 marker.classList.add("bg-white"); 1974 } 1975 } else if (isToday) { 1976 button.classList.add( 1977 "text-red-600", 1978 "font-bold", 1979 ); 1980 } else { 1981 button.classList.add( 1982 "border-gray-200", 1983 "hover:border-red-300" 1984 ); 1985 } 1986 1987 button.addEventListener("click", () => this.selectDate(date)); 1988 this.calendar.appendChild(button); 1989 } 1990 1991 this.updateMonthNavigation(); 1992 } 1993 1994 selectDate(date) { 1995 steps.setNextDisabled(); 1996 reserveData.slotTime = null; 1997 1998 this.selectedDate = startOfDay(date); 1999 this.selectedSlot = null; 2000 2001 this.renderCalendar(); 2002 this.renderSlots(); 2003 2004 this.onDateChange(formatDate(this.selectedDate)); 2005 } 2006 2007 2008 2009 2010 setEvents(events) { 2011 2012 2013 this.events = events ?? {}; 2014
2015 this.renderCalendar(); 2016 } 2017 2018 setEvent(date, event = {}) { 2019 2020 2021 this.events[date] = event; 2022 2023 this.renderCalendar(); 2024 } 2025 2026 removeEvent(date) { 2027 delete this.events[date]; 2028 2029 this.renderCalendar(); 2030 } 2031 2032 getEvent(date) { 2033 return this.events[date] ?? null; 2034 } 2035 2036 2037 2038 2039 prevMonth() { 2040 if (this.calendarPrevButton.disabled) { 2041 return; 2042 } 2043 2044 this.currentMonth = new Date( 2045 this.currentMonth.getFullYear(), 2046 this.currentMonth.getMonth() - 1, 2047 1 2048 ); 2049 2050 this.renderCalendar(); 2051 } 2052 2053 nextMonth() { 2054 if (this.calendarNextButton.disabled) { 2055 return; 2056 } 2057 2058 this.currentMonth = new Date( 2059 this.currentMonth.getFullYear(), 2060 this.currentMonth.getMonth() + 1, 2061 1 2062 ); 2063 2064 this.renderCalendar(); 2065 } 2066 2067 updateMonthNavigation() { 2068 const today = startOfDay(new Date()); 2069 const maxDate = addDays(today, this.maxFutureDays); 2070 const monthStart = new Date(this.currentMonth.getFullYear(), this.currentMonth.getMonth(), 1); 2071 const prevMonthEnd = new Date(monthStart.getFullYear(), monthStart.getMonth(), 0); 2072 2073 this.calendarPrevButton.disabled = prevMonthEnd < today; 2074 this.calendarNextButton.disabled = monthStart > maxDate; 2075 2076 2077 2078 } 2079 2080 2081 2082 2083 setDurations(durs, defaultDur = undefined) { 2084 this.availableDurs = durs ?? []; 2085 2086 this.duration.innerHTML = this.availableDurs 2087 .map(dur => ` 2088 <option value="${dur.value}" ${dur.value === defaultDur ? "selected" : ""}> 2089 ${dur.text} 2090 </option> 2091 `) 2092 .join(""); 2093 2094 if (defaultDur != null && this.availableDurs.some(dur => dur.value === defaultDur)) { 2095 this.duration.value = defaultDur; 2096 } 2097 } 2098 2099 setSlots(slots) { 2100 this.availableSlots = slots ?? []; 2101 this.selectedSlot = null; 2102 2103 this.renderSlots(); 2104 2105 if (!slots || slots.length == 0) { 2106 this.noSlotsNoticeContainer?.classList.remove('hidden'); 2107 this.menuKindContainer?.classList.add('hidden'); 2108 this.durationContainer?.classList.add('hidden'); 2109 } else { 2110 this.noSlotsNoticeContainer?.classList.add('hidden'); 2111 this.menuKindContainer?.classList.remove('hidden'); 2112 this.durationContainer?.classList.remove('hidden'); 2113 } 2114 } 2115 2116 renderSlots() { 2117 this.slots.innerHTML = ""; 2118 2119 for (const slot of this.availableSlots) { 2120 const element = document.createElement("div"); 2121 element.dataset.time = slot.time; 2122 2123 if (slot.isAuto) { 2124 element.dataset.isAuto = "1"; 2125 } 2126 2127 if (slot.price) { 2128 element.dataset.price = slot.price; 2129 } 2130 2131 element.className = [ 2132 "time-slot", 2133 "flex", 2134 "items-center", 2135 "gap-1", 2136 "justify-between", 2137 "border", 2138 "border", 2139 "rounded-lg", 2140 "px-3", 2141 "py-3", 2142 "cursor-pointer", 2143 "transition", 2144 "gdrst-body-text", 2145 "font-medium", 2146 ].join(" "); 2147 2148 if (this.selectedSlot?.time === slot.time) { 2149 element.classList.add( 2150 "gdrst-nav", 2151 "gdrst-nav-text", 2152 "selected", 2153 "font-black", 2154 ); 2155 element.classList.remove("gdrst-body-text", "font-medium"); 2156 } 2157 2158 const time = document.createElement("span"); 2159 time.textContent = slot.time; 2160 element.appendChild(time); 2161 2162 const indicators = document.createElement("span"); 2163 indicators.className = "flex items-center gap-1"; 2164 2165 if (slot.isAuto) { 2166 const auto = document.createElement("span"); 2167 2168 auto.className = [ 2169 "w-2", 2170 "h-2", 2171 "bg-green-500", 2172 "rounded-full" 2173 ].join(" "); 2174 2175 indicators.prepend(auto); 2176 } 2177 2178 if (slot.price) { 2179 const price = document.createElement("span"); 2180 2181 price.textContent = "$"; 2182 2183 price.className = [ 2184 "text-green-600", 2185 "text-md", 2186 "font-bold", 2187 "leading-none" 2188 ].join(" "); 2189 2190 indicators.appendChild(price); 2191 } 2192 2193 element.appendChild(indicators); 2194 element.addEventListener("click", () => this.selectSlot(slot)); 2195 2196 this.slots.appendChild(element); 2197 } 2198 } 2199 2200 selectSlot(slot) { 2201 this.selectedSlot = slot; 2202 2203 this.renderSlots(); 2204 2205 this.onSlotChange(slot); 2206 } 2207 } 2208 2209 dateTimePicker = new DateTimePicker({ 2210 id: "date-time-picker", 2211 selectedDate: reserveData.date, 2212 months: [], 2213 weekdays: [], 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 maxFutureDays: PAGEDATA.maxDays, 2234 2235 onDateChange: async date => { 2236 reserveData.date = date; 2237 2238 const { durDefault, durations, slots, signedSlots, signedSlots2 } = await fetchSlots(date, reserveData.duration, reserveData.people); 2239 dateTimePicker.setDurations(durations, durDefault); 2240 dateTimePicker.setSlots(slots); 2241 reserveData.signedSlots = signedSlots; 2242 reserveData.signedSlots2 = signedSlots2; 2243 2244 if (reserveData.date && reserveData.duration && reserveData.slotTime) { 2245 steps.setNextEnabled(); 2246 } 2247 }, 2248 2249 onDurationChange: duration => { 2250 reserveData.duration = duration; 2251 2252 if (reserveData.date && reserveData.duration && reserveData.slotTime) { 2253 steps.setNextEnabled(); 2254 } 2255 }, 2256 2257 onSlotChange: slot => { 2258 reserveData.slotTime = slot.time; 2259 reserveData.slotIsAuto = slot.isAuto; 2260 reserveData.slotPrice = slot.price; 2261 2262 if (slot.isAuto) { 2263 autoSlotInfo.classList.remove("hidden"); 2264 } else { 2265 autoSlotInfo.classList.add("hidden"); 2266 } 2267 2268 if (slot.price) { 2269 priceSlotInfo.classList.remove("hidden"); 2270 } else { 2271 priceSlotInfo.classList.add("hidden"); 2272 } 2273 2274 if (reserveData.date && reserveData.duration && reserveData.slotTime) { 2275 steps.setNextEnabled(); 2276 } 2277 } 2278 }); 2279 2280 let header = new NavHeader('header-container', { 2281 navName: 'Casa Italia', 2282 isMichelin: `false`, 2283 isMichelinBib: `false`, 2284 isStarWine: `false`, 2285 isGaultMillau: `false`, 2286 logoUrl: 'https:\/\/cdn.goodresto.app\/id\/382549298295_md', 2287 activeNavLink: '\/reserve', 2288 2289 navLinks: [ 2290 { label: 'Home page', href: '/', i18n: 'home' }, 2291 { label: 'Menu', href: '/menu', i18n: 'menu' },
2292 { label: 'Reservation', href: '/reserve', i18n: 'reservation' }, 2293 ], 2294 }); 2295 2296 const langSwitcher = new LanguageDialog('lang-btn', pageLangs, { 2297 noUpdate: true, 2298 defaultLang: getLang(), 2299 onLanguageChange: (lang) => { 2300 window.location.reload(); 2301 } 2302 }); 2303 2304 window.addEventListener("DOMContentLoaded", () => { 2305 new CookieConsent({ 2306 gIds: "G-WZLNJ9YQSY".split(","), 2307 metaPixelIds: "1815606706548107".split(","), 2308 }); 2309 }); 2310 </script>
2310 2311 2312</body> 2313 2314</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.