1/** 2 * Redcat Booking System JavaScript 3 * Simple implementation - just the essentials to make it work 4 */ 5 6(function () { 7 "use strict"; 8 9 // Main booking object 10 const RedcatBooking = { 11 panel: null, 12 overlay: null, 13 currentTab: "table", 14 bookingUrl: "https://booking.eu.guestline.app/RCI/availability", // Default URL 15 hotelCode: "RCISTANF", // Default hotel code 16 tableWidget: "", 17 partyShortcode: "", 18 19 init: function () { 20 this.panel = document.getElementById("redcat-booking-panel"); 21 22 if (!this.panel) { 23 console.warn("Booking panel not found"); 24 return; 25 } 26 27 this.overlay = this.panel.querySelector(".booking-panel-overlay"); 28 this.setupEventListeners(); 29 this.loadDynamicContent(); 30 }, 31 32 loadDynamicContent: function () { 33 // Look for booking buttons container to get dynamic settings 34 const bookingContainer = document.querySelector( 35 ".redcat-booking-buttons" 36 ); 37 38 if (bookingContainer) { 39 console.log("Found booking container:", bookingContainer); // Debug 40 41 // Get dynamic hotel code 42 const customHotelCode = 43 bookingContainer.getAttribute("data-hotel-code"); 44 if (customHotelCode) { 45 this.hotelCode = customHotelCode; 46 console.log("Set hotel code:", this.hotelCode); // Debug 47 } 48 49 // Get dynamic booking URL 50 const customUrl = bookingContainer.getAttribute("data-booking-url"); 51 if (customUrl) { 52 this.bookingUrl = customUrl; 53 console.log("Set booking URL:", this.bookingUrl); // Debug 54 } 55 56 // Get table widget content (updated from iframe to widget code) 57 const widgetContent = 58 bookingContainer.getAttribute("data-table-widget"); 59 if (widgetContent) { 60 this.tableWidget = widgetContent; 61 console.log("Found table widget content"); // Debug 62 this.setupTableWidget(); 63 } 64 65 // The party shortcode is now handled server-side, so we don't need to load it here 66 // Just check if it exists for status reporting 67 const shortcodeInput = document.getElementById("party-shortcode-data"); 68 if (shortcodeInput && shortcodeInput.value) { 69 console.log("Party shortcode will be processed server-side"); // Debug 70 } 71 } else { 72 console.log("No booking container found"); // Debug 73 } 74 }, 75 76 setupTableWidget: function () { 77 // Check if widget is already loaded server-side 78 const tableContent = this.panel.querySelector("#table-booking-content"); 79 const placeholder = this.panel.querySelector("#table-placeholder"); 80 const existingWidget = tableContent 81 ? tableContent.querySelector(".table-widget-container") 82 : null; 83 84 // If server-side rendering worked, don't override it 85 if (existingWidget) { 86 console.log("Table widget already loaded server-side"); 87 return; 88 } 89 90 // Client-side fallback for dynamic loading 91 if (!this.tableWidget || !tableContent || !placeholder) { 92 console.log( 93 "Table widget client-side setup failed: missing data or elements" 94 ); 95 return; 96 } 97 98 try { 99 // Hide the placeholder 100 placeholder.style.display = "none"; 101 102 // Create widget container 103 const widgetContainer = document.createElement("div"); 104 widgetContainer.className = "table-widget-container"; 105 106 // Basic security check on client side (server-side is more thorough) 107 if (this.isWidgetCodeSafe(this.tableWidget)) { 108 widgetContainer.innerHTML = this.tableWidget; 109 110 // Add basic styling 111 const style = document.createElement("style"); 112 style.textContent = ` 113 .table-widget-container { 114 width: 100%; 115 min-height: 400px; 116 } 117 .table-widget-container iframe { 118 width: 100%; 119 min-height: 400px; 120 border: none; 121 } 122 .table-widget-container > div { 123 width: 100%; 124 min-height: 400px; 125 } 126 `; 127 document.head.appendChild(style); 128 129 // Add widget to content 130 tableContent.appendChild(widgetContainer); 131 132 console.log("Table widget loaded successfully via client-side"); 133 } else { 134 // Show error for unsafe content 135 widgetContainer.className = "widget-error"; 136 widgetContainer.innerHTML = ` 137 <p>Table booking widget could not be loaded due to security restrictions.</p> 138 <small>Please ensure the widget code is from a trusted provider.</small> 139 `; 140 tableContent.appendChild(widgetContainer); 141 console.error("Table widget failed security check"); 142 } 143 } catch (error) { 144 console.error("Error loading table widget:", error); 145 146 // Show error message 147 const errorContainer = document.createElement("div"); 148 errorContainer.className = "widget-error"; 149 errorContainer.innerHTML = ` 150 <p>Error loading table booking widget.</p> 151 <small>Please check the widget code format.</small> 152 `; 153 tableContent.appendChild(errorContainer); 154 } 155 }, 156 157 isWidgetCodeSafe: function (code) { 158 if (!code) return false;
159 160 // Basic client-side security checks 161 const dangerousPatterns = [ 162 /javascript\s*:/i, 163 /on\w+\s*=/i, // onclick, onload, etc. 164 /<\s*\/?\s*(object|embed|applet)\s*>/i, 165 /expression\s*\(/i, 166 /vbscript\s*:/i, 167 ]; 168 169 for (let pattern of dangerousPatterns) { 170 if (pattern.test(code)) { 171 return false; 172 } 173 } 174 175 // Check for trusted domains (basic check) 176 const trustedDomains = [ 177 "designmynight.com", 178 "onsass.designmynight.com", 179 "widgets.designmynight.com", 180 "opentable.com", 181 "resy.com", 182 "bookatable.com", 183 "guestline.app", 184 ]; 185 186 // If it contains external references, ensure they're from trusted domains 187 const urlMatches = code.match(/(src|href)=["']([^"']+)["']/g); 188 if (urlMatches) { 189 for (let match of urlMatches) { 190 const url = match.match(/(src|href)=["']([^"']+)["']/)[2]; 191 try { 192 const urlObj = new URL(url, window.location.origin); 193 const hostname = urlObj.hostname; 194 195 const isTrusted = trustedDomains.some( 196 (domain) => hostname === domain || hostname.endsWith("." + domain) 197 ); 198 199 if (!isTrusted) { 200 console.warn( 201 "Table widget references untrusted domain:", 202 hostname 203 ); 204 // Allow but warn - server-side validation is more strict 205 } 206 } catch (e) { 207 // Relative URLs or invalid URLs are generally safer 208 continue; 209 } 210 } 211 } 212 213 return true; 214 }, 215 216 setupEventListeners: function () { 217 // Close button 218 const closeBtn = this.panel.querySelector(".booking-panel-close"); 219 if (closeBtn) { 220 closeBtn.addEventListener("click", () => this.closePanel()); 221 } 222 223 // Overlay click to close 224 if (this.overlay) { 225 this.overlay.addEventListener("click", () => this.closePanel()); 226 } 227 228 // Escape key to close 229 document.addEventListener("keydown", (e) => { 230 if (e.key === "Escape" && this.panel.classList.contains("active")) { 231 this.closePanel(); 232 } 233 }); 234 235 // Tab switching 236 const tabs = this.panel.querySelectorAll(".booking-tab");
237 tabs.forEach((tab) => { 238 tab.addEventListener("click", () => { 239 const tabType = tab.getAttribute("data-tab"); 240 this.switchTab(tabType); 241 }); 242 }); 243 244 // Room booking form 245 const roomForm = this.panel.querySelector("#room-booking-form"); 246 if (roomForm) { 247 roomForm.addEventListener("submit", (e) => this.handleRoomBooking(e)); 248 } 249 }, 250 251 openPanel: function (type) { 252 if (!this.panel) return; 253 254 // Show the panel 255 this.panel.classList.add("active"); 256 this.panel.setAttribute("aria-hidden", "false"); 257 258 // Switch to the requested tab 259 if (type) { 260 this.switchTab(type); 261 } 262 263 // Prevent body scroll 264 document.body.style.overflow = "hidden"; 265 }, 266 267 closePanel: function () { 268 if (!this.panel) return; 269 270 this.panel.classList.remove("active"); 271 this.panel.setAttribute("aria-hidden", "true"); 272 273 // Restore body scroll 274 document.body.style.overflow = ""; 275 }, 276 277 switchTab: function (tabType) { 278 if (!this.panel) return; 279 280 // Update tabs 281 const tabs = this.panel.querySelectorAll(".booking-tab");
282 tabs.forEach((tab) => { 283 if (tab.getAttribute("data-tab") === tabType) { 284 tab.classList.add("active"); 285 tab.setAttribute("aria-selected", "true"); 286 } else { 287 tab.classList.remove("active"); 288 tab.setAttribute("aria-selected", "false"); 289 } 290 }); 291 292 // Update content 293 const contents = this.panel.querySelectorAll(".booking-content"); 294 contents.forEach((content) => { 295 if (content.id === `booking-content-${tabType}`) { 296 content.classList.add("active"); 297 } else { 298 content.classList.remove("active"); 299 } 300 }); 301 302 this.currentTab = tabType; 303 }, 304 305 handleRoomBooking: function (e) { 306 e.preventDefault(); 307 308 const form = e.target; 309 const formData = new FormData(form); 310 311 // Get form values 312 const arrival = formData.get("arrival"); 313 const departure = formData.get("departure"); 314 const adults = formData.get("adults"); 315 const children = formData.get("children"); 316 const promo = formData.get("promo"); 317 318 // Simple validation 319 if (!arrival || !departure) { 320 alert("Please select arrival and departure dates"); 321 return; 322 } 323 324 if (new Date(departure) <= new Date(arrival)) { 325 alert("Departure date must be after arrival date"); 326 return; 327 } 328 329 // Build the booking URL using the dynamic URL and hotel code 330 const bookingUrl = this.buildBookingUrl( 331 arrival, 332 departure, 333 adults, 334 children, 335 promo 336 ); 337 338 // Open in new tab 339 window.open(bookingUrl, "_blank"); 340 341 // Close the panel 342 this.closePanel(); 343 }, 344 345 buildBookingUrl: function (arrival, departure, adults, children, promo) { 346 // Use the dynamic booking URL and hotel code from settings 347 const baseUrl = this.bookingUrl; 348 const params = new URLSearchParams({ 349 hotel: this.hotelCode, // Now uses dynamic hotel code 350 arrival: arrival, 351 departure: departure, 352 adults: adults, 353 children: children, 354 }); 355 356 // Add promo code if provided 357 if (promo && promo.trim()) { 358 params.append("codes", promo.trim()); 359 } 360 361 return `${baseUrl}?${params.toString()}`; 362 }, 363 }; 364 365 // Initialize when DOM is ready 366 document.addEventListener("DOMContentLoaded", function () { 367 RedcatBooking.init(); 368 369 // Set up global event listeners for booking buttons 370 document.addEventListener("click", function (e) { 371 const button = e.target.closest(".booking-button"); 372 // Only handle table and room buttons here (party is handled separately with query params) 373 if (button && button.id !== "book-party-btn") { 374 const bookingType = button.getAttribute("data-booking-type"); 375 if (bookingType && RedcatBooking.panel) { 376 RedcatBooking.openPanel(bookingType); 377 } 378 } 379 }); 380 381 // Handle direct links to party booking section 382 if (window.location.hash === "#party-booking-content") { 383 // Open party tab 384 RedcatBooking.openPanel("party"); 385 } 386 }); 387 388 // Make it globally available 389 window.RedcatBooking = RedcatBooking; 390 391// Initialize Airship form height adjustment 392function adjustAirshipIframes() { 393 const airshipIframes = document.querySelectorAll( 394 'iframe[src*="airship.co.uk"]' 395 ); 396 airshipIframes.forEach(function (iframe) { 397 if (!iframe.hasAttribute("data-height-set")) { 398 iframe.setAttribute("data-height-set", "true"); 399 iframe.style.width = "100%"; 400 iframe.style.border = "none"; 401 402 // Different heights for different screen sizes 403 let minHeight; 404 if (window.innerWidth <= 480) { 405 // Small mobile: extra height for full form visibility 406 minHeight = "1300px"; 407 } else if (window.innerWidth <= 768) { 408 // Large mobile/tablet portrait: good height for forms 409 minHeight = "1800px"; 410 } else if (window.innerWidth <= 1024) { 411 // Tablet landscape/small desktop: moderate height 412 minHeight = "900px"; 413 } else { 414 // Large desktop: minimal height to avoid whitespace 415 minHeight = "850px"; 416 } 417 iframe.style.minHeight = minHeight; 418
419 try { 420 const contentHeight = iframe.contentWindow.document.body.scrollHeight; 421 if (contentHeight && contentHeight > 100) { 422 iframe.style.height = contentHeight + "px"; 423 } else { 424 // Responsive fallback instead of fixed 100vh 425 iframe.style.height = minHeight; 426 } 427 } catch (e) { 428 // Cross-origin fallback - responsive height 429 iframe.style.height = minHeight; 430 } 431 } 432 }); 433} 434 435// Add resize listener for responsiveness 436window.addEventListener('resize', function() { 437 // Remove height-set attribute to allow re-adjustment 438 const airshipIframes = document.querySelectorAll( 439 'iframe[src*="airship.co.uk"]' 440 ); 441 airshipIframes.forEach(function (iframe) { 442 iframe.removeAttribute("data-height-set"); 443 }); 444 adjustAirshipIframes(); 445}); 446 447// Run adjustments with delays to catch dynamically loaded iframes 448setTimeout(adjustAirshipIframes, 1000); 449setTimeout(adjustAirshipIframes, 3000); 450setTimeout(adjustAirshipIframes, 5000); 451// End Airship form height adjustment 452})();
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.