1/** 2 * ÐÑÑÑаиваемÑй Ð²Ð¸Ð´Ð¶ÐµÑ Ð¿Ð¾Ð¸Ñка номеÑов (пÑблиÑнÑй API /api/v1/widget/*). 3 * ÐÑполÑзование: 4 * Ðдин блок: BookingWidget.init({ mount: '#id', apiBaseUrl, organizationId, token, legalBaseUrl }); 5 * ФоÑма и ÑезÑлÑÑаÑÑ Ð² ÑазнÑÑ Ð¼ÐµÑÑÐ°Ñ ÑÑÑаниÑÑ: Ñе же Ð¿Ð¾Ð»Ñ + formMount и resultsMount 6 * (оба конÑейнеÑа Ð´Ð¾Ð»Ð¶Ð½Ñ Ð±ÑÑÑ Ð² DOM до вÑзова init; ÑкÑÐ¸Ð¿Ñ Ð¿Ð¾Ð´ÐºÐ»ÑÑаеÑÑÑ Ð¾Ð´Ð¸Ð½ Ñаз). 7 * ÐоÑÑÑие пÑедложениÑ: BookingWidget.init({ mount, mode: 'hot-offers', layout: 'horizontal'|'vertical'|'tiles', ... }); 8 */ 9(function (global) { 10 "use strict"; 11 12 function qs(sel, root) { 13 return (root || document).querySelector(sel); 14 } 15 16 function el(tag, attrs, children) { 17 var n = document.createElement(tag); 18 if (attrs) { 19 Object.keys(attrs).forEach(function (k) { 20 if (k === "class") n.className = attrs[k]; 21 else if (k === "html") n.innerHTML = attrs[k]; 22 else if (k.slice(0, 2) === "on" && typeof attrs[k] === "function") n.addEventListener(k.slice(2), attrs[k]); 23 else n.setAttribute(k, attrs[k]); 24 }); 25 } 26 (children || []).forEach(function (c) { 27 if (c) n.appendChild(typeof c === "string" ? document.createTextNode(c) : c); 28 }); 29 return n; 30 } 31 32 function legalHref(path, extra) { 33 var b = String((extra && extra.legalBaseUrl) || "https://bronefix.ru").replace(/\/$/, ""); 34 return b + path; 35 } 36 37 function appendPdConsentAndSubmit(modal, opts) { 38 var termsId = opts.termsId || "bw-guest-terms"; 39 var consentId = opts.consentId; 40 var termsCb = el("input", { type: "checkbox", id: termsId }); 41 var consentCb = el("input", { type: "checkbox", id: consentId }); 42 var submitBtn = el( 43 "button", 44 { 45 type: "button", 46 id: opts.submitId, 47 class: "primary", 48 disabled: "disabled", 49 style: opts.style || "margin-top:8px", 50 onclick: opts.onSubmit, 51 }, 52 ["ÐÑпÑавиÑÑ Ð·Ð°ÑвкÑ"], 53 ); 54 function sync() { 55 submitBtn.disabled = !(termsCb.checked && consentCb.checked); 56 } 57 termsCb.addEventListener("change", sync); 58 consentCb.addEventListener("change", sync); 59 var termsA = el("a", { href: legalHref("/legal/guest-terms", opts), target: "_blank", rel: "noopener" }, [ 60 "полÑзоваÑелÑÑким ÑоглаÑением модÑлÑ", 61 ]); 62 var pdA = el("a", { href: legalHref("/legal/guest-consent-template", opts), target: "_blank", rel: "noopener" }, [ 63 "ÑоглаÑием на обÑабоÑÐºÑ Ð¿ÐµÑÑоналÑнÑÑ Ð´Ð°Ð½Ð½ÑÑ ", 64 ]); 65 modal.appendChild(el("label", { class: "bw-consent" }, [termsCb, " ÐÑÐ¸Ð½Ð¸Ð¼Ð°Ñ ", termsA])); 66 modal.appendChild(el("label", { class: "bw-consent" }, [consentCb, " ÐÐ°Ñ ", pdA])); 67 modal.appendChild(submitBtn); 68 return { consentCb: consentCb, termsCb: termsCb, submitBtn: submitBtn }; 69 } 70 71 function syncSubmitWithPdConsent(submitEl, consentEl, termsEl) { 72 if (!submitEl) return; 73 submitEl.disabled = !(consentEl && consentEl.checked && termsEl && termsEl.checked); 74 } 75 76 function init(opts) { 77 if (String((opts && opts.mode) || "").toLowerCase() === "hot-offers") { 78 initHotOffers(opts || {}); 79 return; 80 } 81 var apiBase = (opts.apiBaseUrl || "").replace(/\/$/, ""); 82 var orgId = opts.organizationId || ""; 83 var token = opts.token || ""; 84 if (opts.metrikaCounterId) { 85 var mc = String(opts.metrikaCounterId).replace(/\D/g, ""); 86 if (mc.length >= 5) { 87 window.__bronefixMetrikaId = window.__bronefixMetrikaId || mc; 88 } 89 } 90 91 function resolveMount(m) { 92 return typeof m === "string" ? qs(m) : m; 93 } 94 95 var formRoot = null; 96 var resultsRoot = null; 97 var useSplitShell = false; 98 99 if (opts.formMount && opts.resultsMount) { 100 formRoot = resolveMount(opts.formMount); 101 resultsRoot = resolveMount(opts.resultsMount); 102 if (!formRoot || !resultsRoot) { 103 var hint = formRoot || resultsRoot; 104 if (hint) { 105 hint.innerHTML = ""; 106 hint.appendChild( 107 el("p", { class: "booking-widget-error" }, ["Ðе найден formMount или resultsMount"]), 108 ); 109 } 110 return; 111 } 112 useSplitShell = formRoot !== resultsRoot; 113 } else { 114 var mount = opts.mount; 115 var single = resolveMount(mount); 116 if (!single) return; 117 formRoot = single; 118 resultsRoot = single; 119 } 120
121 if (!apiBase || !orgId || !token) { 122 formRoot.appendChild(el("p", { class: "booking-widget-error" }, ["ÐÑÐ¶Ð½Ñ apiBaseUrl, organizationId и token"])); 123 return; 124 } 125 126 var landing = (function () { 127 var p = new URLSearchParams(typeof window !== "undefined" ? window.location.search : ""); 128 return { 129 offer: p.get("offer") || "", 130 room: p.get("room") || "", 131 in: p.get("in") || "", 132 out: p.get("out") || "", 133 adults: p.get("adults") || "", 134 rate: p.get("rate") || "", 135 utm_source: p.get("utm_source") || "", 136 utm_medium: p.get("utm_medium") || "", 137 utm_campaign: p.get("utm_campaign") || "", 138 utm_content: p.get("utm_content") || "", 139 utm_term: p.get("utm_term") || "", 140 yclid: p.get("yclid") || "", 141 }; 142 })(); 143 144 function pushMetrikaGoal(name) { 145 try { 146 if (typeof window.ym === "function" && window.__bronefixMetrikaId) { 147 window.ym(window.__bronefixMetrikaId, "reachGoal", name); 148 } 149 } catch (e) {} 150 } 151 152 function pushEcommerce(action, product) { 153 try { 154 window.dataLayer = window.dataLayer || []; 155 var payload = { ecommerce: { currencyCode: "RUB" } }; 156 payload.ecommerce[action] = { products: [product] }; 157 window.dataLayer.push(payload); 158 } catch (e) {} 159 } 160 161 function trackBookingPaid(offerId, name, priceRub) { 162 pushMetrikaGoal("booking_paid"); 163 if (offerId) { 164 pushEcommerce("purchase", { 165 id: offerId, 166 name: name || "booking", 167 price: priceRub || 0, 168 quantity: 1, 169 }); 170 } 171 } 172 window.__bronefixTrackBookingPaid = trackBookingPaid; 173 174 var state = { 175 config: null, 176 categories: [], 177 transitionOptions: [], 178 nearbyDateWindows: [], 179 selected: null, 180 selectedStay: null, 181 lastSearch: null, 182 captchaToken: null, 183 nearbyLoading: false, 184 contactPhone: "", 185 maxUrl: "", 186 landing: landing, 187 resolvedOffer: null, 188 }; 189 190 formRoot.innerHTML = ""; 191 formRoot.className = (formRoot.className ? formRoot.className + " " : "") + "booking-widget-root"; 192 if (useSplitShell) { 193 resultsRoot.innerHTML = ""; 194 resultsRoot.className = (resultsRoot.className ? resultsRoot.className + " " : "") + "booking-widget-root"; 195 } 196 197 var style = el("style", {}, []); 198 style.textContent = 199 ".booking-widget-root{font-family:system-ui,-apple-system,sans-serif;font-size:14px;line-height:1.45;color:#0f172a;width:100%;max-width:none;box-sizing:border-box}" + 200 ".booking-widget-root .bw-panel{border:1px solid #e2e8f0;border-radius:12px;padding:12px 12px 10px;background:#fff}" + 201 ".booking-widget-root .bw-panel+.bw-panel{margin-top:12px}" + 202 ".booking-widget-root .bw-results-panel.bw-results-panel--hidden{display:none}" + 203 ".booking-widget-root .bw-panel-title{margin:0 0 10px;font-size:15px;font-weight:650;color:#0f172a}" + 204 ".booking-widget-root .bw-panel-note{margin:0 0 10px}" + 205 ".booking-widget-root input,.booking-widget-root select,.booking-widget-root textarea,.booking-widget-root button{border-radius:8px;border:1px solid #cbd5e1;padding:8px 10px}" + 206 ".booking-widget-root button.primary{background:#0284c7;color:#fff;border-color:#0284c7;cursor:pointer}" + 207 ".booking-widget-root button.primary:disabled{opacity:.55;cursor:not-allowed}" + 208 ".booking-widget-root .bw-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:12px;align-items:stretch}" + 209 ".booking-widget-root .bw-cta{flex:1 1 120px;background:#0284c7;color:#fff;border:1px solid #0284c7;border-radius:8px;padding:8px 12px;cursor:pointer;font-weight:600;font-size:14px;text-align:center;text-decoration:none;display:inline-flex;align-items:center;justify-content:center}" + 210 ".booking-widget-root .bw-cta:hover{filter:brightness(1.05)}" + 211 ".booking-widget-root .bw-cta--secondary{background:#fff;color:#0f172a;border-color:#cbd5e1;font-weight:600}" + 212 ".booking-widget-root .bw-cta--secondary:hover{background:#f8fafc;filter:none}" + 213 ".booking-widget-root .bw-cta--max{background:#1a1a2e;color:#fff;border-color:#1a1a2e}" + 214 ".booking-widget-root .bw-cta--max:hover{filter:brightness(1.15)}" + 215 ".booking-widget-root .bw-consent{display:flex;align-items:flex-start;gap:8px;margin:12px 0 4px;font-size:13px;color:#334155;cursor:pointer;line-height:1.35}" + 216 ".booking-widget-root .bw-consent input{margin:2px 0 0;flex-shrink:0;width:16px;height:16px;accent-color:#0284c7}" + 217 ".booking-widget-root .row{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;margin-bottom:10px}" +
218 ".booking-widget-root .field{display:flex;flex-direction:column;gap:4px}" + 219 ".booking-widget-root .bw-field-err{color:#b91c1c;font-size:12px;line-height:1.25}" + 220 ".booking-widget-root .bw-invalid{border-color:#ef4444 !important;box-shadow:0 0 0 2px rgba(239,68,68,.12)}" + 221 ".booking-widget-root .bw-captcha-row{display:flex;align-items:center;gap:10px;flex-wrap:wrap}" + 222 ".booking-widget-root .bw-captcha-q{font-size:13px;color:#334155}" + 223 ".booking-widget-root .muted{color:#64748b;font-size:12px}" + 224 ".booking-widget-root .bw-cards{display:flex;flex-direction:column;gap:12px;margin:12px 0}" + 225 ".booking-widget-root .bw-card{border:1px solid #e2e8f0;border-radius:12px;padding:12px;background:#fff;box-shadow:0 1px 2px rgba(15,23,42,.06)}" + 226 ".booking-widget-root .bw-card--sel{border-color:#0284c7;background:#f0f9ff;box-shadow:0 2px 8px rgba(2,132,199,.12)}" + 227 ".booking-widget-root .bw-card-head{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-start}" + 228 ".booking-widget-root .bw-gal{flex:0 1 420px;min-width:min(100%,220px);max-width:100%}" + 229 ".booking-widget-root .bw-gal-main{position:relative;border-radius:10px;overflow:hidden;background:#f1f5f9;aspect-ratio:4/3;max-height:280px}" + 230 ".booking-widget-root .bw-gal-main img,.booking-widget-root .bw-gal-main video{width:100%;height:100%;object-fit:cover;display:block;cursor:pointer}" + 231 ".booking-widget-root .bw-gal-main .bw-vplay{position:absolute;inset:0;z-index:2;display:flex;align-items:center;justify-content:center;background:rgba(15,23,42,.35);cursor:pointer}" + 232 ".booking-widget-root .bw-gal-main .bw-vplay span{width:52px;height:52px;border-radius:50%;background:rgba(255,255,255,.95);display:flex;align-items:center;justify-content:center;font-size:22px;padding-left:4px}" + 233 ".booking-widget-root .bw-thumbs{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}" + 234 ".booking-widget-root .bw-thumb{width:56px;height:56px;border-radius:8px;overflow:hidden;border:2px solid transparent;cursor:pointer;background:#e2e8f0;flex-shrink:0}" + 235 ".booking-widget-root .bw-thumb--on{border-color:#0284c7}" + 236 ".booking-widget-root .bw-thumb img{width:100%;height:100%;object-fit:cover;display:block}" + 237 ".booking-widget-root .bw-thumb--vid{position:relative;display:flex;align-items:center;justify-content:center;font-size:20px;background:#1e293b;color:#fff}" + 238 ".booking-widget-root .bw-card-body{flex:1 1 280px;min-width:min(100%,220px)}" + 239 ".booking-widget-root .bw-title{font-size:16px;font-weight:650;margin:0 0 6px}" + 240 ".booking-widget-root .bw-price{font-size:15px;font-weight:600;color:#0f172a;margin:4px 0}" + 241 ".booking-widget-root .bw-price-sub{font-size:12px;font-weight:400;color:#64748b}" + 242 ".booking-widget-root .bw-tr-badge{display:inline-block;font-size:11px;font-weight:600;color:#0369a1;background:#e0f2fe;border-radius:999px;padding:2px 8px;margin-bottom:6px}" + 243 ".booking-widget-root .bw-tr-seg{font-size:13px;color:#334155;margin:4px 0;padding:6px 8px;background:#f8fafc;border-radius:8px;border:1px solid #e2e8f0}" + 244 ".booking-widget-root .bw-tr-seg strong{color:#0f172a;font-weight:600}" + 245 ".booking-widget-root .bw-facts{display:flex;flex-wrap:wrap;gap:8px 14px;margin:8px 0;font-size:12px;color:#475569}" + 246 ".booking-widget-root .bw-amenity-groups{margin:4px 0 2px;min-width:0}" + 247 ".booking-widget-root .bw-amenity-list{list-style:none;margin:0;padding:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:8px;row-gap:2px;align-items:start}" + 248 "@media (min-width:900px){.booking-widget-root .bw-amenity-list{grid-template-columns:repeat(3,minmax(0,1fr))}}" + 249 "@media (max-width:360px){.booking-widget-root .bw-amenity-list{grid-template-columns:1fr}}" + 250 ".booking-widget-root .bw-amenity-item{display:block;margin:0;padding:0;min-width:0}" + 251 ".booking-widget-root .bw-amenity-row{display:flex;align-items:flex-start;gap:4px;min-width:0;font-size:10px;line-height:1.25;color:#64748b}" + 252 ".booking-widget-root .bw-amenity-row>.amenity-icon{color:#0284c7;font-size:1.05em;line-height:inherit;flex-shrink:0;margin-top:.08em}" + 253 ".booking-widget-root .bw-amenity-cap{flex:1;min-width:0;word-break:break-word;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;line-clamp:2}" + 254 ".booking-widget-root .bw-tags{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0}" + 255 ".booking-widget-root .bw-tag{font-size:11px;background:#f1f5f9;color:#334155;padding:3px 8px;border-radius:999px}" + 256 ".booking-widget-root .bw-desc{font-size:13px;color:#475569;margin:6px 0 10px;max-height:4.2em;overflow:hidden}" + 257 ".booking-widget-root .bw-actions{display:flex;flex-wrap:wrap;gap:8px;align-items:center}" + 258 ".booking-widget-root .bw-search-msg{margin:10px 0;padding:10px 12px;border-radius:10px;border:1px solid #e2e8f0;background:#f8fafc;color:#334155;font-size:13px;line-height:1.35}" + 259 ".booking-widget-root .bw-search-msg--error{border-color:#fecaca;background:#fef2f2;color:#991b1b}" + 260 ".booking-widget-root .bw-nearby-actions{margin:8px 0 10px;display:flex;flex-direction:column;gap:8px}" + 261 ".booking-widget-root .bw-nearby-hint{margin:0;font-size:13px;color:#334155;line-height:1.35}" +
262 ".booking-widget-root .bw-nearby-chips{display:flex;flex-wrap:wrap;gap:8px;align-items:center}" + 263 ".booking-widget-root .bw-nearby-chip{background:#fff;color:#0369a1;border:1px solid #7dd3fc;cursor:pointer;font-size:13px;padding:7px 10px;border-radius:999px}" + 264 ".booking-widget-root .bw-nearby-chip:hover{background:#f0f9ff;border-color:#38bdf8}" + 265 ".booking-widget-root .bw-nearby-chip:disabled{opacity:.6;cursor:not-allowed}" + 266 ".booking-widget-root .bw-nearby-chip--active{background:#e0f2fe;border-color:#0284c7;color:#075985;font-weight:600}" + 267 ".booking-widget-root .bw-nearby-meta{font-size:12px;color:#64748b}" + 268 ".booking-widget-root .bw-nearby-window{margin-top:16px;padding:14px 14px 10px;border:1px solid #bae6fd;border-radius:12px;background:linear-gradient(180deg,#f0f9ff 0%,#fff 48%)}" + 269 ".booking-widget-root .bw-nearby-title{margin:0 0 6px;font-size:18px;font-weight:700;line-height:1.3;color:#0369a1}" + 270 ".booking-widget-root .bw-nearby-sub{margin:0 0 10px;font-size:13px;color:#0369a1}" + 271 ".booking-widget-root .bw-req-overlay{position:fixed;inset:0;background:rgba(15,23,42,.55);z-index:99998;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}" + 272 ".booking-widget-root .bw-req-modal{width:min(680px,96vw);max-height:88vh;overflow:auto;background:#fff;border-radius:12px;padding:14px;box-shadow:0 14px 40px rgba(2,6,23,.35);position:relative}" + 273 ".booking-widget-root .bw-req-close{position:absolute;top:8px;right:8px;width:32px;height:32px;border-radius:999px;border:1px solid #cbd5e1;background:#fff;cursor:pointer;font-size:20px;line-height:1}" + 274 ".bw-lightbox{position:fixed;inset:0;background:rgba(15,23,42,.72);z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}" + 275 ".bw-lightbox-inner{position:relative;max-width:min(960px,100%);max-height:90vh;background:#0f172a;border-radius:12px;overflow:hidden}" + 276 ".bw-lightbox-inner img,.bw-lightbox-inner video{max-width:100%;max-height:min(80vh,720px);display:block;margin:0 auto}" + 277 ".bw-lightbox-inner iframe{width:min(960px,96vw);height:min(540px,56.25vw);border:0;display:block;background:#000}" + 278 ".bw-lb-close{position:absolute;top:8px;right:8px;z-index:2;width:36px;height:36px;border-radius:999px;border:0;background:rgba(255,255,255,.92);cursor:pointer;font-size:20px;line-height:1}" + 279 ".bw-lb-nav{position:absolute;top:50%;transform:translateY(-50%);width:40px;height:40px;border-radius:999px;border:0;background:rgba(255,255,255,.92);cursor:pointer;font-size:22px;z-index:2}" + 280 ".bw-lb-prev{left:8px}.bw-lb-next{right:8px}"; 281 formRoot.appendChild(style); 282 if (useSplitShell) { 283 var styleResults = el("style", {}, []); 284 styleResults.textContent = style.textContent; 285 resultsRoot.appendChild(styleResults); 286 } 287 288 function widgetPublicBase() { 289 try { 290 var scripts = document.getElementsByTagName("script"); 291 for (var i = 0; i < scripts.length; i++) { 292 var src = scripts[i].src || ""; 293 var idx = src.indexOf("widget-booking.js"); 294 if (idx !== -1) return src.slice(0, idx).replace(/\/$/, ""); 295 } 296 } catch (e) {} 297 return ""; 298 } 299 300 function ensureAmenityIcons() { 301 try { 302 if (document.getElementById("bronefix-amenity-icons")) return; 303 var base = widgetPublicBase(); 304 if (!base) return; 305 var link = document.createElement("link"); 306 link.id = "bronefix-amenity-icons"; 307 link.rel = "stylesheet"; 308 link.href = base + "/fonts/amenities/amenities.css"; 309 document.head.appendChild(link); 310 } catch (e) {} 311 } 312 313 function amenityIconClass(slug) { 314 var s = String(slug || "wifi"); 315 if (!/^[a-z0-9_]+$/.test(s)) s = "wifi"; 316 return "amenity-icon amenity-icon--" + s; 317 } 318 319 var errBox = el("div", { class: "muted" }); 320 formRoot.appendChild(errBox); 321 322 function setErr(t) { 323 errBox.textContent = t || ""; 324 } 325 326 function searchErrorRu(msg) { 327 var s = String(msg || "").trim(); 328 if (!s) return "ÐÑибка поиÑка. ÐопÑобÑйÑе измениÑÑ Ð´Ð°ÑÑ Ð¸Ð»Ð¸ ÑоÑÑав гоÑÑей."; 329 var m = s.match(/^Minimum stay is (\d+) night\(s\)\.?$/i); 330 if (m) return "ÐинималÑнÑй ÑÑок пÑÐ¾Ð¶Ð¸Ð²Ð°Ð½Ð¸Ñ â " + m[1] + " ноÑи."; 331 if (/check_in must be before check_out/i.test(s)) return "ÐаÑа вÑезда должна бÑÑÑ Ð¿Ð¾Ð·Ð¶Ðµ даÑÑ Ð·Ð°ÐµÐ·Ð´Ð°."; 332 if (/check_in must be today or later/i.test(s)) return "ÐаÑа заезда не Ð¼Ð¾Ð¶ÐµÑ Ð±ÑÑÑ ÑанÑÑе ÑегоднÑÑнего днÑ."; 333 if (/Too many requests/i.test(s)) return "СлиÑком много запÑоÑов. ÐопÑобÑйÑе ÑÑÑÑ Ð¿Ð¾Ð·Ð¶Ðµ."; 334 if (/No room available in this category/i.test(s)) return "РвÑбÑанной каÑегоÑии ÑейÑÐ°Ñ Ð½ÐµÑ ÑвободнÑÑ Ð½Ð¾Ð¼ÐµÑов."; 335 if (/Room no longer available/i.test(s)) return "ÐÑбÑаннÑй Ð½Ð¾Ð¼ÐµÑ Ñже занÑли. ÐбновиÑе поиÑк."; 336 return s; 337 } 338 339 function setResultsPanelVisible(visible) { 340 if (visible) resultsSection.classList.remove("bw-results-panel--hidden"); 341 else resultsSection.classList.add("bw-results-panel--hidden"); 342 } 343 344 function showSearchMessage(text, isError) { 345 setResultsPanelVisible(true); 346 listHost.innerHTML = ""; 347 nearbyHost.innerHTML = ""; 348 state.nearbyDateWindows = []; 349 renderNearbyAction(); 350 listHost.appendChild( 351 el( 352 "div", 353 { class: "bw-search-msg" + (isError ? " bw-search-msg--error" : "") }, 354 [text], 355 ), 356 ); 357 } 358 359 function renderOfferNearbyAlternatives(windows) { 360 if (!windows || !windows.length) return; 361 var wrap = el("div", { class: "bw-nearby-window" }, []); 362 wrap.appendChild( 363 el("p", { class: "muted bw-nearby-sub" }, [ 364 "ÐлижайÑие ÑвободнÑе даÑÑ Ð´Ð»Ñ ÑÑой каÑегоÑии:", 365 ]), 366 ); 367 var row = el("div", { class: "bw-actions" }, []); 368 windows.slice(0, 8).forEach(function (win) { 369 var btn = el("button", { type: "button", class: "bw-cta bw-cta--secondary" }, [ 370 formatIsoRu(win.check_in) + " â " + formatIsoRu(win.check_out), 371 ]); 372 btn.onclick = function () { 373 var L = state.landing; 374 if (L) { 375 L.in = win.check_in;
376 L.out = win.check_out; 377 } 378 applyLandingToForm(); 379 runSearch(); 380 }; 381 row.appendChild(btn); 382 }); 383 wrap.appendChild(row); 384 nearbyHost.appendChild(wrap); 385 } 386 387 function localTodayIso() { 388 var d = new Date(); 389 var y = d.getFullYear(); 390 var m = String(d.getMonth() + 1).padStart(2, "0"); 391 var dd = String(d.getDate()).padStart(2, "0"); 392 return y + "-" + m + "-" + dd; 393 } 394 395 var formSearch = el("div", {}, []); 396 formSearch.appendChild(el("div", { class: "row" }, [ 397 el("div", { class: "field" }, [ 398 el("label", {}, ["Ðаезд"]), 399 (function () { 400 var i = el("input", { type: "date" }); 401 i.id = "bw-ci"; 402 i.min = localTodayIso(); 403 return i; 404 })(), 405 ]), 406 el("div", { class: "field" }, [ 407 el("label", {}, ["ÐÑезд"]), 408 (function () { 409 var i = el("input", { type: "date" }); 410 i.id = "bw-co"; 411 i.min = localTodayIso(); 412 return i; 413 })(), 414 ]), 415 el("div", { class: "field" }, [ 416 el("label", {}, ["ÐзÑоÑлÑÑ "]), 417 (function () { 418 var s = el("select", { id: "bw-ad" }); 419 for (var a = 1; a <= 10; a++) s.appendChild(el("option", { value: String(a) }, [String(a)])); 420 return s; 421 })(), 422 ]), 423 el("div", { class: "field" }, [ 424 el("label", {}, ["ÐеÑей"]), 425 (function () { 426 var s = el("select", { id: "bw-ch" }); 427 for (var c = 0; c <= 10; c++) s.appendChild(el("option", { value: String(c) }, [String(c)])); 428 return s; 429 })(), 430 ]), 431 el( 432 "button", 433 { 434 type: "button", 435 class: "primary", 436 onclick: function () { 437 void runSearch(); 438 }, 439 }, 440 ["ÐайÑи"], 441 ), 442 ])); 443 var searchSection = el("section", { class: "bw-panel bw-search-panel", "aria-label": "ÐоиÑк номеÑа" }, []); 444 searchSection.appendChild(el("h2", { class: "bw-panel-title" }, ["ÐоиÑк номеÑа"])); 445 searchSection.appendChild(formSearch); 446 formRoot.appendChild(searchSection); 447 448 function addDaysIso(iso, days) { 449 try { 450 var d = new Date(iso + "T00:00:00"); 451 d.setDate(d.getDate() + days); 452 var y = d.getFullYear(); 453 var m = String(d.getMonth() + 1).padStart(2, "0"); 454 var dd = String(d.getDate()).padStart(2, "0"); 455 return y + "-" + m + "-" + dd; 456 } catch (e) { 457 return iso; 458 } 459 } 460 461 function syncDateInputs(changed) { 462 var ciEl = qs("#bw-ci", formRoot); 463 var coEl = qs("#bw-co", formRoot); 464 if (!ciEl || !coEl) return; 465 var today = localTodayIso(); 466 ciEl.min = today; 467 var ci = ciEl.value || ""; 468 if (ci && ci < today) { 469 ciEl.value = today; 470 ci = today; 471 } 472 var co = coEl.value || ""; 473 if (ci) coEl.min = ci; 474 else coEl.min = today; 475 if (!ci || !co) return; 476 if (co <= ci) { 477 if (changed === "co") { 478 coEl.value = addDaysIso(ci, 1); 479 } else { 480 coEl.value = addDaysIso(ci, 1); 481 } 482 } 483 } 484 485 (function attachDateSync() { 486 var ciEl = qs("#bw-ci", formRoot); 487 var coEl = qs("#bw-co", formRoot); 488 if (!ciEl || !coEl) return; 489 ["change", "input"].forEach(function (ev) { 490 ciEl.addEventListener(ev, function () { 491 syncDateInputs("ci"); 492 }); 493 coEl.addEventListener(ev, function () { 494 syncDateInputs("co"); 495 }); 496 }); 497 })(); 498 499 var resultsSection = el("section", { class: "bw-panel bw-results-panel", "aria-label": "РезÑлÑÑаÑÑ Ð¿Ð¾Ð¸Ñка" }, []); 500 resultsSection.appendChild(el("h2", { class: "bw-panel-title" }, ["РезÑлÑÑаÑÑ Ð¿Ð¾Ð¸Ñка"])); 501 var resultsNote = el("p", { class: "muted bw-panel-note" }, ["ÐÑбеÑиÑе ваÑианÑ, ÑÑÐ¾Ð±Ñ Ð¾ÑÑавиÑÑ Ð·Ð°ÑвкÑ."]); 502 resultsSection.appendChild(resultsNote); 503 var nearbyActionHost = el("div", { class: "bw-nearby-actions" }, []); 504 resultsSection.appendChild(nearbyActionHost); 505 var listHost = el("div", {}, []); 506 resultsSection.appendChild(listHost); 507 var nearbyHost = el("div", {}, []); 508 resultsSection.appendChild(nearbyHost); 509 resultsSection.classList.add("bw-results-panel--hidden"); 510 resultsRoot.appendChild(resultsSection); 511 512 var reqHost = el("div", { id: "bw-request-anchor" }, []); 513 resultsRoot.appendChild(reqHost); 514 515 function apiUrl(path, q) { 516 var p = path.charAt(0) === "/" ? path : "/" + path; 517 var u = new URL(apiBase + p); 518 Object.keys(q || {}
518).forEach(function (k) { 519 if (q[k] != null && q[k] !== "") u.searchParams.set(k, String(q[k])); 520 }); 521 return u.toString(); 522 } 523 524 /** ÐÑблиÑнÑе ÑÐ°Ð¹Ð»Ñ Ð²Ð¸Ð´Ð¶ÐµÑа ÑÑебÑÑÑ organization_id и token в query; полнÑй http(s)-URL без Ð½Ð¸Ñ Ð´Ð°ÑÑ 422. */ 525 function widgetAssetUrl(path) { 526 if (!path) return ""; 527 if (/^https?:\/\//i.test(path)) { 528 try { 529 var u = new URL(path); 530 if (u.pathname.indexOf("/api/v1/widget/") === 0) { 531 u.searchParams.set("organization_id", orgId); 532 u.searchParams.set("token", token); 533 } 534 return u.toString(); 535 } catch (e) { 536 return path; 537 } 538 } 539 return apiUrl(path, { organization_id: orgId, token: token }); 540 } 541 542 function galleryItemsForRoom(r) { 543 if (r.gallery_items && r.gallery_items.length) return r.gallery_items; 544 var g = []; 545 (r.media_urls || []).forEach(function (u) { 546 g.push({ kind: "image", url: u }); 547 }); 548 (r.video_urls || []).forEach(function (u) { 549 g.push({ kind: "video", url: u }); 550 }); 551 return g; 552 } 553 554 function youtubeEmbedUrl(url) { 555 try { 556 var u = new URL(url); 557 if (u.hostname.indexOf("youtu.be") >= 0) { 558 return "https://www.youtube.com/embed/" + u.pathname.replace(/^\//, "").split("?")[0]; 559 } 560 if (u.hostname.indexOf("youtube.com") >= 0) { 561 var v = u.searchParams.get("v"); 562 if (v) return "https://www.youtube.com/embed/" + v; 563 var em = u.pathname.match(/^\/embed\/([^/?]+)/); 564 if (em) return "https://www.youtube.com/embed/" + em[1]; 565 var sh = u.pathname.match(/^\/shorts\/([^/?]+)/); 566 if (sh) return "https://www.youtube.com/embed/" + sh[1]; 567 } 568 } catch (e) {} 569 return null; 570 } 571 572 function vimeoEmbedUrl(url) { 573 try { 574 var u = new URL(url); 575 if (u.hostname.indexOf("vimeo.com") < 0) return null; 576 var m = u.pathname.match(/\/(\d+)/); 577 if (m) return "https://player.vimeo.com/video/" + m[1]; 578 } catch (e) {} 579 return null; 580 } 581 582 function rutubeEmbedUrl(url) { 583 try { 584 var u = new URL(url); 585 if (u.hostname.indexOf("rutube.ru") < 0) return null; 586 var em = u.pathname.match(/^\/play\/embed\/([a-z0-9_-]+)\/?$/i); 587 if (em) return "https://rutube.ru/play/embed/" + em[1] + "/"; 588 var vid = u.pathname.match(/^\/video\/([a-z0-9_-]+)\/?$/i); 589 if (vid) return "https://rutube.ru/play/embed/" + vid[1] + "/"; 590 } catch (e) {} 591 return null; 592 } 593 594 function vkEmbedUrl(url) { 595 try { 596 var u = new URL(url); 597 var host = u.hostname.toLowerCase(); 598 if (host.indexOf("vk.com") < 0 && host.indexOf("vkvideo.ru") < 0) return null; 599 var m = u.pathname.match(/video(-?\d+)_(\d+)/i); 600 if (!m) return null; 601 return ( 602 "https://vk.com/video_ext.php?oid=" + 603 encodeURIComponent(m[1]) + 604 "&id=" + 605 encodeURIComponent(m[2]) + 606 "&hd=2" 607 ); 608 } catch (e) {} 609 return null; 610 } 611 612 function okEmbedUrl(url) { 613 try { 614 var u = new URL(url); 615 if (u.hostname.indexOf("ok.ru") < 0) return null; 616 var m = u.pathname.match(/\/video\/(\d+)/i); 617 if (m) return "https://ok.ru/videoembed/" + m[1]; 618 } catch (e) {} 619 return null; 620 } 621 622 function embedUrlWithAutoplay(embedUrl) { 623 if (!embedUrl) return embedUrl; 624 return embedUrl + (embedUrl.indexOf("?") >= 0 ? "&" : "?") + "autoplay=1"; 625 } 626 627 function isDirectVideoUrl(url) { 628 return /\.(mp4|webm|ogg|mov)(\?|$)/i.test(url || ""); 629 } 630 631 function videoPlayableUrl(url) { 632 var resolved = widgetAssetUrl(url); 633 return resolved || url; 634 } 635 636 var lightboxEl = null; 637 var requestModalEl = null; 638 function clearFieldValidation() { 639 var bad = resultsRoot.querySelectorAll(".bw-invalid"); 640 for (var i = 0; i < bad.length; i++) bad[i].classList.remove("bw-invalid"); 641 var em = resultsRoot.querySelectorAll(".bw-field-err"); 642 for (var j = 0; j < em.length; j++) em[j].remove(); 643 } 644 function markRequiredField(inputEl) { 645 if (!inputEl) return; 646 var holder = inputEl.closest(".field") || inputEl.parentElement; 647 inputEl.classList.add("bw-invalid"); 648 if (holder && !qs(".bw-field-err", holder)) { 649 holder.appendChild(el("div", { class: "bw-field-err" }, ["ÐбÑзаÑелÑное поле"])); 650 } 651 } 652 function closeLightbox() { 653 if (lightboxEl && lightboxEl.parentNode) lightboxEl.parentNode.removeChild(lightboxEl); 654 lightboxEl = null; 655 document.removeEventListener("keydown", onLbKey); 656 } 657 function closeRequestModal() { 658 if (requestModalEl && requestModalEl.parentNode) requestModalEl.parentNode.removeChild(requestModalEl); 659 requestModalEl = null; 660 } 661 function onLbKey(ev) { 662 if (ev.key === "Escape") closeLightbox(); 663 } 664 665 function openLightbox(room, startIdx) { 666 var items = galleryItemsForRoom(room); 667 if (!items.length) return; 668 var idx = Math.max(0, Math.min(startIdx || 0, items.length - 1)); 669 closeLightbox(); 670 var box = el("div", { class: "bw-lightbox", onclick: function (e) { 671 if (e.target === box) closeLightbox(); 672 }}); 673 var inner = el("div", { class: "bw-lightbox-inner" }); 674 var body = el("div", {}, []);
675 function renderSlide() { 676 body.innerHTML = ""; 677 var it = items[idx]; 678 if (!it) return; 679 if (it.kind === "image") { 680 var iu = widgetAssetUrl(it.url); 681 body.appendChild(el("img", { src: iu, alt: "" })); 682 } else { 683 var u = it.url; 684 var y = youtubeEmbedUrl(u); 685 var vm = vimeoEmbedUrl(u); 686 var rt = rutubeEmbedUrl(u); 687 var vk = vkEmbedUrl(u); 688 var ok = okEmbedUrl(u); 689 var iframeAttrs = { 690 allowfullscreen: "true", 691 title: "video", 692 allow: "autoplay; fullscreen; encrypted-media; picture-in-picture", 693 }; 694 if (y) 695 body.appendChild(el("iframe", Object.assign({ src: embedUrlWithAutoplay(y) }, iframeAttrs))); 696 else if (vm) 697 body.appendChild(el("iframe", Object.assign({ src: embedUrlWithAutoplay(vm) }, iframeAttrs))); 698 else if (rt) 699 body.appendChild(el("iframe", Object.assign({ src: embedUrlWithAutoplay(rt) }, iframeAttrs))); 700 else if (vk) 701 body.appendChild(el("iframe", Object.assign({ src: embedUrlWithAutoplay(vk) }, iframeAttrs))); 702 else if (ok) 703 body.appendChild(el("iframe", Object.assign({ src: embedUrlWithAutoplay(ok) }, iframeAttrs))); 704 else if (isDirectVideoUrl(u) || isDirectVideoUrl(videoPlayableUrl(u))) 705 body.appendChild( 706 el("video", { 707 src: videoPlayableUrl(u), 708 controls: "true", 709 autoplay: "true", 710 playsinline: "true", 711 }), 712 ); 713 else 714 body.appendChild( 715 el("div", { style: "padding:24px;color:#fff;text-align:center" }, [ 716 el("p", {}, ["Ðе ÑдалоÑÑ Ð²ÑÑÑоиÑÑ Ð²Ð¸Ð´ÐµÐ¾."]), 717 el("a", { href: u, target: "_blank", rel: "noopener", style: "color:#7dd3fc" }, ["ÐÑкÑÑÑÑ ÑÑÑлкÑ"]), 718 ]), 719 ); 720 } 721 } 722 renderSlide(); 723 inner.appendChild(el("button", { type: "button", class: "bw-lb-close", onclick: closeLightbox }, ["Ã"])); 724 if (items.length > 1) { 725 inner.appendChild( 726 el("button", { 727 type: "button", 728 class: "bw-lb-nav bw-lb-prev", 729 onclick: function (e) { 730 e.stopPropagation(); 731 idx = (idx - 1 + items.length) % items.length; 732 renderSlide(); 733 }, 734 }, ["â¹"]), 735 ); 736 inner.appendChild( 737 el("button", { 738 type: "button", 739 class: "bw-lb-nav bw-lb-next", 740 onclick: function (e) { 741 e.stopPropagation(); 742 idx = (idx + 1) % items.length; 743 renderSlide(); 744 }, 745 }, ["âº"]), 746 ); 747 } 748 inner.appendChild(body); 749 box.appendChild(inner); 750 document.body.appendChild(box); 751 lightboxEl = box; 752 document.addEventListener("keydown", onLbKey); 753 } 754 755 function fmtMoney(cents, cur) { 756 if (cents == null || cents === "") return null; 757 var n = Number(cents); 758 if (!isFinite(n)) return null; 759 return (n / 100).toFixed(0) + " " + (cur || "RUB"); 760 } 761 762 function clearCardSel() { 763 var cards = resultsRoot.querySelectorAll(".bw-card"); 764 for (var i = 0; i < cards.length; i++) { 765 cards[i].className = (cards[i].className || "").replace(/\bbw-card--sel\b/g, "").trim(); 766 } 767 } 768 769 function openRequestFor(r, card, stayInfo) { 770 state.selected = r; 771 state.selectedStay = stayInfo || null; 772 clearCardSel(); 773 if (card) card.className = ((card.className || "") + " bw-card--sel").trim(); 774 renderForm(); 775 pushMetrikaGoal("booking_start"); 776 setTimeout(function () { 777 var gs = qs("#bw-gs", resultsRoot); 778 if (gs) gs.focus(); 779 }, 300); 780 } 781 782 function renderAmenitiesBlock(r) { 783 ensureAmenityIcons(); 784 var groups = r.amenity_groups; 785 if (groups && groups.length) { 786 var ul = el("ul", { class: "bw-amenity-list" }); 787 for (var i = 0; i < groups.length; i++) { 788 var g = groups[i]; 789 var items = g.items || []; 790 for (var j = 0; j < items.length; j++) { 791 var it = items[j]; 792 var label = ""; 793 var icon = "wifi"; 794 if (it && typeof it === "object") { 795 label = String(it.label || ""); 796 icon = it.icon || "wifi"; 797 } else { 798 label = String(it || ""); 799 } 800 ul.appendChild( 801 el("li", { class: "bw-amenity-item" }, [ 802 el( 803 "div", 804 { 805 class: "bw-amenity-row", 806 title: label, 807 }, 808 [ 809 el("i", { class: amenityIconClass(icon), "aria-hidden": "true" }), 810 el("span", { class: "bw-amenity-cap" }, [label]), 811 ], 812 ), 813 ]), 814 ); 815 } 816 } 817 if (ul.childNodes.length) { 818 return el("div", { class: "bw-amenity-groups" }, [ul]); 819 } 820 } 821 var hl = r.amenity_highlights; 822 if (hl && hl.length) { 823 var tags = el("div", { class: "bw-tags" }, []); 824 for (var h = 0; h < hl.length; h++) { 825 tags.appendChild(el("span", { class: "bw-tag" }, [hl[h]])); 826 } 827 return tags; 828 } 829 return null; 830 } 831 832 function pluralNights(n) { 833 var k = n % 100; 834 var k1 = n % 10; 835 if (k >= 11 && k <= 14) return "ноÑей"; 836 if (k1 === 1) return "ноÑÑ"; 837 if (k1 >= 2 && k1 <= 4) return "ноÑи"; 838 return "ноÑей"; 839 } 840 841 function telHref(phone) { 842 var digits = String(phone || "").replace(/[^\d+]/g, ""); 843 if (!digits) return ""; 844 return "tel:" + digits; 845 } 846 847 function appendCardContactActions(actionsEl) { 848 var tel = telHref(state.contactPhone); 849 if (tel) { 850 actionsEl.appendChild(el("a", { class: "bw-cta bw-cta--secondary", href: tel }, ["ÐозвониÑÑ"])); 851 } 852 if (state.maxUrl) { 853 actionsEl.appendChild( 854 el( 855 "a", 856 { 857 class: "bw-cta bw-cta--max", 858 href: state.maxUrl, 859 target: "_blank", 860 rel: "noopener noreferrer", 861 }, 862 ["ÐапиÑаÑÑ Ð² MAX"], 863 ), 864 ); 865 } 866 } 867 868 function fetchConfig() { 869 return fetch( 870 apiUrl("/api/v1/widget/config", { 871 organization_id: orgId, 872 token: token, 873 }), 874 { credentials: "omit" }, 875 ).then(function (r) { 876 if (!r.ok) throw new Error("ÐÐ¸Ð´Ð¶ÐµÑ Ð½ÐµÐ´Ð¾ÑÑÑпен"); 877 return r.json(); 878 }); 879 } 880 881 function guestEmailRequired() { 882 return !!(state.config && state.config.guest_email_required); 883 } 884 885 function isValidGuestEmail(s) { 886 var t = String(s || "").trim(); 887 return t.length > 3 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t); 888 } 889 890 function formatIsoRu(iso) { 891 var m = String(iso || "").match(/^(\d{4})-(\d{2})-(\d{2})$/); 892 if (!m) return String(iso || ""); 893 return m[3] + "." + m[2] + "." + m[1]; 894 } 895 896 function nearbyOffsetLabel(offset) { 897 var d = Math.abs(Number(offset) || 0); 898 if (!d) return "иÑÑ Ð¾Ð´Ð½Ñе даÑÑ"; 899 if (offset < 0) return "на " + d + " дн. ÑанÑÑе"; 900 return "на " + d + " дн. позже"; 901 } 902 903 function pluralCategories(n) { 904 var x = Math.abs(Number(n) || 0) % 100; 905 var n1 = x % 10; 906 if (x > 10 && x < 20) return "ваÑианÑов"; 907 if (n1 > 1 && n1 < 5) return "ваÑианÑа"; 908 if (n1 === 1) return "ваÑианÑ"; 909 return "ваÑианÑов"; 910 } 911 912 function buildSearchCriteriaFromInputs() { 913 var ci = qs("#bw-ci", formRoot).value; 914 var co = qs("#bw-co", formRoot).value; 915 var ad = qs("#bw-ad", formRoot).value; 916 var ch = qs("#bw-ch", formRoot).value; 917 return { check_in: ci, check_out: co, adults: ad, children: ch }; 918 } 919 920 function isNearbyEnabled() { 921 return ( 922 !!state.config && 923 !!state.config.nearby_dates_enabled && 924 Number(state.config.nearby_dates_range_days || 0) > 0 925 ); 926 } 927 928 function applyNearbyWindow(win) { 929 if (!win) return; 930 var ci = String(win.check_in || ""); 931 var co = String(win.check_out || ""); 932 if (!ci || !co) return; 933 var ciEl = qs("#bw-ci", formRoot); 934 var coEl = qs("#bw-co", formRoot); 935 if (ciEl) ciEl.value = ci; 936 if (coEl) coEl.value = co; 937 syncDateInputs("ci"); 938 state.lastSearch = { 939 check_in: ci, 940 check_out: co, 941 adults: state.lastSearch ? state.lastSearch.adults : qs("#bw-ad", formRoot).value, 942 children: state.lastSearch ? state.lastSearch.children : qs("#bw-ch", formRoot).value, 943 }; 944 state.categories = win.categories || []; 945 state.transitionOptions = []; 946 state.selected = null; 947 state.selectedStay = null;
948 // Keep other nearby windows for context, but mark applied dates as main list. 949 renderList({ appliedOffset: Number(win.offset_days || 0) }); 950 if (listHost && typeof listHost.scrollIntoView === "function") { 951 try { 952 listHost.scrollIntoView({ behavior: "smooth", block: "nearest" }); 953 } catch (e) {} 954 } 955 } 956 957 function renderNearbyAction(opts) { 958 nearbyActionHost.innerHTML = ""; 959 if (!state.lastSearch) return; 960 var appliedOffset = opts && opts.appliedOffset != null ? Number(opts.appliedOffset) : null; 961 var windows = state.nearbyDateWindows || []; 962 var hasPrimary = !!(state.categories && state.categories.length); 963 var rangeDays = Math.max(0, Number((state.config && state.config.nearby_dates_range_days) || 5)); 964 var showNearbyUi = isNearbyEnabled() || windows.length > 0; 965 966 if (!showNearbyUi) { 967 // ÐонÑиг еÑÑ Ð½Ðµ пÑиÑÑл â не ÑÑмим; еÑли вклÑÑено, поÑле загÑÑзки config renderNearbyAction вÑзовÑÑ Ñнова. 968 if (state.nearbyLoading && !state.config) { 969 nearbyActionHost.appendChild( 970 el("p", { class: "bw-nearby-hint muted" }, ["ÐодбиÑаем ÑоÑедние даÑÑâ¦"]), 971 ); 972 } 973 return; 974 } 975 976 if (state.nearbyLoading) { 977 nearbyActionHost.appendChild( 978 el("p", { class: "bw-nearby-hint muted" }, [ 979 "ÐодбиÑаем ваÑианÑÑ Ð² диапазоне ±" + rangeDays + " днейâ¦", 980 ]), 981 ); 982 return; 983 } 984 985 if (!windows.length) { 986 if (!hasPrimary && isNearbyEnabled()) { 987 nearbyActionHost.appendChild( 988 el("p", { class: "bw-nearby-hint muted" }, [ 989 "Рдиапазоне ±" + rangeDays + " дней ÑвободнÑÑ Ð²Ð°ÑианÑов Ñоже не наÑлоÑÑ.", 990 ]), 991 ); 992 } 993 return; 994 } 995 996 var hintText = hasPrimary 997 ? "Также еÑÑÑ Ð²Ð°ÑианÑÑ Ð½Ð° ÑоÑедние даÑÑ â можно ÑдвинÑÑÑ Ð·Ð°ÐµÐ·Ð´/вÑезд:" 998 : "Ðа вÑбÑаннÑе даÑÑ ÑвободнÑÑ Ð½Ð¾Ð¼ÐµÑов неÑ. ÐÑÑÑ Ð²Ð°ÑианÑÑ Ð½Ð° ÑоÑедние даÑÑ:"; 999 nearbyActionHost.appendChild(el("p", { class: "bw-nearby-hint" }, [hintText])); 1000 1001 var chips = el("div", { class: "bw-nearby-chips" }, []); 1002 windows.forEach(function (win) { 1003 var cats = win.categories || []; 1004 var count = cats.length; 1005 if (!count) return; 1006 var offset = Number(win.offset_days || 0); 1007 var label = 1008 formatIsoRu(win.check_in) + 1009 " â " + 1010 formatIsoRu(win.check_out) + 1011 " (" + 1012 nearbyOffsetLabel(offset) + 1013 ", " + 1014 count + 1015 " " + 1016 pluralCategories(count) + 1017 ")"; 1018 var chipClass = 1019 "bw-nearby-chip" + (appliedOffset != null && appliedOffset === offset ? " bw-nearby-chip--active" : ""); 1020 chips.appendChild( 1021 el( 1022 "button", 1023 { 1024 type: "button", 1025 class: chipClass, 1026 onclick: function () { 1027 applyNearbyWindow(win); 1028 }, 1029 }, 1030 [label], 1031 ), 1032 ); 1033 }); 1034 if (chips.childNodes.length) nearbyActionHost.appendChild(chips); 1035 nearbyActionHost.appendChild( 1036 el("span", { class: "bw-nearby-meta" }, [ 1037 "Ðайдено дополниÑелÑнÑÑ Ð¾ÐºÐ¾Ð½: " + windows.length + " (диапазон ±" + rangeDays + " дн.)", 1038 ]), 1039 ); 1040 } 1041 1042 function requestAvailability(criteria, includeNearbyDates) { 1043 var q = { 1044 organization_id: orgId, 1045 token: token, 1046 check_in: criteria.check_in, 1047 check_out: criteria.check_out, 1048 adults: criteria.adults, 1049 children: criteria.children, 1050 }; 1051 if (includeNearbyDates) q.include_nearby_dates = "true"; 1052 return fetch(apiUrl("/api/v1/widget/availability", q), { credentials: "omit" }) 1053 .then(function (r) { 1054 if (r.status === 429) throw new Error("СлиÑком много запÑоÑов"); 1055 if (!r.ok) { 1056 return r.json().then(function (j) { 1057 var d = j && j.detail; 1058 var msg = 1059 typeof d === "string" 1060 ? d 1061 : d && d[0] && d[0].msg 1062 ? d[0].msg 1063 : "ÐÑибка поиÑка"; 1064 throw new Error(msg); 1065 }); 1066 } 1067 return r.json(); 1068 }); 1069 } 1070 1071 function runSearch() { 1072 setErr(""); 1073 syncDateInputs("search"); 1074 var c = buildSearchCriteriaFromInputs(); 1075 var ci = c.check_in; 1076 var co = c.check_out; 1077 if (!ci || !co) { 1078 showSearchMessage("УкажиÑе даÑÑ Ð·Ð°ÐµÐ·Ð´Ð° и вÑезда.", true); 1079 return; 1080 } 1081 if (co <= ci) { 1082 showSearchMessage("ÐаÑа вÑезда должна бÑÑÑ Ð¿Ð¾Ð·Ð¶Ðµ даÑÑ Ð·Ð°ÐµÐ·Ð´Ð°.", true); 1083 return; 1084 } 1085 state.lastSearch = c; 1086 state.nearbyDateWindows = []; 1087 state.transitionOptions = []; 1088 state.selected = null; 1089 state.selectedStay = null; 1090 setResultsPanelVisible(true); 1091 nearbyHost.innerHTML = ""; 1092 listHost.innerHTML = "ÐагÑÑзкаâ¦"; 1093 // ÐÑегда пÑоÑим ÑоÑедние даÑÑ: API веÑнÑÑ Ð¸Ñ ÑолÑко еÑли вклÑÑено в наÑÑÑÐ¾Ð¹ÐºÐ°Ñ . 1094 state.nearbyLoading = true; 1095 renderNearbyAction(); 1096 1097 requestAvailability(c, true) 1098 .then(function (data) { 1099 state.categories = data.categories || data.rooms || []; 1100 state.transitionOptions = data.transition_options || []; 1101 state.nearbyDateWindows = data.nearby_date_windows || []; 1102 state.nearbyLoading = false;
1103 renderList(); 1104 }) 1105 .catch(function (e) { 1106 state.nearbyDateWindows = []; 1107 state.nearbyLoading = false; 1108 showSearchMessage(searchErrorRu(e && e.message ? e.message : ""), true); 1109 }) 1110 .finally(function () { 1111 state.nearbyLoading = false; 1112 }); 1113 } 1114 1115 function renderCards(categories, stayInfo) { 1116 var wrap = el("div", { class: "bw-cards" }); 1117 categories.forEach(function (r) { 1118 var items = galleryItemsForRoom(r); 1119 var previewIdx = 0; 1120 var card = el("div", { class: "bw-card" }); 1121 var gal = el("div", { class: "bw-gal" }); 1122 var galMain = el("div", { class: "bw-gal-main" }); 1123 var galThumbs = el("div", { class: "bw-thumbs" }); 1124 1125 function updateThumbStyles() { 1126 var ths = galThumbs.querySelectorAll(".bw-thumb"); 1127 for (var t = 0; t < ths.length; t++) { 1128 ths[t].className = ths[t].className.replace(/\bbw-thumb--on\b/g, "").trim(); 1129 if (t === previewIdx) ths[t].className = (ths[t].className + " bw-thumb--on").trim(); 1130 } 1131 } 1132 1133 function syncPreview() { 1134 galMain.innerHTML = ""; 1135 if (!items.length) { 1136 galMain.appendChild( 1137 el("div", { class: "muted", style: "padding:20px;text-align:center;font-size:12px" }, [ 1138 "ÐÐµÑ ÑоÑо и видео", 1139 ]), 1140 ); 1141 return; 1142 } 1143 var it = items[previewIdx]; 1144 if (it.kind === "image") { 1145 var img = el("img", { src: widgetAssetUrl(it.url), alt: "" }); 1146 img.addEventListener("click", function (e) { 1147 e.stopPropagation(); 1148 openLightbox(r, previewIdx); 1149 }); 1150 galMain.appendChild(img); 1151 } else { 1152 var firstImg = r.media_urls && r.media_urls[0] ? widgetAssetUrl(r.media_urls[0]) : ""; 1153 if (firstImg) { 1154 galMain.appendChild( 1155 el("img", { 1156 src: firstImg, 1157 alt: "", 1158 style: 1159 "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;pointer-events:none", 1160 }), 1161 ); 1162 } else { 1163 galMain.style.background = "linear-gradient(145deg,#1e293b,#0f172a)"; 1164 } 1165 var ov = el("div", { class: "bw-vplay" }); 1166 ov.appendChild(el("span", {}, ["â¶"])); 1167 ov.addEventListener("click", function (e) { 1168 e.stopPropagation(); 1169 openLightbox(r, previewIdx); 1170 }); 1171 galMain.appendChild(ov); 1172 } 1173 } 1174 1175 items.forEach(function (it, gi) { 1176 var th = el("div", { 1177 class: "bw-thumb" + (gi === 0 ? " bw-thumb--on" : ""), 1178 onclick: function (e) { 1179 e.stopPropagation(); 1180 previewIdx = gi; 1181 syncPreview(); 1182 updateThumbStyles(); 1183 if (it.kind === "video") { 1184 openLightbox(r, gi); 1185 } 1186 }, 1187 }); 1188 if (it.kind === "image") { 1189 th.appendChild(el("img", { src: widgetAssetUrl(it.url), alt: "" })); 1190 } else { 1191 th.className = (th.className + " bw-thumb--vid").trim(); 1192 th.appendChild(document.createTextNode("â¶")); 1193 } 1194 galThumbs.appendChild(th); 1195 }); 1196 1197 syncPreview(); 1198 gal.appendChild(galMain); 1199 if (items.length) gal.appendChild(galThumbs); 1200 1201 var body = el("div", { class: "bw-card-body" }); 1202 body.appendChild( 1203 el("h3", { class: "bw-title" }, [ 1204 r.public_name || (r.kind === "category" ? "ÐаÑегоÑиÑ" : "ÐомеÑ"), 1205 ]), 1206 ); 1207 1208 var nights = r.stay_nights != null ? Number(r.stay_nights) : null; 1209 var total = fmtMoney(r.stay_total_cents, r.currency); 1210 var perNight = fmtMoney(r.base_rate_cents, r.currency); 1211 if (total && nights && nights > 0) { 1212 body.appendChild( 1213 el("div", { class: "bw-price" }, [ 1214 total, 1215 el("span", { class: "bw-price-sub" }, [" за " + nights + " " + pluralNights(nights)]), 1216 ]), 1217 ); 1218 if (perNight) 1219 body.appendChild( 1220 el("div", { class: "muted" }, [perNight + " à " + nights + " ноÑ."]), 1221 ); 1222 } else if (perNight) { 1223 body.appendChild(el("div", { class: "bw-price" }, ["Ð¾Ñ " + perNight + " / ноÑÑ"])); 1224 } 1225 1226 var facts = []; 1227 if (r.floor != null && r.floor !== "") facts.push("ÑÑаж " + r.floor); 1228 var cap = r.max_guests != null ? r.max_guests : r.bed_count; 1229 if (cap != null && cap !== "") facts.push("до " + cap + " гоÑÑей"); 1230 if (r.area_sqm != null && r.area_sqm !== "") 1231 facts.push(Math.round(r.area_sqm * 10) / 10 + " м²"); 1232 if (facts.length) body.appendChild(el("div", { class: "bw-facts" }, [facts.join(" · ")])); 1233 1234 var amBlock = renderAmenitiesBlock(r); 1235 if (amBlock) body.appendChild(amBlock); 1236 1237 if (r.description) 1238 body.appendChild(el("div", { class: "bw-desc" }, [r.description])); 1239 1240 var cta = el( 1241 "button", 1242 { 1243 type: "button", 1244 class: "bw-cta", 1245 onclick: function (e) { 1246 e.stopPropagation(); 1247 openRequestFor(r, card, stayInfo); 1248 }, 1249 }, 1250 ["ÐÑÑавиÑÑ Ð·Ð°ÑвкÑ"], 1251 ); 1252 var actions = el("div", { class: "bw-actions" }, [cta]); 1253 appendCardContactActions(actions); 1254 body.appendChild(actions); 1255 1256 var head = el("div", { class: "bw-card-head" }); 1257 head.appendChild(gal); 1258 head.appendChild(body); 1259 card.appendChild(head); 1260 1261 card.addEventListener("click", function (e) { 1262 var t = e.target; 1263 if (t && typeof t.closest === "function" && t.closest(".bw-thumb,button,a,.bw-gal-main")) return; 1264 openRequestFor(r, card, stayInfo); 1265 }); 1266 1267 wrap.appendChild(card); 1268 }); 1269 return wrap; 1270 } 1271 1272 function renderTransitionCards(options, stayInfo) { 1273 var wrap = el("div", { class: "bw-cards" });
1274 (options || []).forEach(function (opt) { 1275 var segs = opt.segments || []; 1276 if (segs.length !== 2) return; 1277 var card = el("div", { class: "bw-card" }); 1278 var body = el("div", { class: "bw-card-body", style: "flex:1 1 100%" }); 1279 body.appendChild(el("div", { class: "bw-tr-badge" }, ["С пеÑеездом"])); 1280 body.appendChild(el("h3", { class: "bw-title" }, ["Ðва номеÑа на пеÑиод"])); 1281 segs.forEach(function (seg) { 1282 body.appendChild( 1283 el("div", { class: "bw-tr-seg" }, [ 1284 el("strong", {}, [ 1285 formatIsoRu(seg.check_in) + " â " + formatIsoRu(seg.check_out), 1286 ]), 1287 document.createTextNode(" · " + (seg.public_name || "ÐомеÑ")), 1288 ]), 1289 ); 1290 }); 1291 var nights = opt.stay_nights != null ? Number(opt.stay_nights) : null; 1292 var total = fmtMoney(opt.stay_total_cents, opt.currency); 1293 if (total) { 1294 body.appendChild( 1295 el("div", { class: "bw-price" }, [ 1296 total, 1297 nights && nights > 0 1298 ? el("span", { class: "bw-price-sub" }, [" за " + nights + " " + pluralNights(nights)]) 1299 : null, 1300 ].filter(Boolean)), 1301 ); 1302 } 1303 var selectable = { 1304 kind: "transition", 1305 id: opt.id, 1306 public_name: "С пеÑеездом", 1307 segments: segs, 1308 stay_total_cents: opt.stay_total_cents, 1309 currency: opt.currency, 1310 stay_nights: opt.stay_nights, 1311 }; 1312 var cta = el( 1313 "button", 1314 { 1315 type: "button", 1316 class: "bw-cta", 1317 onclick: function (e) { 1318 e.stopPropagation(); 1319 openRequestFor(selectable, card, stayInfo); 1320 }, 1321 }, 1322 ["ÐÑÑавиÑÑ Ð·Ð°ÑвкÑ"], 1323 ); 1324 var actions = el("div", { class: "bw-actions" }, [cta]); 1325 appendCardContactActions(actions); 1326 body.appendChild(actions); 1327 card.appendChild(body); 1328 card.addEventListener("click", function (e) { 1329 var t = e.target; 1330 if (t && typeof t.closest === "function" && t.closest("button,a")) return; 1331 openRequestFor(selectable, card, stayInfo); 1332 }); 1333 wrap.appendChild(card); 1334 }); 1335 return wrap; 1336 } 1337 1338 function renderList(opts) { 1339 listHost.innerHTML = ""; 1340 nearbyHost.innerHTML = ""; 1341 renderNearbyAction(opts); 1342 var baseStay = state.lastSearch 1343 ? { 1344 check_in: state.lastSearch.check_in, 1345 check_out: state.lastSearch.check_out, 1346 offset_days: 0, 1347 } 1348 : null; 1349 var hasTransitions = !!(state.transitionOptions && state.transitionOptions.length); 1350 if (!state.categories.length && !hasTransitions) { 1351 listHost.appendChild( 1352 el("p", { class: "muted" }, [ 1353 state.nearbyDateWindows && state.nearbyDateWindows.length 1354 ? "Ðа вÑбÑаннÑе даÑÑ Ð¿Ð¾Ð´Ñ Ð¾Ð´ÑÑÐ¸Ñ ÐºÐ°ÑегоÑий Ð½ÐµÑ â вÑбеÑиÑе ÑоÑедние даÑÑ Ð²ÑÑе." 1355 : "ÐÐµÑ Ð¿Ð¾Ð´Ñ Ð¾Ð´ÑÑÐ¸Ñ ÐºÐ°ÑегоÑий на вÑбÑаннÑе даÑÑ Ð¸ ÑоÑÑав гоÑÑей.", 1356 ]), 1357 ); 1358 } 1359 if (state.categories.length) { 1360 listHost.appendChild(renderCards(state.categories, baseStay)); 1361 } else if (hasTransitions) { 1362 var trStay = state.lastSearch 1363 ? { check_in: state.lastSearch.check_in, check_out: state.lastSearch.check_out, offset_days: 0 } 1364 : null; 1365 listHost.appendChild( 1366 el("p", { class: "muted" }, [ 1367 "ÐепÑеÑÑвного заÑÐµÐ»ÐµÐ½Ð¸Ñ Ð½Ð° веÑÑ Ð¿ÐµÑиод Ð½ÐµÑ â доÑÑÑÐ¿Ð½Ñ Ð²Ð°ÑианÑÑ Ñ Ð¾Ð´Ð½Ð¸Ð¼ пеÑеездом:", 1368 ]), 1369 ); 1370 listHost.appendChild(renderTransitionCards(state.transitionOptions, trStay)); 1371 } 1372 if (state.landing && state.landing.room && state.categories.length && !state.selected) { 1373 var wantRoom = String(state.landing.room); 1374 for (var ai = 0; ai < state.categories.length; ai++) { 1375 var cr = state.categories[ai]; 1376 if (String(cr.room_category_id || cr.id) === wantRoom) { 1377 openRequestFor(cr, null, baseStay); 1378 break; 1379 } 1380 } 1381 } 1382 if (state.nearbyDateWindows && state.nearbyDateWindows.length) { 1383 var appliedOffset = 1384 opts && opts.appliedOffset != null ? Number(opts.appliedOffset)
1384: null; 1385 state.nearbyDateWindows.forEach(function (win) { 1386 var stay = { 1387 check_in: String(win.check_in || ""), 1388 check_out: String(win.check_out || ""), 1389 offset_days: Number(win.offset_days || 0), 1390 }; 1391 if (appliedOffset != null && stay.offset_days === appliedOffset) return; 1392 var cats = win.categories || []; 1393 if (!cats.length) return; 1394 var sec = el("div", { class: "bw-nearby-window", id: "bw-near-" + stay.offset_days }, []); 1395 sec.appendChild( 1396 el("h3", { class: "bw-nearby-title" }, [ 1397 "ÐаÑианÑÑ " + 1398 nearbyOffsetLabel(stay.offset_days) + 1399 ": " + 1400 formatIsoRu(stay.check_in) + 1401 " â " + 1402 formatIsoRu(stay.check_out), 1403 ]), 1404 ); 1405 sec.appendChild( 1406 el("p", { class: "muted bw-nearby-sub" }, [ 1407 cats.length + 1408 " " + 1409 pluralCategories(cats.length) + 1410 ". ÐажмиÑе «ÐÑÑавиÑÑ Ð·Ð°ÑвкÑ» â даÑÑ Ð·Ð°ÐµÐ·Ð´Ð°/вÑезда подÑÑавÑÑÑÑ Ð°Ð²ÑомаÑиÑеÑки.", 1411 ]), 1412 ); 1413 sec.appendChild(renderCards(cats, stay)); 1414 nearbyHost.appendChild(sec); 1415 }); 1416 } 1417 if ( 1418 !state.categories.length && 1419 !hasTransitions && 1420 (!state.nearbyDateWindows || !state.nearbyDateWindows.length) 1421 ) { 1422 reqHost.innerHTML = ""; 1423 } 1424 reqHost.innerHTML = ""; 1425 } 1426 1427 function renderForm() { 1428 reqHost.innerHTML = ""; 1429 closeRequestModal(); 1430 if (!state.selected) return; 1431 var r = state.selected; 1432 var overlay = el("div", { 1433 class: "bw-req-overlay", 1434 onclick: function (e) { 1435 if (e.target === overlay) closeRequestModal(); 1436 }, 1437 }); 1438 var modal = el("div", { class: "bw-req-modal" }); 1439 modal.appendChild( 1440 el( 1441 "button", 1442 { type: "button", class: "bw-req-close", onclick: closeRequestModal, "aria-label": "ÐакÑÑÑÑ" }, 1443 ["Ã"], 1444 ), 1445 ); 1446 var reqTitle = 1447 r.kind === "transition" 1448 ? "ÐаÑвка: ваÑÐ¸Ð°Ð½Ñ Ñ Ð¿ÐµÑеездом" 1449 : "ÐаÑвка: «" + 1450 (r.public_name || (r.kind === "category" ? "каÑегоÑиÑ" : "номеÑ")) + 1451 "»"; 1452 modal.appendChild(el("h3", { style: "font-size:16px;margin:12px 0 8px" }, [reqTitle])); 1453 if (r.kind === "transition" && r.segments && r.segments.length) { 1454 r.segments.forEach(function (seg) { 1455 modal.appendChild( 1456 el("div", { class: "bw-tr-seg", style: "margin-bottom:6px" }, [ 1457 formatIsoRu(seg.check_in) + 1458 " â " + 1459 formatIsoRu(seg.check_out) + 1460 " · " + 1461 (seg.public_name || "ÐомеÑ"), 1462 ]), 1463 ); 1464 }); 1465 } 1466 1467 var gsurname = el("input", { type: "text", placeholder: "ФамилиÑ", id: "bw-gs" }); 1468 var gname = el("input", { type: "text", placeholder: "ÐмÑ", id: "bw-gf" }); 1469 var phone = el("input", { type: "tel", placeholder: "+7 (___) ___-__-__", id: "bw-ph", inputmode: "tel" }); 1470 gsurname.addEventListener("input", clearFieldValidation); 1471 gname.addEventListener("input", clearFieldValidation); 1472 phone.addEventListener("input", clearFieldValidation); 1473 function digitsOnly(v) { 1474 return String(v || "").replace(/\D/g, ""); 1475 } 1476 function formatRuPhone(v) { 1477 var d = digitsOnly(v); 1478 if (!d) return ""; 1479 if (d[0] === "8") d = "7" + d.slice(1); 1480 if (d[0] !== "7") d = "7" + d; 1481 d = d.slice(0, 11); 1482 var p = d.slice(1); 1483 var out = "+7"; 1484 if (p.length) out += " (" + p.slice(0, 3); 1485 if (p.length >= 3) out += ")"; 1486 if (p.length > 3) out += " " + p.slice(3, 6); 1487 if (p.length > 6) out += "-" + p.slice(6, 8); 1488 if (p.length > 8) out += "-" + p.slice(8, 10); 1489 return out; 1490 } 1491 phone.addEventListener("input", function () { 1492 phone.value = formatRuPhone(phone.value); 1493 }); 1494 phone.addEventListener("focus", function () { 1495 if (!phone.value) phone.value = "+7 ("; 1496 }); 1497 var email = el("input", { 1498 type: "email", 1499 placeholder: guestEmailRequired() ? "Email *" : "Email (необÑзаÑелÑно)", 1500 id: "bw-em", 1501 }); 1502 if (guestEmailRequired()) email.required = true; 1503 var notes = el("textarea", { placeholder: "ÐомменÑаÑий", id: "bw-no", rows: "3", style: "width:100%;resize:vertical" }); 1504 1505 modal.appendChild( 1506 el("div", { class: "row" }, [ 1507 el("div", { class: "field", style: "flex:1;min-width:180px" }, [el("label", {}, ["ФамилиÑ"]), gsurname]), 1508 el("div", { class: "field", style: "flex:1;min-width:180px" }, [el("label", {}, ["ÐмÑ"]), gname]), 1509 el("div", { class: "field", style: "flex:1;min-width:160px" }, [el("label", {}, ["ТелеÑон"]), phone]), 1510 ]), 1511 ); 1512 modal.appendChild( 1513 el("div", { class: "row" }, [el("div", { class: "field", style: "flex:1" }, [el("label", {}, ["Email"]), email])]), 1514 ); 1515 modal.appendChild( 1516 el("div", { class: "row" }, [el("div", { class: "field", style: "flex:1" }, [el("label", {}, ["ÐомменÑаÑий"]), notes])]), 1517 ); 1518
1519 var hp = el("input", { type: "text", name: "website", style: "position:absolute;left:-5000px", tabindex: "-1" }); 1520 modal.appendChild(hp); 1521 1522 var capDiv = el("div", { id: "bw-captcha" }); 1523 modal.appendChild(capDiv); 1524 1525 appendPdConsentAndSubmit(modal, { 1526 consentId: "bw-pd-consent", 1527 termsId: "bw-guest-terms", 1528 submitId: "bw-submit", 1529 style: "margin-top:10px", 1530 legalBaseUrl: opts.legalBaseUrl, 1531 onSubmit: function () { 1532 void submitReq(); 1533 }, 1534 }); 1535 overlay.appendChild(modal); 1536 requestModalEl = overlay; 1537 resultsRoot.appendChild(overlay); 1538 1539 state.captchaToken = null; 1540 1541 if (state.config && state.config.captcha_enabled) { 1542 var provider = String(state.config.captcha_provider || "altcha").toLowerCase(); 1543 if (provider === "turnstile" && state.config.captcha_site_key) { 1544 loadTurnstile(function () { 1545 if (!global.turnstile) return; 1546 capDiv.innerHTML = ""; 1547 global.turnstile.render(capDiv, { 1548 sitekey: state.config.captcha_site_key, 1549 callback: function (tok) { 1550 state.captchaToken = tok; 1551 }, 1552 "expired-callback": function () { 1553 state.captchaToken = null; 1554 }, 1555 }); 1556 }); 1557 } else if (provider === "altcha") { 1558 capDiv.innerHTML = ""; 1559 var box = el("div", { class: "row", style: "margin-bottom:0" }); 1560 var tokenPart = ""; 1561 var qText = el("div", { class: "bw-captcha-q" }, ["ÐÑовеÑка..."]); 1562 var ans = el("input", { type: "text", inputmode: "numeric", placeholder: "ÐÑвеÑ", style: "max-width:120px" }); 1563 ans.addEventListener("input", function () { 1564 var v = String(ans.value || "").trim(); 1565 state.captchaToken = tokenPart && v ? tokenPart + ":" + v : null; 1566 }); 1567 box.appendChild(el("div", { class: "bw-captcha-row" }, [qText, ans])); 1568 capDiv.appendChild(box); 1569 fetch( 1570 apiUrl("/api/v1/widget/captcha/challenge", { 1571 organization_id: orgId, 1572 token: token, 1573 }), 1574 { credentials: "omit" }, 1575 ) 1576 .then(function (r) { 1577 if (!r.ok) throw new Error("ÐапÑа недоÑÑÑпна"); 1578 return r.json(); 1579 }) 1580 .then(function (j) { 1581 tokenPart = String((j && j.token) || ""); 1582 qText.textContent = String((j && j.question) || "РеÑиÑе пÑимеÑ"); 1583 state.captchaToken = null; 1584 }) 1585 .catch(function () { 1586 qText.textContent = "Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ ÐºÐ°Ð¿ÑÑ"; 1587 state.captchaToken = null; 1588 }); 1589 } 1590 } 1591 } 1592 1593 function loadTurnstile(cb) { 1594 if (global.turnstile) return cb(); 1595 var s = document.createElement("script"); 1596 s.src = "https://challenges.cloudflare.com/turnstile/v0/api.js"; 1597 s.async = true; 1598 s.onload = cb; 1599 document.head.appendChild(s); 1600 } 1601 1602 function submitReq() { 1603 setErr(""); 1604 if (!state.selected) return; 1605 var selectedStay = state.selectedStay || {}; 1606 var ci = selectedStay.check_in || qs("#bw-ci", formRoot).value; 1607 var co = selectedStay.check_out || qs("#bw-co", formRoot).value; 1608 var ad = parseInt(qs("#bw-ad", formRoot).value, 10) || 1; 1609 var ch = parseInt(qs("#bw-ch", formRoot).value, 10) || 0; 1610 var gs = (qs("#bw-gs", resultsRoot) && qs("#bw-gs", resultsRoot).value) || ""; 1611 var gf = (qs("#bw-gf", resultsRoot) && qs("#bw-gf", resultsRoot).value) || ""; 1612 var ph = (qs("#bw-ph", resultsRoot) && qs("#bw-ph", resultsRoot).value) || ""; 1613 var em = (qs("#bw-em", resultsRoot) && qs("#bw-em", resultsRoot).value) || ""; 1614 var no = (qs("#bw-no", resultsRoot) && qs("#bw-no", resultsRoot).value) || ""; 1615 var website = 1616 (qs('input[name="website"]', resultsRoot) && qs('input[name="website"]', resultsRoot).value) || ""; 1617 1618 clearFieldValidation(); 1619 var hasMissing = false;
1620 if (!gs.trim()) { 1621 markRequiredField(qs("#bw-gs", resultsRoot)); 1622 hasMissing = true; 1623 } 1624 if (!gf.trim()) { 1625 markRequiredField(qs("#bw-gf", resultsRoot)); 1626 hasMissing = true; 1627 } 1628 if (!ph.trim()) { 1629 markRequiredField(qs("#bw-ph", resultsRoot)); 1630 hasMissing = true; 1631 } 1632 if (guestEmailRequired() && !isValidGuestEmail(em)) { 1633 markRequiredField(qs("#bw-em", resultsRoot)); 1634 hasMissing = true; 1635 } 1636 if (hasMissing) { 1637 setErr("ÐаполниÑе обÑзаÑелÑнÑе полÑ"); 1638 return; 1639 } 1640 var consent = qs("#bw-pd-consent", resultsRoot); 1641 var terms = qs("#bw-guest-terms", resultsRoot); 1642 if (!consent || !consent.checked || !terms || !terms.checked) { 1643 setErr("ÐÑимиÑе ÑоглаÑение модÑÐ»Ñ Ð¸ дайÑе ÑоглаÑие на обÑабоÑÐºÑ Ð¿ÐµÑÑоналÑнÑÑ Ð´Ð°Ð½Ð½ÑÑ "); 1644 return; 1645 } 1646 if ( 1647 state.config && 1648 state.config.captcha_enabled && 1649 String(state.config.captcha_provider || "altcha").toLowerCase() !== "none" && 1650 !state.captchaToken 1651 ) { 1652 setErr("ÐÑойдиÑе пÑовеÑÐºÑ ÐºÐ°Ð¿Ñи"); 1653 return; 1654 } 1655 1656 var body = { 1657 check_in: ci, 1658 check_out: co, 1659 adults: ad, 1660 children: ch, 1661 guest_name: (gs.trim() + " " + gf.trim()).trim(), 1662 guest_surname: gs.trim(), 1663 guest_first_name: gf.trim(), 1664 phone: ph.trim(), 1665 email: em.trim() || null, 1666 notes: no.trim() || null, 1667 captcha_token: state.captchaToken || null, 1668 website: website || null, 1669 pd_consent: true, 1670 guest_terms_accepted: true, 1671 }; 1672 if (state.selected.kind === "transition" && state.selected.segments) { 1673 body.stay_segments = state.selected.segments.map(function (seg) { 1674 var s = { 1675 check_in: seg.check_in, 1676 check_out: seg.check_out, 1677 }; 1678 if (seg.kind === "room" && seg.room_id) s.room_id = seg.room_id; 1679 else if (seg.room_category_id) s.room_category_id = seg.room_category_id; 1680 else if (seg.room_id) s.room_id = seg.room_id; 1681 return s; 1682 }); 1683 } else if (state.selected.kind === "room") { 1684 body.room_id = state.selected.id; 1685 } else { 1686 body.room_category_id = state.selected.id; 1687 } 1688 if (state.landing) { 1689 var offerKey = 1690 (state.resolvedOffer && state.resolvedOffer.offer_id) || state.landing.offer || ""; 1691 if (offerKey) body.offer_id = offerKey; 1692 ["utm_source", "utm_medium", "utm_campaign", "utm_content", "utm_term", "yclid"].forEach( 1693 function (k) { 1694 if (state.landing[k]) body[k] = state.landing[k]; 1695 }, 1696 ); 1697 } 1698 1699 var btn = qs("#bw-submit", resultsRoot); 1700 if (btn) btn.disabled = true; 1701 fetch( 1702 apiUrl("/api/v1/widget/requests", { 1703 organization_id: orgId, 1704 token: token, 1705 }), 1706 { 1707 method: "POST", 1708 credentials: "omit", 1709 headers: { "Content-Type": "application/json" }, 1710 body: JSON.stringify(body), 1711 }, 1712 ) 1713 .then(function (r) { 1714 if (r.status === 429) throw new Error("СлиÑком много запÑоÑов или дÑÐ±Ð»Ð¸ÐºÐ°Ñ Ð·Ð°Ñвки"); 1715 if (!r.ok) 1716 return r.json().then(function (j) { 1717 var d = j.detail; 1718 var msg = 1719 typeof d === "string" 1720 ? d 1721 : d && d[0] && d[0].msg 1722 ? d[0].msg 1723 : "ÐÑибка"; 1724 throw new Error(msg); 1725 }); 1726 return r.json(); 1727 }) 1728 .then(function (res) { 1729 reqHost.innerHTML = ""; 1730 closeRequestModal(); 1731 listHost.innerHTML = ""; 1732 setErr(""); 1733 var offerId = 1734 (state.resolvedOffer && state.resolvedOffer.offer_id) || 1735 (state.landing && state.landing.offer) || 1736 ""; 1737 if (offerId) { 1738 pushEcommerce("add", { 1739 id: offerId, 1740 name: (state.selected && state.selected.public_name) || "booking", 1741 price: (state.selected && state.selected.stay_total_cents) / 100 || 0, 1742 quantity: 1, 1743 }); 1744 } 1745 pushMetrikaGoal("booking_request"); 1746 resultsRoot.appendChild(el("p", { style: "color:#16a34a" }, ["ÐаÑвка оÑпÑавлена. ÐÑ ÑвÑжемÑÑ Ñ Ð²Ð°Ð¼Ð¸."])); 1747 }) 1748 .catch(function (e) { 1749 setErr(e.message || "ÐÑибка"); 1750 }) 1751 .finally(function () { 1752 syncSubmitWithPdConsent( 1753 qs("#bw-submit", resultsRoot), 1754 qs("#bw-pd-consent", resultsRoot), 1755 qs("#bw-guest-terms", resultsRoot), 1756 ); 1757 }); 1758 } 1759 1760 function applyLandingToForm() { 1761 var L = state.landing; 1762 if (!L) return; 1763 var ciEl = qs("#bw-ci", formRoot); 1764 var coEl = qs("#bw-co", formRoot); 1765 var adEl = qs("#bw-ad", formRoot); 1766 if (L.in && ciEl) ciEl.value = L.in; 1767 if (L.out && coEl) coEl.value = L.out; 1768 if (L.adults && adEl) adEl.value = L.adults; 1769 } 1770 1771 function resolveOfferThenSearch() { 1772 var L = state.landing; 1773 if (!L || !L.offer) { 1774 if (L && L.in && L.out) runSearch(); 1775 return; 1776 } 1777 fetch( 1778 apiUrl("/api/v1/widget/offer-resolve", { 1779 organization_id: orgId, 1780 token: token, 1781 offer: L.offer, 1782 adults: L.adults || undefined, 1783 }), 1784 { credentials: "omit" }, 1785 ) 1786 .then(function (r) { 1787 if (!r.ok) throw new Error("offer"); 1788 return r.json(); 1789 }) 1790 .then(function (data) { 1791 state.resolvedOffer = data; 1792 if (data.available) { 1793 if (!L.in && data.check_in) L.in = data.check_in; 1794 if (!L.out && data.check_out) L.out = data.check_out; 1795 if (!L.room && data.room_category_id) L.room = data.room_category_id; 1796 applyLandingToForm(); 1797 pushMetrikaGoal("offer_landing_view"); 1798 if (data.offer_id) { 1799 pushEcommerce("detail", { 1800 id: data.offer_id, 1801 name: data.category_name || "offer", 1802 price: data.price || 0, 1803 quantity: 1, 1804 }); 1805 } 1806 runSearch(); 1807 } else { 1808 pushMetrikaGoal("offer_unavailable"); 1809 showSearchMessage( 1810 "ÐÑбÑанное пÑедложение болÑÑе недоÑÑÑпно. ÐопÑобÑйÑе дÑÑгие даÑÑ Ð½Ð¸Ð¶Ðµ.", 1811 true, 1812 ); 1813 if (data.nearby_date_windows && data.nearby_date_windows.length) { 1814 renderOfferNearbyAlternatives(data.nearby_date_windows); 1815 } else if (L.in && L.out) { 1816 runSearch(); 1817 } 1818 } 1819 }) 1820 .catch(function () { 1821 applyLandingToForm(); 1822 if (L.in && L.out) runSearch(); 1823 }); 1824 } 1825 1826 fetchConfig() 1827 .then(function (cfg) { 1828 state.config = cfg; 1829 state.contactPhone = String((cfg && cfg.contact_phone) || "").trim(); 1830 state.maxUrl = String((cfg && cfg.max_url) || "").trim(); 1831 renderNearbyAction(); 1832 applyLandingToForm(); 1833 if ( 1834 state.landing && 1835 (state.landing.offer || (state.landing.in && state.landing.out)) 1836 ) { 1837 resolveOfferThenSearch(); 1838 } 1839 }) 1840 .catch(function () { 1841 setErr("Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ Ð½Ð°ÑÑÑойки виджеÑа"); 1842 }); 1843 } 1844 1845 function initHotOffers(opts) { 1846 var apiBase = (opts.apiBaseUrl || "").replace(/\/$/, ""); 1847 var orgId = opts.organizationId || ""; 1848 var token = opts.token || ""; 1849 var layoutOpt = String(opts.layout || "").toLowerCase();
1850 var root = typeof opts.mount === "string" ? qs(opts.mount) : opts.mount; 1851 if (!root) return; 1852 if (!apiBase || !orgId || !token) { 1853 root.innerHTML = ""; 1854 root.appendChild(el("p", { class: "booking-widget-error" }, ["ÐÑÐ¶Ð½Ñ apiBaseUrl, organizationId и token"])); 1855 return; 1856 } 1857 1858 root.innerHTML = ""; 1859 root.className = (root.className ? root.className + " " : "") + "booking-widget-root bw-hot-root"; 1860 1861 var state = { 1862 config: null, 1863 items: [], 1864 layout: layoutOpt === "vertical" || layoutOpt === "tiles" ? layoutOpt : "horizontal", 1865 contactPhone: "", 1866 maxUrl: "", 1867 photoClickUrl: "", 1868 selected: null, 1869 captchaToken: null, 1870 }; 1871 1872 function guestEmailRequired() { 1873 return !!(state.config && state.config.guest_email_required); 1874 } 1875 1876 function isValidGuestEmail(s) { 1877 var t = String(s || "").trim(); 1878 return t.length > 3 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(t); 1879 } 1880 1881 var style = el("style", {}, []); 1882 style.textContent = 1883 ".bw-hot-root{font-family:system-ui,-apple-system,sans-serif;font-size:15px;line-height:1.45;color:#0f172a;width:100%;box-sizing:border-box}" + 1884 ".bw-hot-root *{box-sizing:border-box}" + 1885 ".bw-hot-title{margin:0 0 14px;font-size:22px;font-weight:700;color:#0f172a}" + 1886 ".bw-hot-msg{margin:8px 0;padding:12px 14px;border-radius:12px;border:1px solid #e2e8f0;background:#f8fafc;color:#334155;font-size:14px}" + 1887 ".bw-hot-msg--err{border-color:#fecaca;background:#fef2f2;color:#991b1b}" + 1888 ".bw-hot-track{display:flex;gap:14px;overflow-x:auto;padding:6px 12px 16px;scroll-snap-type:x mandatory;scroll-padding-inline:12px;-webkit-overflow-scrolling:touch}" + 1889 ".bw-hot-track--vertical{flex-direction:column;overflow-x:visible;overflow-y:auto;max-height:960px;scroll-snap-type:y mandatory;padding:0;width:100%;gap:12px}" + 1890 ".bw-hot-track--tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(320px,1fr));gap:16px;overflow:visible;scroll-snap-type:none;padding:6px 0 12px}" + 1891 ".bw-hot-card{position:relative;flex:0 0 min(420px,calc(100% - 40px));width:min(420px,calc(100% - 40px));scroll-snap-align:start;scroll-snap-stop:always;border:1px solid #e2e8f0;border-radius:16px;background:#fff;box-shadow:0 3px 12px rgba(15,23,42,.1);overflow:hidden;display:flex;flex-direction:column}" + 1892 ".bw-hot-track--vertical .bw-hot-card{flex:0 0 auto;width:100%;max-width:none}" + 1893 ".bw-hot-track--vertical .bw-hot-photo{min-height:0;aspect-ratio:4/3}" + 1894 ".bw-hot-track--vertical .bw-hot-actions{flex-direction:column}" + 1895 ".bw-hot-track--vertical .bw-hot-cta{flex:1 1 auto;width:100%}" + 1896 ".bw-hot-track--tiles .bw-hot-card{flex:initial;width:auto}" + 1897 ".bw-hot-photo{position:relative;aspect-ratio:4/3;min-height:210px;background:#f1f5f9;overflow:hidden}" + 1898 "a.bw-hot-photo{display:block;color:inherit;text-decoration:none;cursor:pointer}" + 1899 ".bw-hot-photo img{width:100%;height:100%;object-fit:cover;display:block}" + 1900 ".bw-hot-photo-overlay{position:absolute;inset:0;padding:16px;background:linear-gradient(180deg,rgba(15,23,42,.72),rgba(15,23,42,.9));color:#fff;opacity:0;pointer-events:none;transition:opacity .18s ease;overflow:auto;z-index:3}" + 1901 ".bw-hot-photo:hover .bw-hot-photo-overlay,.bw-hot-photo:focus-within .bw-hot-photo-overlay,.bw-hot-photo.bw-hot-photo--open .bw-hot-photo-overlay{opacity:1;pointer-events:auto}" + 1902 ".bw-hot-ov-title{font-size:16px;font-weight:700;margin:0 0 10px}" + 1903 ".bw-hot-ov-facts{display:flex;flex-wrap:wrap;gap:8px;margin:0 0 10px}" + 1904 ".bw-hot-ov-chip{font-size:13px;background:rgba(255,255,255,.16);border:1px solid rgba(255,255,255,.22);border-radius:999px;padding:4px 10px}" + 1905 ".bw-hot-ov-desc{font-size:14px;line-height:1.4;margin:0 0 10px;opacity:.95;display:-webkit-box;-webkit-line-clamp:8;-webkit-box-orient:vertical;overflow:hidden}" + 1906 ".bw-hot-ov-tags{display:flex;flex-wrap:wrap;gap:6px}" + 1907 ".bw-hot-ov-tag{font-size:12px;background:rgba(2,132,199,.35);border-radius:8px;padding:3px 9px}" + 1908 ".bw-hot-badge{position:absolute;top:12px;left:12px;z-index:2;background:linear-gradient(135deg,#ef4444,#f97316);color:#fff;font-weight:800;font-size:18px;padding:8px 12px;border-radius:999px;box-shadow:0 4px 12px rgba(239,68,68,.45);letter-spacing:.02em}" + 1909 ".bw-hot-body{padding:16px;display:flex;flex-direction:column;gap:8px;flex:1}" + 1910 ".bw-hot-name{font-size:18px;font-weight:650;margin:0;color:#0f172a}" + 1911 ".bw-hot-meta{font-size:14px;color:#475569;margin:0}" + 1912 ".bw-hot-dates{font-size:14px;color:#64748b;margin:0}" + 1913 ".bw-hot-prices{display:flex;flex-wrap:wrap;align-items:baseline;gap:10px;margin-top:4px}" + 1914 ".bw-hot-old{font-size:15px;color:#94a3b8;text-decoration:line-through}" + 1915 ".bw-hot-new{font-size:26px;font-weight:800;color:#dc2626}" + 1916 ".bw-hot-new small{font-size:13px;font-weight:600;color:#b91c1c;margin-left:2px}" + 1917 ".bw-hot-actions{margin-top:auto;display:flex;flex-wrap:wrap;gap:8px;align-items:stretch}" + 1918 ".bw-hot-cta{flex:1 1 120px;background:#0284c7;color:#fff;border:1px solid #0284c7;border-radius:10px;padding:10px 12px;cursor:pointer;font-weight:600;font-size:14px;text-align:center;text-decoration:none;display:inline-flex;align-items:center;justify-content:center}" + 1919 ".bw-hot-cta:hover{filter:brightness(1.05)}" + 1920 ".bw-hot-cta:disabled{opacity:.55;cursor:not-allowed}" + 1921 ".bw-hot-cta--secondary{background:#fff;color:#0f172a;border-color:#cbd5e1;font-weight:600}" + 1922 ".bw-hot-cta--secondary:hover{background:#f8fafc;filter:none}" + 1923 ".bw-hot-cta--max{background:#1a1a2e;color:#fff;border-color:#1a1a2e}" + 1924 ".bw-hot-cta--max:hover{filter:brightness(1.15)}" + 1925 "@media (max-width:640px){" + 1926 ".bw-hot-track:not(.bw-hot-track--vertical):not(.bw-hot-track--tiles){gap:12px;padding:4px 10px 18px;scroll-padding-inline:10px}" + 1927 ".bw-hot-track:not(.bw-hot-track--vertical):not(.bw-hot-track--tiles) .bw-hot-card{flex:0 0 calc(100% - 28px);width:calc(100% - 28px);max-width:none}" + 1928 ".bw-hot-photo{min-height:240px}" + 1929 ".bw-hot-ov-desc{-webkit-line-clamp:10}" + 1930 "}" + 1931 ".bw-hot-root .bw-req-overlay{position:fixed;inset:0;background:rgba(15,23,42,.55);z-index:99998;display:flex;align-items:center;justify-content:center;padding:16px}" + 1932 ".bw-hot-root .bw-req-modal{width:min(560px,96vw);max-height:88vh;overflow:auto;background:#fff;border-radius:14px;padding:16px;box-shadow:0 14px 40px rgba(2,6,23,.35);position:relative}" + 1933 ".bw-hot-root .bw-req-close{position:absolute;top:8px;right:8px;width:36px;height:36px;border-radius:999px;border:1px solid #cbd5e1;background:#fff;cursor:pointer;font-size:22px;line-height:1}" + 1934 ".bw-hot-root .row{display:flex;flex-wrap:wrap;gap:10px;align-items:flex-end;margin-bottom:12px}" +
1935 ".bw-hot-root .field{display:flex;flex-direction:column;gap:4px;flex:1 1 160px}" + 1936 ".bw-hot-root input,.bw-hot-root select,.bw-hot-root textarea,.bw-hot-root button{border-radius:10px;border:1px solid #cbd5e1;padding:10px 12px;font-size:15px}" + 1937 ".bw-hot-root button.primary{background:#0284c7;color:#fff;border-color:#0284c7;cursor:pointer}" + 1938 ".bw-hot-root button.primary:disabled{opacity:.55;cursor:not-allowed}" + 1939 ".bw-hot-root .bw-consent{display:flex;align-items:flex-start;gap:8px;margin:12px 0 4px;font-size:13px;color:#334155;cursor:pointer;line-height:1.35}" + 1940 ".bw-hot-root .bw-consent input{margin:2px 0 0;flex-shrink:0;width:16px;height:16px;accent-color:#0284c7}" + 1941 ".bw-hot-root .muted{color:#64748b;font-size:13px}" + 1942 ".bw-hot-root .bw-invalid{border-color:#ef4444 !important;box-shadow:0 0 0 2px rgba(239,68,68,.12)}" + 1943 ".bw-hot-root .bw-field-err{color:#b91c1c;font-size:13px}"; 1944 root.appendChild(style); 1945 1946 var title = el("h3", { class: "bw-hot-title" }, ["ÐоÑÑÑие пÑедложениÑ"]); 1947 root.appendChild(title); 1948 var msgBox = el("div", { class: "muted" }); 1949 root.appendChild(msgBox); 1950 var listHost = el("div", {}, []); 1951 root.appendChild(listHost); 1952 var reqHost = el("div", {}, []); 1953 root.appendChild(reqHost); 1954 1955 function setMsg(t, isErr) { 1956 msgBox.className = t ? "bw-hot-msg" + (isErr ? " bw-hot-msg--err" : "") : "muted"; 1957 msgBox.textContent = t || ""; 1958 } 1959 1960 function apiUrl(path, q) { 1961 var p = path.charAt(0) === "/" ? path : "/" + path; 1962 var u = new URL(apiBase + p); 1963 Object.keys(q || {}).forEach(function (k) { 1964 if (q[k] != null && q[k] !== "") u.searchParams.set(k, String(q[k])); 1965 }); 1966 return u.toString(); 1967 } 1968 1969 function widgetAssetUrl(path) { 1970 if (!path) return ""; 1971 if (/^https?:\/\//i.test(path)) { 1972 try { 1973 var u = new URL(path); 1974 if (u.pathname.indexOf("/api/v1/widget/") === 0) { 1975 u.searchParams.set("organization_id", orgId); 1976 u.searchParams.set("token", token); 1977 } 1978 return u.toString(); 1979 } catch (e) { 1980 return path; 1981 } 1982 } 1983 return apiUrl(path, { organization_id: orgId, token: token }); 1984 } 1985 1986 function formatMoney(cents) { 1987 var rub = Math.max(0, Math.round(Number(cents || 0) / 100)); 1988 return String(rub).replace(/\B(?=(\d{3})+(?!\d))/g, " ") + " â½"; 1989 } 1990 1991 function formatIsoRu(iso) { 1992 if (!iso) return ""; 1993 var p = String(iso).split("-"); 1994 if (p.length !== 3) return String(iso); 1995 return p[2] + "." + p[1] + "." + p[0]; 1996 } 1997 1998 function nightsWord(n) { 1999 var x = Math.abs(n) % 100; 2000 var y = x % 10; 2001 if (x > 10 && x < 20) return n + " ноÑей"; 2002 if (y === 1) return n + " ноÑÑ"; 2003 if (y >= 2 && y <= 4) return n + " ноÑи"; 2004 return n + " ноÑей"; 2005 } 2006 2007 function daysWord(n) { 2008 var x = Math.abs(n) % 100; 2009 var y = x % 10; 2010 if (x > 10 && x < 20) return n + " ÑÑÑок"; 2011 if (y === 1) return n + " ÑÑÑки"; 2012 return n + " ÑÑÑок"; 2013 } 2014 2015 function freeNightsLabel(item) { 2016 var nmin = parseInt(item.nights_min != null ? item.nights_min : item.nights, 10) || 0; 2017 var nmax = parseInt(item.nights_max != null ? item.nights_max : item.nights, 10) || nmin; 2018 if (nmin < 1) nmin = 1; 2019 if (nmax < nmin) nmax = nmin; 2020 if (nmin === nmax) return "Ñвободно " + daysWord(nmin); 2021 return "Ñвободно " + nmin + "â" + nmax + " ÑÑÑок"; 2022 } 2023 2024 function addDaysIso(iso, days) { 2025 var p = String(iso || "").split("-"); 2026 if (p.length !== 3) return iso; 2027 var d = new Date(Number(p[0]), Number(p[1]) - 1, Number(p[2])); 2028 d.setDate(d.getDate() + days); 2029 var y = d.getFullYear(); 2030 var m = String(d.getMonth() + 1).padStart(2, "0"); 2031 var dd = String(d.getDate()).padStart(2, "0"); 2032 return y + "-" + m + "-" + dd; 2033 } 2034 2035 function telHref(phone) { 2036 var digits = String(phone || "").replace(/[^\d+]/g, ""); 2037 if (!digits) return ""; 2038 return "tel:" + digits; 2039 } 2040 2041 function closeRequestModal() { 2042 reqHost.innerHTML = ""; 2043 state.selected = null; 2044 state.captchaToken = null; 2045 } 2046 2047 function openRequestModal(item) { 2048 state.selected = item; 2049 state.captchaToken = null; 2050 reqHost.innerHTML = "";
2051 var overlay = el("div", { 2052 class: "bw-req-overlay", 2053 onclick: function (ev) { 2054 if (ev.target === overlay) closeRequestModal(); 2055 }, 2056 }); 2057 var modal = el("div", { class: "bw-req-modal" }, []); 2058 modal.appendChild( 2059 el( 2060 "button", 2061 { 2062 type: "button", 2063 class: "bw-req-close", 2064 onclick: function () { 2065 closeRequestModal(); 2066 }, 2067 }, 2068 ["Ã"], 2069 ), 2070 ); 2071 modal.appendChild(el("h3", { style: "margin:0 0 8px;font-size:16px" }, ["ÐаÑвка на бÑониÑование"])); 2072 modal.appendChild( 2073 el("p", { class: "muted", style: "margin:0 0 10px" }, [ 2074 item.public_name + 2075 " · заезд " + 2076 formatIsoRu(item.check_in) + 2077 " · " + 2078 freeNightsLabel(item) + 2079 " · Ð¾Ñ " + 2080 formatMoney(item.price_cents) + 2081 " (â" + 2082 item.discount_percent + 2083 "%)", 2084 ]), 2085 ); 2086 var nmin = parseInt(item.nights_min != null ? item.nights_min : item.nights, 10) || 1; 2087 var nmax = parseInt(item.nights_max != null ? item.nights_max : item.nights, 10) || nmin; 2088 if (nmax < nmin) nmax = nmin; 2089 modal.appendChild( 2090 el("div", { class: "row" }, [ 2091 el("div", { class: "field" }, [ 2092 el("label", {}, ["СÑок пÑоживаниÑ"]), 2093 (function () { 2094 var s = el("select", { id: "bw-ho-nights" }); 2095 for (var n = nmin; n <= nmax; n++) { 2096 var opt = el("option", { value: String(n) }, [daysWord(n)]); 2097 if (n === nmin) opt.selected = true; 2098 s.appendChild(opt); 2099 } 2100 return s; 2101 })(), 2102 ]), 2103 ]), 2104 ); 2105 modal.appendChild( 2106 el("div", { class: "row" }, [ 2107 el("div", { class: "field" }, [ 2108 el("label", {}, ["Ð¤Ð°Ð¼Ð¸Ð»Ð¸Ñ *"]), 2109 el("input", { type: "text", id: "bw-ho-gs", autocomplete: "family-name" }), 2110 ]), 2111 el("div", { class: "field" }, [ 2112 el("label", {}, ["ÐÐ¼Ñ *"]), 2113 el("input", { type: "text", id: "bw-ho-gf", autocomplete: "given-name" }), 2114 ]), 2115 ]), 2116 ); 2117 modal.appendChild( 2118 el("div", { class: "row" }, [ 2119 el("div", { class: "field" }, [ 2120 el("label", {}, ["ТелеÑон *"]), 2121 el("input", { type: "tel", id: "bw-ho-ph", autocomplete: "tel" }), 2122 ]), 2123 el("div", { class: "field" }, [ 2124 el("label", {}, [guestEmailRequired() ? "Email *" : "Email"]), 2125 el("input", { 2126 type: "email", 2127 id: "bw-ho-em", 2128 autocomplete: "email", 2129 required: guestEmailRequired() ? true : undefined, 2130 }), 2131 ]), 2132 ]), 2133 ); 2134 modal.appendChild( 2135 el("div", { class: "row" }, [ 2136 el("div", { class: "field" }, [ 2137 el("label", {}, ["ÐзÑоÑлÑÑ "]), 2138 (function () { 2139 var s = el("select", { id: "bw-ho-ad" }); 2140 for (var a = 1; a <= 10; a++) s.appendChild(el("option", { value: String(a) }, [String(a)])); 2141 return s; 2142 })(), 2143 ]), 2144 el("div", { class: "field" }, [ 2145 el("label", {}, ["ÐеÑей"]), 2146 (function () { 2147 var s = el("select", { id: "bw-ho-ch" }); 2148 for (var c = 0; c <= 10; c++) s.appendChild(el("option", { value: String(c) }, [String(c)])); 2149 return s; 2150 })(), 2151 ]), 2152 ]), 2153 ); 2154 modal.appendChild( 2155 el("div", { class: "field", style: "margin-bottom:10px" }, [ 2156 el("label", {}, ["ÐомменÑаÑий"]), 2157 el("textarea", { id: "bw-ho-no", rows: "2" }), 2158 ]), 2159 );
2160 modal.appendChild(el("input", { type: "text", name: "website", tabindex: "-1", autocomplete: "off", style: "position:absolute;left:-9999px;opacity:0", "aria-hidden": "true" })); 2161 var capDiv = el("div", { id: "bw-ho-cap", style: "margin-bottom:10px" }); 2162 modal.appendChild(capDiv); 2163 var errLine = el("p", { class: "bw-field-err", id: "bw-ho-err" }); 2164 modal.appendChild(errLine); 2165 appendPdConsentAndSubmit(modal, { 2166 consentId: "bw-ho-pd-consent", 2167 termsId: "bw-ho-guest-terms", 2168 submitId: "bw-ho-submit", 2169 legalBaseUrl: opts.legalBaseUrl, 2170 onSubmit: function () { 2171 void submitHotOffer(); 2172 }, 2173 }); 2174 overlay.appendChild(modal); 2175 reqHost.appendChild(overlay); 2176 2177 if ( 2178 state.config && 2179 state.config.captcha_enabled && 2180 String(state.config.captcha_provider || "altcha").toLowerCase() === "altcha" 2181 ) { 2182 var tokenPart = ""; 2183 var qText = el("div", { class: "muted" }, ["ÐÑовеÑка..."]); 2184 var ans = el("input", { type: "text", inputmode: "numeric", placeholder: "ÐÑвеÑ", style: "max-width:120px" }); 2185 ans.addEventListener("input", function () { 2186 var v = String(ans.value || "").trim(); 2187 state.captchaToken = tokenPart && v ? tokenPart + ":" + v : null; 2188 }); 2189 capDiv.appendChild(el("div", { class: "row", style: "align-items:center" }, [qText, ans])); 2190 fetch(apiUrl("/api/v1/widget/captcha/challenge", { organization_id: orgId, token: token }), { credentials: "omit" }) 2191 .then(function (r) { 2192 if (!r.ok) throw new Error("ÐапÑа недоÑÑÑпна"); 2193 return r.json(); 2194 }) 2195 .then(function (j) { 2196 tokenPart = String((j && j.token) || ""); 2197 qText.textContent = String((j && j.question) || "РеÑиÑе пÑимеÑ"); 2198 state.captchaToken = null; 2199 }) 2200 .catch(function () { 2201 qText.textContent = "Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ ÐºÐ°Ð¿ÑÑ"; 2202 state.captchaToken = null; 2203 }); 2204 } 2205 } 2206 2207 function submitHotOffer() { 2208 var item = state.selected; 2209 if (!item) return; 2210 var gs = (qs("#bw-ho-gs", reqHost) && qs("#bw-ho-gs", reqHost).value) || ""; 2211 var gf = (qs("#bw-ho-gf", reqHost) && qs("#bw-ho-gf", reqHost).value) || ""; 2212 var ph = (qs("#bw-ho-ph", reqHost) && qs("#bw-ho-ph", reqHost).value) || ""; 2213 var em = (qs("#bw-ho-em", reqHost) && qs("#bw-ho-em", reqHost).value) || ""; 2214 var no = (qs("#bw-ho-no", reqHost) && qs("#bw-ho-no", reqHost).value) || ""; 2215 var ad = parseInt((qs("#bw-ho-ad", reqHost) && qs("#bw-ho-ad", reqHost).value) || "1", 10) || 1; 2216 var ch = parseInt((qs("#bw-ho-ch", reqHost) && qs("#bw-ho-ch", reqHost).value) || "0", 10) || 0; 2217 var nmin = parseInt(item.nights_min != null ? item.nights_min : item.nights, 10) || 1; 2218 var nmax = parseInt(item.nights_max != null ? item.nights_max : item.nights, 10) || nmin; 2219 var nightsSel = parseInt((qs("#bw-ho-nights", reqHost) && qs("#bw-ho-nights", reqHost).value) || String(nmin), 10) || nmin; 2220 if (nightsSel < nmin) nightsSel = nmin; 2221 if (nightsSel > nmax) nightsSel = nmax; 2222 var website = 2223 (qs('input[name="website"]', reqHost) && qs('input[name="website"]', reqHost).value) || ""; 2224 var errEl = qs("#bw-ho-err", reqHost); 2225 function setErr(t) { 2226 if (errEl) errEl.textContent = t || ""; 2227 } 2228 setErr(""); 2229 if (!gs.trim() || !gf.trim() || !ph.trim()) { 2230 setErr("ÐаполниÑе ÑамилиÑ, Ð¸Ð¼Ñ Ð¸ ÑелеÑон"); 2231 return; 2232 } 2233 if (guestEmailRequired() && !isValidGuestEmail(em)) { 2234 setErr("УкажиÑе коÑÑекÑнÑй email"); 2235 return; 2236 } 2237 var consent = qs("#bw-ho-pd-consent", reqHost); 2238 var terms = qs("#bw-ho-guest-terms", reqHost); 2239 if (!consent || !consent.checked || !terms || !terms.checked) { 2240 setErr("ÐÑимиÑе ÑоглаÑение модÑÐ»Ñ Ð¸ дайÑе ÑоглаÑие на обÑабоÑÐºÑ Ð¿ÐµÑÑоналÑнÑÑ Ð´Ð°Ð½Ð½ÑÑ "); 2241 return; 2242 } 2243 if ( 2244 state.config && 2245 state.config.captcha_enabled && 2246 String(state.config.captcha_provider || "altcha").toLowerCase() !== "none" && 2247 !state.captchaToken 2248 ) { 2249 setErr("ÐÑойдиÑе пÑовеÑÐºÑ ÐºÐ°Ð¿Ñи"); 2250 return; 2251 } 2252 var body = { 2253 check_in: item.check_in, 2254 check_out: addDaysIso(item.check_in, nightsSel), 2255 adults: ad, 2256 children: ch, 2257 guest_surname: gs.trim(), 2258 guest_first_name: gf.trim(), 2259 guest_name: (gs.trim() + " " + gf.trim()).trim(), 2260 phone: ph.trim(), 2261 email: em.trim() || null, 2262 notes: no.trim() || null, 2263 captcha_token: state.captchaToken || null, 2264 website: website || null, 2265 hot_offer: true, 2266 pd_consent: true, 2267 guest_terms_accepted: true, 2268 }; 2269 if (item.kind === "room" && item.room_id) body.room_id = item.room_id; 2270 else if (item.room_category_id) body.room_category_id = item.room_category_id; 2271 else { 2272 setErr("Ðе вÑбÑан номеÑ"); 2273 return; 2274 } 2275
2276 var btn = qs("#bw-ho-submit", reqHost); 2277 if (btn) btn.disabled = true; 2278 fetch(apiUrl("/api/v1/widget/requests", { organization_id: orgId, token: token }), { 2279 method: "POST", 2280 credentials: "omit", 2281 headers: { "Content-Type": "application/json" }, 2282 body: JSON.stringify(body), 2283 }) 2284 .then(function (r) { 2285 if (r.status === 429) throw new Error("СлиÑком много запÑоÑов или дÑÐ±Ð»Ð¸ÐºÐ°Ñ Ð·Ð°Ñвки"); 2286 if (!r.ok) 2287 return r.json().then(function (j) { 2288 var d = j.detail; 2289 var msg = typeof d === "string" ? d : d && d[0] && d[0].msg ? d[0].msg : "ÐÑибка"; 2290 throw new Error(msg); 2291 }); 2292 return r.json(); 2293 }) 2294 .then(function () { 2295 closeRequestModal(); 2296 setMsg("ÐаÑвка оÑпÑавлена. ÐÑ ÑвÑжемÑÑ Ñ Ð²Ð°Ð¼Ð¸.", false); 2297 }) 2298 .catch(function (e) { 2299 setErr(e.message || "ÐÑибка"); 2300 }) 2301 .finally(function () { 2302 syncSubmitWithPdConsent( 2303 qs("#bw-ho-submit", reqHost), 2304 qs("#bw-ho-pd-consent", reqHost), 2305 qs("#bw-ho-guest-terms", reqHost), 2306 ); 2307 }); 2308 } 2309 2310 function guestsLabel(item) { 2311 var cap = item.max_guests != null ? item.max_guests : item.bed_count; 2312 if (cap == null || cap === "" || isNaN(Number(cap)) || Number(cap) < 1) return ""; 2313 var n = Number(cap); 2314 var x = Math.abs(n) % 100; 2315 var y = x % 10; 2316 if (n % 10 === 1 && n % 100 !== 11) return "до " + n + " гоÑÑÑ"; 2317 if (y >= 2 && y <= 4 && !(x >= 12 && x <= 14)) return "до " + n + " гоÑÑей"; 2318 return "до " + n + " гоÑÑей"; 2319 } 2320 2321 function formatArea(area) { 2322 if (area == null || area === "" || isNaN(Number(area))) return ""; 2323 var v = Math.round(Number(area) * 10) / 10; 2324 return String(v).replace(".", ",") + " м²"; 2325 } 2326 2327 function buildPhotoOverlay(item) { 2328 var overlay = el("div", { class: "bw-hot-photo-overlay", tabindex: "0" }, []); 2329 overlay.appendChild(el("p", { class: "bw-hot-ov-title" }, [item.public_name || "ÐомеÑ"])); 2330 var facts = el("div", { class: "bw-hot-ov-facts" }, []); 2331 var guests = guestsLabel(item); 2332 if (guests) facts.appendChild(el("span", { class: "bw-hot-ov-chip" }, [guests])); 2333 var area = formatArea(item.area_sqm); 2334 if (area) facts.appendChild(el("span", { class: "bw-hot-ov-chip" }, [area])); 2335 if (item.floor != null && item.floor !== "") 2336 facts.appendChild(el("span", { class: "bw-hot-ov-chip" }, ["ÑÑаж " + item.floor])); 2337 if (facts.childNodes.length) overlay.appendChild(facts); 2338 var desc = String(item.description || "").trim(); 2339 if (desc) overlay.appendChild(el("p", { class: "bw-hot-ov-desc" }, [desc])); 2340 var tags = item.amenity_highlights; 2341 if (tags && tags.length) { 2342 var wrap = el("div", { class: "bw-hot-ov-tags" }, []); 2343 tags.slice(0, 8).forEach(function (t) { 2344 wrap.appendChild(el("span", { class: "bw-hot-ov-tag" }, [String(t)])); 2345 }); 2346 overlay.appendChild(wrap); 2347 } 2348 if (!facts.childNodes.length && !desc && !(tags && tags.length)) { 2349 overlay.appendChild(el("p", { class: "bw-hot-ov-desc" }, ["ÐодÑобноÑÑи ÑÑоÑнÑйÑе Ñ Ð°Ð´Ð¼Ð¸Ð½Ð¸ÑÑÑаÑоÑа"])); 2350 } 2351 return overlay; 2352 } 2353 2354 function renderCards() { 2355 listHost.innerHTML = ""; 2356 if (!state.items.length) { 2357 listHost.appendChild(el("div", { class: "bw-hot-msg" }, ["СейÑÐ°Ñ Ð½ÐµÑ Ð³Ð¾ÑÑÑÐ¸Ñ Ð¿Ñедложений."])); 2358 return; 2359 } 2360 var trackClass = "bw-hot-track"; 2361 if (state.layout === "vertical") trackClass += " bw-hot-track--vertical"; 2362 else if (state.layout === "tiles") trackClass += " bw-hot-track--tiles"; 2363 var track = el("div", { class: trackClass }, []); 2364 state.items.forEach(function (item) { 2365 var card = el("article", { class: "bw-hot-card" }, []); 2366 card.appendChild(el("div", { class: "bw-hot-badge" }, ["â" + item.discount_percent + "%"])); 2367 var photoAttrs = { class: "bw-hot-photo" }; 2368 if (state.photoClickUrl) { 2369 photoAttrs.href = state.photoClickUrl; 2370 } 2371 var photo = el(state.photoClickUrl ? "a" : "div", photoAttrs, []); 2372 var media0 = (item.media_urls && item.media_urls[0]) || ""; 2373 if (media0) { 2374 photo.appendChild(el("img", { src: widgetAssetUrl(media0), alt: item.public_name || "" })); 2375 } 2376 photo.appendChild(buildPhotoOverlay(item)); 2377 if (!state.photoClickUrl) { 2378 photo.addEventListener("click", function (ev) { 2379 // Ðа ÑаÑ-ÑÑÑÑойÑÑÐ²Ð°Ñ Ð±ÐµÐ· hover â показаÑÑ/ÑкÑÑÑÑ Ð¾Ð¿Ð¸Ñание Ñапом по ÑоÑо 2380 if (ev.target && ev.target.closest && ev.target.closest("a,button")) return; 2381 var open = photo.classList.contains("bw-hot-photo--open"); 2382 var opened = root.querySelectorAll(".bw-hot-photo--open"); 2383 for (var i = 0; i < opened.length; i++) opened[i].classList.remove("bw-hot-photo--open"); 2384 if (!open) photo.classList.add("bw-hot-photo--open"); 2385 }); 2386 } 2387 card.appendChild(photo); 2388 var body = el("div", { class: "bw-hot-body" }, []); 2389 body.appendChild(el("h4", { class: "bw-hot-name" }, [item.public_name || "ÐомеÑ"])); 2390 var guests = guestsLabel(item); 2391 if (guests) body.appendChild(el("p", { class: "bw-hot-meta" }, [guests])); 2392 body.appendChild( 2393 el("p", { class: "bw-hot-dates" }, [ 2394 "Ðаезд " + formatIsoRu(item.check_in) + " · " + freeNightsLabel(item),
2395 ]), 2396 ); 2397 body.appendChild( 2398 el("div", { class: "bw-hot-prices" }, [ 2399 el("span", { class: "bw-hot-old" }, [formatMoney(item.old_price_cents)]), 2400 el("span", { class: "bw-hot-new" }, [ 2401 formatMoney(item.price_cents), 2402 el("small", {}, [" · Ð¾Ñ " + nightsWord(item.nights_min || item.nights || 1)]), 2403 ]), 2404 ]), 2405 ); 2406 var actions = el("div", { class: "bw-hot-actions" }, []); 2407 actions.appendChild( 2408 el( 2409 "button", 2410 { 2411 type: "button", 2412 class: "bw-hot-cta", 2413 onclick: function () { 2414 openRequestModal(item); 2415 }, 2416 }, 2417 ["ÐабÑониÑоваÑÑ"], 2418 ), 2419 ); 2420 var tel = telHref(state.contactPhone); 2421 if (tel) { 2422 actions.appendChild( 2423 el("a", { class: "bw-hot-cta bw-hot-cta--secondary", href: tel }, ["ÐозвониÑÑ"]), 2424 ); 2425 } 2426 if (state.maxUrl) { 2427 actions.appendChild( 2428 el( 2429 "a", 2430 { 2431 class: "bw-hot-cta bw-hot-cta--max", 2432 href: state.maxUrl, 2433 target: "_blank", 2434 rel: "noopener noreferrer", 2435 }, 2436 ["ÐапиÑаÑÑ Ð² MAX"], 2437 ), 2438 ); 2439 } 2440 body.appendChild(actions); 2441 card.appendChild(body); 2442 track.appendChild(card); 2443 }); 2444 listHost.appendChild(track); 2445 } 2446 2447 setMsg("ÐагÑÑзкаâ¦", false); 2448 Promise.all([ 2449 fetch(apiUrl("/api/v1/widget/config", { organization_id: orgId, token: token }), { credentials: "omit" }).then( 2450 function (r) { 2451 if (!r.ok) throw new Error("config"); 2452 return r.json(); 2453 }, 2454 ), 2455 fetch(apiUrl("/api/v1/widget/hot-offers", { organization_id: orgId, token: token }), { credentials: "omit" }).then( 2456 function (r) { 2457 if (!r.ok) throw new Error("hot-offers"); 2458 return r.json(); 2459 }, 2460 ), 2461 ]) 2462 .then(function (pair) { 2463 state.config = pair[0]; 2464 var data = pair[1] || {}; 2465 state.items = Array.isArray(data.items) ? data.items : []; 2466 state.contactPhone = String( 2467 data.contact_phone || (pair[0] && pair[0].contact_phone) || "", 2468 ).trim(); 2469 state.maxUrl = String(data.max_url || (pair[0] && pair[0].max_url) || "").trim(); 2470 state.photoClickUrl = String(data.photo_click_url || "").trim(); 2471 if (!layoutOpt && data.default_layout) { 2472 var dl = String(data.default_layout || "").toLowerCase(); 2473 if (dl === "vertical" || dl === "tiles" || dl === "horizontal") state.layout = dl; 2474 } 2475 setMsg("", false); 2476 renderCards(); 2477 }) 2478 .catch(function () { 2479 setMsg("Ðе ÑдалоÑÑ Ð·Ð°Ð³ÑÑзиÑÑ Ð³Ð¾ÑÑÑие пÑедложениÑ", true); 2480 }); 2481 } 2482 2483 global.BookingWidget = { init: init }; 2484})(typeof window !== "undefined" ? window : this);
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.