https://www.junipersardinia.com/wp-content/themes/juniper-club/assets/js/jnp-ticket-sheet.js?ver=1.0
1/** 2 * jnp-ticket-sheet.js 3 * Standalone ticket purchase bottom sheet â same UX as the PR microsite. 4 * Triggered by elements with class .jnp-ticket-picker-btn 5 * Required data attributes: data-event-id, data-occurrence-ts, data-event-title 6 * Optional: data-thumbnail, data-age, data-end-time 7 * 8 * Configure via window.jnpSheetConfig before this script loads: 9 * window.jnpSheetConfig = { checkout_url, api_tickets, api_session, site_url, success_base } 10 */ 11(function(){ 12 13// Config injected by wp_localize_script or jnpSheetConfig 14var cfg = window.jnpSheetConfig || {}; 15var EVENTS = []; // not used in standalone mode 16var REF = ''; 17var REF_SLUG = ''; 18var CHECKOUT = cfg.checkout_url || 'https://www.junipersardinia.com/checkout/'; 19var API_TICKETS = cfg.api_tickets || 'https://www.junipersardinia.com/wp-json/jnp-tickets/v1/list'; 20var API_SESSION = cfg.api_session || 'https://www.junipersardinia.com/wp-json/jnp-stripe/v1/create-session'; 21var API_BASE = cfg.api_base || 'https://www.junipersardinia.com/wp-json'; 22var SITE_URL = cfg.site_url || 'https://www.junipersardinia.com'; 23var PR_BASE = cfg.success_base || ''; // Empty on consumer site â server uses thank-you page 24var otpState = { hasPass: false, verified: false, discount: 0, passType: '', passCode: '', sending: false }; 25 26/* ââ Attribution cookie âââ */ 27if (REF) { 28 var exp = new Date(); 29 exp.setDate(exp.getDate() + 30); // 30 days 30 document.cookie = 'jnp_ref=' + encodeURIComponent(REF) 31 + '; expires=' + exp.toUTCString() 32 + '; path=/; SameSite=Lax' 33 + (location.protocol === 'https:' ? '; Secure' : ''); 34} 35 36/* ââ Check for ?order_id= param on page load (Stripe return on main site) âââ */ 37(function() { 38 var params = new URLSearchParams(location.search); 39 var orderId = params.get('order_id'); 40 if (orderId && params.get('jnp_confirm') === '1') { 41 msShowConfirmation(orderId); 42 } 43})(); 44 45/* ââ OTP pass verification ââââââââââââââââââââââââââââââââââââââââââââââââ */ 46function msOtpSetCtaLocked(locked) { 47 // kept as no-op for legacy call sites â use msCheckFormReady instead 48} 49 50function msCheckFormReady() { 51 var firstName = (document.getElementById('msFirstName') || {}).value || ''; 52 var lastName = (document.getElementById('msLastName') || {}).value || ''; 53 var email = (document.getElementById('msEmail') || {}).value || ''; 54 var btn = document.getElementById('msFormSubmitBtn'); 55 if (!btn) return; 56 57 var emailTrim = email.trim(); 58 var emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(emailTrim); 59 var fieldsOk = firstName.trim() && lastName.trim() && emailValid; 60 61 // Show/hide email format hint 62 var emailEl = document.getElementById('msEmail'); 63 var hint = document.getElementById('msEmailHint'); 64 if (hint) { 65 hint.style.display = (emailTrim && !emailValid) ? '' : 'none'; 66 } 67 68 // DOB validation â only when the field is visible 69 var dobGroup = document.getElementById('msDobGroup'); 70 var dobOk = true; 71 if (dobGroup && dobGroup.style.display !== 'none') { 72 var dobVal = (document.getElementById('msDob') || {}).value || ''; 73 var dobHint = document.getElementById('msDobHint'); 74 var ageLimit = activeEv ? (activeEv._age_limit || 0) : 0; 75 if (!dobVal) { 76 dobOk = false; 77 if (dobHint) { dobHint.textContent = ''; dobHint.style.display = 'none'; } 78 } else { 79 var dob = new Date(dobVal); 80 var today = new Date(); 81 var age = today.getFullYear() - dob.getFullYear(); 82 var m = today.getMonth() - dob.getMonth(); 83 if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) age--; 84 if (dob >= today || age > 120) { 85 dobOk = false; 86 if (dobHint) { dobHint.textContent = 'Data non valida.'; dobHint.style.display = ''; dobHint.style.color = 'var(--text-danger, #ef4444)'; } 87 } else if (ageLimit && age < ageLimit) { 88 dobOk = false; 89 if (dobHint) { dobHint.textContent = 'Devi avere almeno ' + ageLimit + ' anni per acquistare questo biglietto.'; dobHint.style.display = ''; dobHint.style.color = 'var(--text-danger, #ef4444)'; } 90 } else { 91 dobOk = true; 92 if (dobHint) { dobHint.textContent = ''; dobHint.style.display = 'none'; } 93 } 94 } 95 } 96 97 if (!fieldsOk || !dobOk) { btn.disabled = true; return; } 98 99 // OTP check in progress â keep disabled 100 if (otpState.sending) { btn.disabled = true; return; } 101 102 // Pass found but not yet verified â keep disabled 103 if (otpState.hasPass && !otpState.verified) { btn.disabled = true; return; } 104 105 // Email changed since last check â keep disabled until new check completes 106 if (emailValid && otpState._checkedEmail !== emailTrim) { btn.disabled = true; return; } 107 108 // Valid email but no check started yet 109 if (emailValid && !otpState._checkedEmail) { btn.disabled = true; return; } 110 111 // All good â enable 112 btn.disabled = false;
113} 114 115function msOtpReset() { 116 var prevEmail = otpState._checkedEmail || ''; 117 otpState = { hasPass: false, verified: false, discount: 0, passType: '', passCode: '', sending: false, _checkedEmail: prevEmail }; 118 // Note: do NOT unlock CTA here â caller decides whether to lock or unlock 119 var block = document.getElementById('msOtpBlock'); 120 if (block) block.style.display = 'none'; 121 var disc = document.getElementById('msOtpDiscountLine'); 122 if (disc) { disc.style.display = 'none'; disc.innerHTML = ''; } 123 var inp = document.getElementById('msOtpInput'); 124 if (inp) inp.value = ''; 125 msUpdateFormTotal(); 126} 127 128function msOtpCheckEmail(email) { 129 if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) return; 130 // Already verified â nothing to do 131 if (otpState.verified) return; 132 // Already sending or already checked this exact email â nothing to do 133 if (otpState.sending || otpState._checkedEmail === email) return; 134 // Set sending flag BEFORE msOtpReset so concurrent calls hit the guard above 135 otpState.sending = true; 136 otpState._checkedEmail = email; 137 msOtpReset(); // clears UI, preserves _checkedEmail and sending via reset 138 otpState.sending = true; 139 otpState._checkedEmail = email; 140 var spinner = document.getElementById('msOtpSpinner'); 141 if (spinner) spinner.style.display = ''; 142 var block = document.getElementById('msOtpBlock'); 143 var notice = document.getElementById('msOtpBanner'); 144 fetch(API_BASE + '/jnp-tickets/v1/pass-otp/send', { 145 method: 'POST', 146 headers: { 'Content-Type': 'application/json' }, 147 body: JSON.stringify({ email: email, event_id: activeEv ? activeEv.event_id : 0, occurrence_ts: activeEv ? activeEv.ts : 0 }), 148 }) 149 .then(function(r) { return r.json(); }) 150 .then(function(data) { 151 otpState.sending = false; 152 var spinner = document.getElementById('msOtpSpinner'); 153 if (spinner) spinner.style.display = 'none'; 154 msCheckFormReady(); 155 if (data.has_pass && data.discount > 0) { 156 otpState.hasPass = true; 157 otpState.discount = data.discount; 158 otpState.passType = data.pass_type; 159 if (notice) notice.innerHTML = 'ð Hai un Season Pass attivo! Abbiamo inviato un codice a <strong>' + esc(email) + '</strong>. Inseriscilo qui sotto per applicare lo sconto di <strong>' + fmt(data.discount) + '</strong> a biglietto.'; 160 if (block) block.style.display = ''; 161 // Show the input row â CTA stays locked until OTP verified 162 var row = document.getElementById('msOtpInputRow'); 163 if (row) row.style.display = ''; 164 var inp = document.getElementById('msOtpInput'); 165 if (inp) { inp.disabled = false; inp.value = ''; inp.placeholder = '000000'; inp.focus(); } 166 } else { 167 // No pass â unlock CTA immediately 168 msOtpSetCtaLocked(false); 169 } 170 }) 171 .catch(function() { 172 otpState.sending = false; 173 var spinner = document.getElementById('msOtpSpinner'); 174 if (spinner) spinner.style.display = 'none'; 175 msCheckFormReady(); 176 }); 177} 178 179function msOtpVerify(otp) { 180 var email = document.getElementById('msEmail') ? document.getElementById('msEmail').value.trim() : ''; 181 if (!email || !otp) return; 182 var inp = document.getElementById('msOtpInput'); 183 if (inp) inp.disabled = true; 184 fetch(API_BASE + '/jnp-tickets/v1/pass-otp/verify', { 185 method: 'POST', 186 headers: { 'Content-Type': 'application/json' }, 187 body: JSON.stringify({ email: email, otp: otp }), 188 }) 189 .then(function(r) { return r.json(); }) 190 .then(function(data) { 191 if (data.valid) { 192 otpState.verified = true; 193 otpState.passCode = data.pass_code; 194 otpState.passType = data.pass_type; 195 otpState.discount = data.discount; 196 // Hide OTP input row, show discount line 197 var row = document.getElementById('msOtpInputRow'); 198 if (row) row.style.display = 'none'; 199 var disc = document.getElementById('msOtpDiscountLine'); 200 if (disc) { 201 disc.innerHTML = 'â Sconto Season Pass applicato: <strong>â' + fmt(data.discount) + '</strong> a biglietto'; 202 disc.style.display = ''; 203 } 204 msCheckFormReady(); // OTP verified â re-evaluate button state 205 msUpdateFormTotal(); 206 } else { 207 if (inp) { inp.disabled = false; inp.value = ''; inp.placeholder = data.message || 'Codice non valido. Riprova.'; } 208 } 209 }) 210 .catch(function() { if (inp) { inp.disabled = false; } }); 211} 212 213window.msOtpResend = function(e) { 214 e.preventDefault(); 215 var email = document.getElementById('msEmail') ? document.getElementById('msEmail').value.trim() : ''; 216 otpState.sending = false;
217 msOtpReset(); 218 msOtpCheckEmail(email); 219}; 220 221function msUpdateFormTotal() { 222 if (!activeEv) return; 223 var tickets = activeEv._jnp_tickets || []; 224 var total = 0; 225 var discountApplied = false; // track whether the 1 discounted ticket has been counted 226 tickets.forEach(function(t) { 227 var q = ticketQty[t.id] || 0; 228 var price = parseFloat(t.price) || 0; 229 if (otpState.verified && otpState.discount > 0 && q > 0 && !discountApplied) { 230 // Apply discount to exactly 1 ticket across all ticket types 231 total += Math.max(0, price - otpState.discount); // 1 discounted 232 total += price * (q - 1); // rest at full price 233 discountApplied = true; 234 } else { 235 total += price * q; 236 } 237 }); 238 // Update summary in form 239 var summary = document.getElementById('msFormSummary'); 240 if (summary && activeEv) { 241 var lines = ''; 242 var summaryDiscountApplied = false; 243 tickets.forEach(function(t) { 244 var q = ticketQty[t.id] || 0; 245 if (q > 0) { 246 var origPrice = parseFloat(t.price) || 0; 247 // Discount applies to first ticket of the first type only 248 var sub; 249 var thisRowDiscounted = false; 250 if (otpState.verified && otpState.discount > 0 && !summaryDiscountApplied) { 251 sub = Math.max(0, origPrice - otpState.discount) + origPrice * (q - 1); 252 summaryDiscountApplied = true; 253 thisRowDiscounted = true; 254 } else { 255 sub = origPrice * q; 256 } 257 lines += '<div class="ms-form-summary-row"><span>' + esc(t.name) + ' Ã ' + q + '</span>' 258 + '<span>' + (thisRowDiscounted ? '<s style="color:#71717a;font-size:12px;">' + fmt(q * origPrice) + '</s> ' : '') 259 + fmt(sub) + '</span></div>'; 260 } 261 }); 262 if (otpState.verified && otpState.discount > 0) { 263 lines += '<div class="ms-form-summary-row" style="color:#22c55e;font-size:12px;"><span>Sconto Season Pass (1 biglietto)</span><span>â' + fmt(otpState.discount) + '</span></div>'; 264 } 265 lines += '<div class="ms-form-summary-row ms-form-summary-total"><span>Totale</span><span>' + fmt(total) + '</span></div>'; 266 summary.innerHTML = lines; 267 } 268} 269 270 271var detailOverlay = document.getElementById('msDetailOverlay'); 272 273var DAY = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat']; 274var MON = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; 275 276/* ââ Show confirmation screen (called on ?order= param) âââ */ 277function msShowConfirmation(orderId) { 278 // Fetch order summary from API 279 fetch(SITE_URL + '/wp-json/jnp-sales/v1/list?order_id=' + orderId) 280 .then(function(r) { return r.ok ? r.json() : null; }) 281 .then(function(data) { 282 msRenderConfirmation(orderId, data); 283 }) 284 .catch(function() { 285 msRenderConfirmation(orderId, null); 286 }); 287} 288 289function msRenderConfirmation(orderId, data) { 290 var overlay = document.getElementById('msDetailOverlay'); 291 var sheet = overlay ? overlay.querySelector('.ms-sheet') : null; 292 if (!sheet) return; 293 294 // Replace sheet content with confirmation 295 sheet.innerHTML = 296 '<div class="ms-confirm-screen">' + 297 '<div class="ms-confirm-icon">ð</div>' + 298 '<h2 class="ms-confirm-title">Acquisto completato!</h2>' + 299 '<p class="ms-confirm-sub">I tuoi biglietti sono stati inviati via email.<br>Trovi i biglietti anche nella tua area personale su Juniper.</p>' + 300 '<div class="ms-confirm-summary" id="msConfirmSummary">' + 301 '<div class="ms-confirm-row"><span>Ordine</span><span>#' + orderId + '</span></div>' + 302 '</div>' + 303 '<button class="ms-sheet-cta-btn" style="width:100%;justify-content:center;gap:10px;" onclick="msCloseDetail()">' + 304 'Chiudi' + 305 '</button>' + 306 '</div>'; 307 308 overlay.classList.add('open'); 309 document.body.style.overflow = 'hidden'; 310 311 // Clean the URL so refreshing doesn't re-show confirmation 312 if (history.replaceState) { 313 history.replaceState({}, '', location.pathname); 314 } 315} 316 317/* ââ Open detail sheet âââ */ 318window.msOpenDetail = function(idx) { 319 var ev = EVENTS[idx]; if (!ev) return; 320 activeEv = ev; 321 ticketQty = {}; 322 sheetStep = 'tickets'; 323 324 var bg = document.getElementById('msSheetHeroBg'); 325 var img = document.getElementById('msSheetHeroImg'); 326 var ph = document.getElementById('msSheetHeroPh'); 327 if (ev.thumbnail) { 328 bg.style.backgroundImage = "url('" + ev.thumbnail + "')"; 329 img.style.backgroundImage = "url('" + ev.thumbnail + "')"; 330 ph.style.display = 'none'; 331 } else { 332 bg.style.backgroundImage = img.style.backgroundImage = ''; 333 ph.style.display = 'block'; 334 } 335 336 var d = new Date(ev.ts * 1000);
337 var endTime = ev.end_time ? ev.end_time.toUpperCase() : 'TILL LATE'; 338 document.getElementById('msSheetDate').innerHTML = 339 '<span class="ms-date-badge">' 340 + '<span class="ms-date-badge-day">' + esc(DAY[d.getDay()]) + ' </span>' 341 + '<span class="ms-date-badge-num">' + pad(d.getDate()) + '</span>' 342 + '<span class="ms-date-badge-mon"> ' + MON[d.getMonth()] + '</span>' 343 + '</span>' 344 + '<span class="ms-card-time">' 345 + esc(pad(d.getHours()) + ':' + pad(d.getMinutes()) + ' â ' + endTime) 346 + '</span>'; 347 document.getElementById('msSheetTitle').textContent = ev.label || ev.event_title; 348 349 msShowTicketsStep(); 350 351 var tSec = document.getElementById('msSheetTickets'); 352 var tRows = document.getElementById('msSheetTicketRows'); 353 tRows.innerHTML = '<p style="color:#52525b;font-size:13px;padding:12px 0;">Caricamentoâ¦</p>'; 354 tSec.style.display = ''; 355 document.getElementById('msSheetTables').style.display = 'none'; 356 document.getElementById('msSheetNoTickets').style.display = 'none'; 357 document.getElementById('msSheetCta').style.display = 'none'; 358 detailOverlay.classList.add('open'); 359 document.body.style.overflow = 'hidden'; 360 361 fetch(API_TICKETS + '?event_id=' + ev.event_id + '&occurrence_ts=' + ev.ts) 362 .then(function(r) { return r.ok ? r.json() : {}; }) 363 .then(function(resp) { 364 // Response is now { tickets, event_age_limit, user_dob } 365 // Fallback to plain array for backwards compatibility 366 var tickets = Array.isArray(resp) ? resp : (resp.tickets || []); 367 ev._jnp_tickets = tickets; 368 ev._age_limit = Array.isArray(resp) ? (ev.age ? parseInt(ev.age, 10) : null) : (resp.event_age_limit || null); 369 ev._user_dob = Array.isArray(resp) ? '' : (resp.user_dob || ''); 370 msRenderTicketRows(ev); 371 }) 372 .catch(function() { ev._jnp_tickets = []; msRenderTicketRows(ev); }); 373 374 var tabSec = document.getElementById('msSheetTables'); 375 var tabRows = document.getElementById('msSheetTableRows'); 376 if (ev.tables && ev.tables.length) { 377 tabRows.innerHTML = ev.tables.map(function(t) { 378 var out = t.sold_out || t.available === 0; 379 return '<div class="ms-ticket-row' + (out ? ' unavail' : '') + '">' 380 + '<div class="ms-ticket-info"><div class="ms-ticket-info-name">' + esc(t.name) + '</div>' 381 + (t.price ? '<div class="ms-ticket-info-price">Da ' + fmt(t.price) + '</div>' : '') + '</div>' 382 + (out ? '<span class="ms-badge-sold">Completo</span>' : '<span class="ms-item-arrow">âº</span>') 383 + '</div>'; 384 }).join(''); 385 tabSec.style.display = ''; 386 } else { tabSec.style.display = 'none'; } 387}; 388 389function msShowTicketsStep() { 390 sheetStep = 'tickets'; 391 document.getElementById('msSheetDate').style.display = ''; 392 document.getElementById('msSheetTitle').style.display = ''; 393 document.getElementById('msSheetInfo') && (document.getElementById('msSheetInfo').style.display = ''); 394 document.getElementById('msSheetTickets').style.display = ''; 395 document.getElementById('msFormStep').style.display = 'none'; 396 // CTA state restored by msUpdateCta 397} 398 399function msShowFormStep() { 400 // Button starts disabled â enabled only when all fields filled + OTP resolved 401 var btn = document.getElementById('msFormSubmitBtn'); 402 if (btn) btn.disabled = true; 403 404 sheetStep = 'form'; 405 // Hide ticket selection, show form 406 document.getElementById('msSheetTickets').style.display = 'none'; 407 document.getElementById('msSheetTables') && (document.getElementById('msSheetTables').style.display = 'none'); 408 document.getElementById('msSheetNoTickets').style.display = 'none'; 409 document.getElementById('msSheetDate').style.display = 'none'; 410 document.getElementById('msSheetTitle').style.display = 'none'; 411 document.getElementById('msSheetInfo') && (document.getElementById('msSheetInfo').style.display = 'none'); 412 document.getElementById('msFormStep').style.display = ''; 413 414 // Populate order summary in form 415 var tickets = activeEv ? (activeEv._jnp_tickets || []) : []; 416 var total = 0; 417 var lines = ''; 418 tickets.forEach(function(t) { 419 var q = ticketQty[t.id] || 0; 420 if (q > 0) { 421 var sub = q * (parseFloat(t.price) || 0); 422 total += sub; 423 lines += '<div class="ms-form-summary-row"><span>' + esc(t.name) + ' à ' + q + '</span><span>' + fmt(sub) + '</span></div>'; 424 } 425 }); 426 lines += '<div class="ms-form-summary-row ms-form-summary-total"><span>Totale</span><span>' + fmt(total) + '</span></div>'; 427 document.getElementById('msFormSummary').innerHTML = lines; 428 429 // Hide sticky CTA â form has its own submit button 430 document.getElementById('msSheetCta').style.display = 'none'; 431 432 // Bind field input listeners â every change re-evaluates button state 433 ['msFirstName','msLastName','msEmail'].forEach(function(id) { 434 var el = document.getElementById(id); 435 if (el && !el._readyBound) { 436 el._readyBound = true; 437 // Remove readonly on focus to allow typing (suppresses iOS autofill scanning) 438 el.addEventListener('focus', function() { this.removeAttribute('readonly'); }); 439 el.addEventListener('touchstart', function() { this.removeAttribute('readonly'); }); 440 ['input','change','blur','keyup'].forEach(function(ev) { 441 el.addEventListener(ev, msCheckFormReady); 442 }); 443 } 444 }); 445 446 // DOB field â show only when event has a minimum age requirement 447 var dobGroup = document.getElementById('msDobGroup'); 448 var dobInput = document.getElementById('msDob'); 449 var ageLimit = activeEv ? (activeEv._age_limit || 0) : 0; 450 if (dobGroup && ageLimit > 0) { 451 dobGroup.style.display = ''; 452 // Set max date to today (can't be born in the future) 453 var todayStr = new Date().toISOString().split('T')[0]; 454 if (dobInput) dobInput.max = todayStr; 455 // Update the "(minimum 18 anni)" label 456 var ageReqSpan = document.getElementById('msDobAgeReq'); 457 if (ageReqSpan) ageReqSpan.textContent = '(minimo ' + ageLimit + ' anni)'; 458 // Pre-fill from user meta if the server returned a stored DOB 459 var userDob = activeEv._user_dob || ''; 460 if (dobInput && userDob && !dobInput.value) { 461 dobInput.value = userDob; 462 } 463 // Bind change listener 464 if (dobInput && !dobInput._readyBound) { 465 dobInput._readyBound = true; 466 ['input','change','blur'].forEach(function(ev) { 467 dobInput.addEventListener(ev, msCheckFormReady); 468 }); 469 } 470 } else if (dobGroup) { 471 dobGroup.style.display = 'none'; 472 if (dobInput) dobInput.value = ''; 473 } 474 475 // OTP check on email â debounced so rapid events (input+change+blur) collapse into one call 476 var emailEl = document.getElementById('msEmail'); 477 if (emailEl && !emailEl._otpBound) { 478 emailEl._otpBound = true; 479 var _otpDebounceTimer = null; 480 var _otpEmailHandler = function() { 481 var val = this.value.trim(); 482 msCheckFormReady(); 483 clearTimeout(_otpDebounceTimer); 484 _otpDebounceTimer = setTimeout(function() { 485 if (/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(val)) { 486 msOtpCheckEmail(val); 487 } 488 }, 300); 489 }; 490 ['blur','change','input'].forEach(function(ev) { 491 emailEl.addEventListener(ev, _otpEmailHandler); 492 }); 493 } 494 495 // Scroll sheet to top 496 var sheet = detailOverlay.querySelector('.ms-sheet'); 497 if (sheet) sheet.scrollTop = 0; 498} 499 500/* Render ticket rows with inline steppers */ 501function msRenderTicketRows(ev) { 502 var tSec = document.getElementById('msSheetTickets'); 503 var tRows = document.getElementById('msSheetTicketRows'); 504 var noTick = document.getElementById('msSheetNoTickets'); 505 var tickets = ev._jnp_tickets || []; 506 507 if (tickets.length) { 508 tRows.innerHTML = tickets.map(function(t) { 509 var out = t.sold_out || t.available === 0 || t.locked; 510 var priceLabel = t.price ? fmt(t.price) : 'Gratuito'; 511 // max = available (if limited) or 10 (if unlimited, available === null), 512 // further capped by the ticket's max_per_order setting (if set) 513 var maxQty = (t.available !== null && t.available !== undefined) ? t.available : 10; 514 if (t.max_per_order !== null && t.max_per_order !== undefined) { 515 maxQty = Math.min(maxQty, t.max_per_order); 516 } 517 // Build combo badge and leg summary if applicable 518 var isCombo = t.combo_legs && t.combo_legs.length > 0; 519 var comboBadge = isCombo ? '<span class="ms-badge-combo">COMBO</span>' : ''; 520 var comboLegs = ''; 521 if (isCombo) { 522 var legParts = t.combo_legs.map(function(leg) { 523 var d = new Date(leg.occurrence_ts * 1000); 524 var day = ('0' + d.getDate()).slice(-2); 525 var months = ['Gen','Feb','Mar','Apr','Mag','Giu','Lug','Ago','Set','Ott','Nov','Dic']; 526 return leg.qty + 'à ' + esc(leg.title) + ' â ' + day + ' ' + months[d.getMonth()]; 527 }); 528 comboLegs = '<div class="ms-combo-legs">ð ' + legParts.join(' · ') + '</div>'; 529 } 530 531 if (out) { 532 var badgeLabel = t.locked ? 'Prossimamente' : (t.outside_window ? 'Non disponibile' : 'Esaurito'); 533 return '<div class="ms-ticket-row unavail">' 534 + '<div class="ms-ticket-info"><div class="ms-ticket-info-name">' + comboBadge + esc(t.name) + '</div>' 535 + '<div class="ms-ticket-info-price">' + priceLabel + '</div>' 536 + comboLegs + '</div>' 537 + (t.description ? '<button class="ms-info-btn" data-info-name="' + esc(t.name) + '" data-info-desc="' + esc(t.description) + '" aria-label="Info"><span>i</span></button>' : '') 538 + '<span class="ms-badge-sold">' + badgeLabel + '</span></div>'; 539 } 540 // Store maxQty as data attribute so msTicketStep can read it 541 return '<div class="ms-ticket-row" id="ms-trow-' + t.id + '" data-max="' + maxQty + '">' 542 + '<div class="ms-ticket-info"><div class="ms-ticket-info-name">' + comboBadge + esc(t.name) + '</div>' 543 + '<div class="ms-ticket-info-price">' + priceLabel + '</div>' 544 + comboLegs + '</div>' 545 + '<div class="ms-ticket-stepper">' 546 + (t.description ? '<button class="ms-info-btn" data-info-name="' + esc(t.name) + '" data-info-desc="' + esc(t.description) + '" aria-label="Info"><span>i</span></button>' : '') 547 + '<button class="ms-inline-btn" onclick="msTicketStep(' + t.id + ',-1,event)">â</button>' 548 + '<span class="ms-inline-qty" id="ms-qty-' + t.id + '">0</span>' 549 + '<button class="ms-inline-btn ms-inline-plus" onclick="msTicketStep(' + t.id + ',1,event)">+</button>' 550 + '</div></div>'; 551 }).join(''); 552 tSec.style.display = ''; 553 noTick.style.display = 'none'; 554 } else { 555 tRows.innerHTML = ''; 556 tSec.style.display = 'none'; 557 noTick.style.display = (ev.tables && ev.tables.length) ? 'none' : ''; 558 } 559} 560 561/* ââ Per-ticket stepper âââ */ 562window.msTicketStep = function(ticketId, delta, e) { 563 e && e.stopPropagation(); 564 // Read per-ticket max from the row's data-max attribute 565 var row = document.getElementById('ms-trow-' + ticketId); 566 var maxQty = row ? parseInt(row.getAttribute('data-max'), 10) : 10; 567 if (isNaN(maxQty)) maxQty = 10; 568 var current = ticketQty[ticketId] || 0; 569 var next = Math.max(0, Math.min(maxQty, current + delta)); 570 ticketQty[ticketId] = next; 571 var qtyEl = document.getElementById('ms-qty-' + ticketId); 572 if (qtyEl) qtyEl.textContent = next; 573 msUpdateCta(); 574}; 575 576function msUpdateCta() { 577 if (!activeEv) return; 578 var tickets = activeEv._jnp_tickets || []; 579 var total = 0, count = 0;
580 tickets.forEach(function(t) { 581 var q = ticketQty[t.id] || 0; 582 total += q * (parseFloat(t.price) || 0); 583 count += q; 584 }); 585 var cta = document.getElementById('msSheetCta'); 586 var ctaBtn = document.getElementById('msSheetCtaBtn'); 587 var ctaTot = document.getElementById('msSheetCtaTotal'); 588 var ctaQty = document.getElementById('msSheetCtaQty'); 589 if (count > 0 && sheetStep === 'tickets') { 590 cta.style.display = ''; 591 if (ctaTot) ctaTot.textContent = fmt(total); 592 if (ctaQty) ctaQty.textContent = count; 593 } else { 594 cta.style.display = 'none'; 595 } 596} 597 598window.msBackToTickets = function() { 599 msShowTicketsStep(); 600 msUpdateCta(); 601}; 602 603window.msCloseDetail = function(force) { 604 // If we're on the form step, ask for confirmation before closing 605 if (!force && sheetStep === 'form') { 606 if (!confirm('Sei sicuro di voler uscire? Perderai i dati inseriti.')) return; 607 } 608 detailOverlay.classList.remove('open'); 609 document.body.style.overflow = ''; 610 sheetStep = 'tickets'; 611}; 612detailOverlay && detailOverlay.addEventListener('click', function(e) { 613 // Backdrop click disabled â use X button to close 614}); 615 616/* ââ Submit â step 1: go to form; step 2: call API âââ */ 617window.msSubmit = function() { 618 if (sheetStep === 'tickets') { 619 msShowFormStep(); 620 return; 621 } 622 // Step 2: validate form and call Stripe session API 623 var firstName = document.getElementById('msFirstName').value.trim(); 624 var lastName = document.getElementById('msLastName').value.trim(); 625 var email = document.getElementById('msEmail').value.trim(); 626 var errEl = document.getElementById('msFormError'); 627 628 if (!firstName || !lastName || !email || !/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(email)) { 629 errEl.classList.add('visible'); 630 return; 631 } 632 errEl.classList.remove('visible'); 633 634 635 636 var tickets = activeEv ? (activeEv._jnp_tickets || []) : []; 637 var selected = {}; 638 var hasAny = false; 639 tickets.forEach(function(t) { 640 var q = ticketQty[t.id] || 0; 641 if (q > 0) { selected[t.id] = q; hasAny = true; } 642 }); 643 if (!hasAny) return; 644 645 var formBtn = document.getElementById('msFormSubmitBtn'); 646 if (formBtn) { formBtn.disabled = true; formBtn.textContent = 'Elaborazioneâ¦'; } 647 648 var dobVal = ''; 649 var dobInput = document.getElementById('msDob'); 650 var dobGroup = document.getElementById('msDobGroup'); 651 if (dobInput && dobGroup && dobGroup.style.display !== 'none') { 652 dobVal = dobInput.value.trim(); 653 } 654 655 // Success URL points back to microsite with order param 656 var payload = { 657 event_id: activeEv.event_id, 658 occurrence_ts: activeEv.ts, 659 selected_tickets: selected, 660 first_name: firstName, 661 last_name: lastName, 662 email: email, 663 ref: REF, 664 referer: location.href, 665 cancel_url: location.href, 666 }; 667 if (dobVal) payload.dob = dobVal; 668 // Include OTP-verified pass discount if present 669 if (otpState.verified && otpState.passCode && otpState.discount > 0) { 670 payload.pass_code = otpState.passCode; 671 payload.otp_discount = otpState.discount; 672 payload.otp_pass_type = otpState.passType; 673 } 674 // Only set success_url_base if we have a PR microsite base (e.g. pr.junipersardinia.com/giulia) 675 // Otherwise leave unset so the server uses the default thank-you page 676 if (PR_BASE) { payload.success_url_base = PR_BASE; } 677 678 fetch(API_SESSION, { 679 method: 'POST', 680 headers: { 'Content-Type': 'application/json' }, 681 body: JSON.stringify(payload), 682 }) 683 .then(function(r) { return r.json(); }) 684 .then(function(data) { 685 if (data.url) { 686 window.location.href = data.url; 687 } else { 688 var msg = data.message || 'Si è verificato un errore. Riprova.'; 689 var formBtn = document.getElementById('msFormSubmitBtn'); 690 if (formBtn) { formBtn.disabled = false; formBtn.textContent = 'Procedi al pagamento â'; } 691 document.getElementById('msFormError').textContent = msg; 692 document.getElementById('msFormError').classList.add('visible'); 693 } 694 }) 695 .catch(function() { 696 var formBtn = document.getElementById('msFormSubmitBtn'); 697 if (formBtn) { formBtn.disabled = false; formBtn.textContent = 'Procedi al pagamento â'; } 698 document.getElementById('msFormError').textContent = 'Errore di connessione. Riprova.'; 699 document.getElementById('msFormError').classList.add('visible'); 700 }); 701}; 702 703/* ââ Keyboard âââ */ 704document.addEventListener('keydown', function(e) { 705 if (e.key === 'Escape' && detailOverlay && detailOverlay.classList.contains('open')) { 706 msCloseDetail(); 707 } 708}); 709 710/* ââ Helpers âââ */ 711function pad(n) { return n < 10 ? '0' + n : '' + n; }
712function fmt(v) { return parseFloat(v).toLocaleString('it-IT', {minimumFractionDigits:2, maximumFractionDigits:2}) + ' â¬'; } 713function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } 714 715/* ââ Ticket info popup âââââââââââââââââââââââââââââââââââââââââââââââ */ 716window.msShowInfo = function msShowInfo(name, description, evt) { 717 if (evt) { evt.preventDefault(); evt.stopPropagation(); } 718 var existing = document.getElementById('msInfoPopup'); 719 if (existing) existing.remove(); 720 var popup = document.createElement('div'); 721 popup.id = 'msInfoPopup'; 722 popup.innerHTML = '<div class="ms-info-popup-backdrop" onclick="document.getElementById(\'msInfoPopup\').remove()"></div>' 723 + '<div class="ms-info-popup-box">' 724 + '<h4 class="ms-info-popup-title">' + esc(name) + '</h4>' 725 + '<div class="ms-info-popup-body">' + esc(description).replace(/\n/g,'<br>') + '</div>' 726 + '<button class="ms-info-popup-close" onclick="document.getElementById(\'msInfoPopup\').remove()">Chiudi</button>' 727 + '</div>'; 728 document.body.appendChild(popup); 729} 730 731 732/* ââ Public API âââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 733window.jnpOpenTicketSheet = function(eventId, occurrenceTs, eventTitle, thumbnail, age, endTime) { 734 // Close any open Bootstrap modal first 735 if (typeof jQuery !== 'undefined') { 736 jQuery('.modal.in').modal('hide'); 737 } 738 739 activeEv = { 740 event_id: eventId, 741 ts: occurrenceTs, 742 event_title: eventTitle || '', 743 label: eventTitle || '', 744 thumbnail: thumbnail || '', 745 age: age || '', 746 end_time: endTime || '', 747 _jnp_tickets: null, 748 }; 749 ticketQty = {}; 750 sheetStep = 'tickets'; 751 otpState = { hasPass: false, verified: false, discount: 0, passType: '', passCode: '', sending: false, _checkedEmail: '' }; 752 753 var overlay = document.getElementById('msDetailOverlay'); 754 if (!overlay) { buildSheetDOM(); overlay = document.getElementById('msDetailOverlay'); } 755 756 // Set hero 757 var bg = document.getElementById('msSheetHeroBg'); 758 var img = document.getElementById('msSheetHeroImg'); 759 var ph = document.getElementById('msSheetHeroPh'); 760 if (thumbnail) { 761 bg.style.backgroundImage = img.style.backgroundImage = "url('" + thumbnail + "')"; 762 ph.style.display = 'none'; 763 } else { 764 bg.style.backgroundImage = img.style.backgroundImage = ''; 765 ph.style.display = 'block'; 766 } 767 768 // Set title/date 769 var d = new Date(occurrenceTs * 1000); 770 var timeLabel = pad(d.getHours()) + ':' + pad(d.getMinutes()) + (endTime ? ' â ' + endTime : ''); 771 document.getElementById('msSheetDate').innerHTML = 772 '<span class="ms-date-badge"><span class="ms-date-badge-day">' + DAY[d.getDay()] + ' </span>' 773 + '<span class="ms-date-badge-num">' + pad(d.getDate()) + '</span>' 774 + '<span class="ms-date-badge-mon"> ' + MON[d.getMonth()] + '</span></span>' 775 + '<span class="ms-card-time">' + esc(timeLabel) + '</span>' 776 + (age ? '<span class="ms-card-age" style="margin-left:6px;padding:1px 7px;border-radius:10px;background:rgba(0,0,0,0.08);font-size:11px;font-weight:600;letter-spacing:.02em;">' + esc(age) + '+</span>' : ''); 777 document.getElementById('msSheetTitle').textContent = eventTitle || ''; 778 779 msShowTicketsStep(); 780 document.getElementById('msSheetTicketRows').innerHTML = '<p style="color:#52525b;font-size:13px;padding:12px 0;">Caricamentoâ¦</p>'; 781 document.getElementById('msSheetTickets').style.display = ''; 782 document.getElementById('msSheetTables').style.display = 'none'; 783 document.getElementById('msSheetNoTickets').style.display = 'none'; 784 document.getElementById('msSheetCta').style.display = 'none'; 785 overlay.classList.add('open'); 786 document.body.style.overflow = 'hidden'; 787 788 fetch(API_TICKETS + '?event_id=' + eventId + '&occurrence_ts=' + occurrenceTs) 789 .then(function(r) { return r.ok ? r.json() : {}; }) 790 .then(function(resp) { 791 var tickets = Array.isArray(resp) ? resp : (resp.tickets || []); 792 activeEv._jnp_tickets = tickets; 793 activeEv._age_limit = Array.isArray(resp) ? (age ? parseInt(age, 10) : null) : (resp.event_age_limit || null); 794 activeEv._user_dob = Array.isArray(resp) ? '' : (resp.user_dob || ''); 795 msRenderTicketRows(activeEv); 796 }) 797 .catch(function() { activeEv._jnp_tickets = []; msRenderTicketRows(activeEv); }); 798}; 799 800function buildSheetDOM() { 801 if (document.getElementById('msDetailOverlay')) return; 802 var el = document.createElement('div'); 803 el.id = 'msDetailOverlay'; 804 el.className = 'ms-overlay'; 805 el.innerHTML = [ 806 '<div class="ms-sheet">', 807 '<div class="ms-sheet-handle"></div>', 808 '<div class="ms-sheet-hero" id="msSheetHero">', 809 ' <div class="ms-sheet-hero-ph" id="msSheetHeroPh"></div>', 810 ' <div class="ms-sheet-hero-bg" id="msSheetHeroBg"></div>', 811 ' <div class="ms-sheet-hero-img" id="msSheetHeroImg"></div>', 812 ' <div class="ms-sheet-hero-grad"></div>', 813 ' <button class="ms-sheet-close" onclick="msCloseDetail()" aria-label="Chiudi"><i class="las la-times" aria-hidden="true"></i></button>', 814 '</div>', 815 '<div class="ms-sheet-body">', 816 ' <div class="ms-sheet-date" id="msSheetDate"></div>', 817 ' <h2 class="ms-sheet-title" id="msSheetTitle"></h2>', 818 ' <div class="ms-sheet-info" id="msSheetInfo"></div>', 819 ' <div id="msSheetTickets" style="display:none;">', 820 ' <div class="ms-sec-head"><div class="ms-sec-bar"></div><span class="ms-sec-label">Biglietti</span></div>', 821 ' <div id="msSheetTicketRows"></div>', 822 ' </div>', 823 ' <div id="msSheetTables" style="display:none;">', 824 ' <div class="ms-sec-head"><div class="ms-sec-bar"></div><span class="ms-sec-label">Tavoli</span></div>', 825 ' <div id="msSheetTableRows"></div>', 826 ' </div>', 827 ' <div id="msSheetNoTickets" style="display:none;padding:12px 0 4px;text-align:center;color:#52525b;font-size:13px;">Biglietti in arrivo</div>', 828 ' <div class="ms-sheet-cta" id="msSheetCta" style="display:none;">', 829 ' <button class="ms-sheet-cta-btn" onclick="msSubmit()" id="msSheetCtaBtn">',
830 ' <span class="ms-sheet-cta-left"><span class="ms-sheet-cta-qty" id="msSheetCtaQty">1</span><span class="ms-sheet-cta-label">Vai al pagamento</span></span>', 831 ' <span class="ms-sheet-cta-right"><span class="ms-sheet-cta-total" id="msSheetCtaTotal">â¬0.00</span><span class="ms-sheet-cta-arrow">â</span></span>', 832 ' </button>', 833 ' </div>', 834 ' <div class="ms-form-step" id="msFormStep" style="display:none;">', 835 ' <button class="ms-back-btn" onclick="msBackToTickets()">â Torna ai biglietti</button>', 836 ' <div class="ms-form-summary" id="msFormSummary"></div>', 837 ' <form autocomplete="off" onsubmit="return false;" style="display:contents;">', 838 ' <div class="ms-form-row">', 839 ' <div class="ms-form-group"><label class="ms-form-label" for="msFirstName">Nome</label><input class="ms-form-input" type="text" id="msFirstName" name="jnp-fname" placeholder="Mario" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" readonly></div>', 840 ' <div class="ms-form-group"><label class="ms-form-label" for="msLastName">Cognome</label><input class="ms-form-input" type="text" id="msLastName" name="jnp-lname" placeholder="Rossi" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" readonly></div>', 841 ' </div>', 842 ' <div class="ms-form-group ms-email-group"><label class="ms-form-label" for="msEmail">Email</label><div class="ms-email-wrap"><input class="ms-form-input" type="text" id="msEmail" name="jnp-email" placeholder="[email protected]" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" inputmode="email" readonly><span class="ms-otp-spinner" id="msOtpSpinner" style="display:none;"></span></div><span class="ms-email-hint" id="msEmailHint" style="display:none;">Inserisci un indirizzo email valido (es. [email protected])</span></div>', 843 ' <div class="ms-form-group" id="msDobGroup" style="display:none;">', 844 ' <label class="ms-form-label" for="msDob">Data di nascita <span id="msDobAgeReq" style="font-weight:400;opacity:.7;"></span></label>', 845 ' <input class="ms-form-input" type="date" id="msDob" name="jnp-dob" autocomplete="bday" max="">', 846 ' <span class="ms-email-hint" id="msDobHint" style="display:none;"></span>', 847 ' </div>', 848 ' <div class="ms-otp-block" id="msOtpBlock" style="display:none;">', 849 ' <div class="ms-otp-banner" id="msOtpBanner"></div>', 850 ' <div class="ms-otp-input-row" id="msOtpInputRow" style="display:none;">', 851 ' <input class="ms-form-input ms-otp-input" type="text" id="msOtpInput" placeholder="000000" maxlength="6" inputmode="numeric" autocomplete="one-time-code">', 852 ' <div class="ms-otp-error" id="msOtpError"></div>', 853 ' <button type="button" class="ms-otp-resend" id="msOtpResend" onclick="msOtpResend()">Non hai ricevuto il codice? Reinvia</button>', 854 ' </div>', 855 ' <div class="ms-otp-discount-line" id="msOtpDiscountLine" style="display:none;"></div>', 856 ' </div>', 857 ' <div class="ms-form-error" id="msFormError">Compila tutti i campi correttamente.</div>', 858 ' </form>', 859 ' <button class="ms-form-submit" id="msFormSubmitBtn" onclick="msSubmit()" disabled>Procedi al pagamento â</button>', 860 ' <p class="ms-form-secure">ð Pagamento sicuro via Stripe</p>', 861 ' </div>', 862 '</div>', 863 '</div>', 864 ].join('\n'); 865 document.body.appendChild(el); 866 el.addEventListener('click', function(e) { if (e.target === el) msCloseDetail(); }); 867 868 // Inject combo styles 869 if (!document.getElementById('ms-combo-styles')) { 870 var s = document.createElement('style'); 871 s.id = 'ms-combo-styles'; 872 s.textContent = [ 873 '.ms-badge-combo{display:inline-block;background:#8E0633;color:#fff;font-size:9px;font-weight:700;letter-spacing:.08em;padding:2px 6px;border-radius:4px;margin-right:6px;vertical-align:middle;text-transform:uppercase;}', 874 '.ms-combo-legs{font-size:11px;color:rgba(255,255,255,0.6);margin-top:4px;line-height:1.4;}', 875 ].join(''); 876 document.head.appendChild(s); 877 } 878 879 // Re-bind overlay reference 880 detailOverlay = el; 881} 882 883// Build DOM and bind buttons â works regardless of readyState 884function jnpInit() { 885 if (!document.getElementById('msDetailOverlay')) buildSheetDOM(); 886 document.addEventListener('click', function(e) { 887 // Info button 888 var info = e.target.closest('.ms-info-btn'); 889 if (info) { 890 e.preventDefault(); e.stopPropagation(); 891 window.msShowInfo(info.dataset.infoName || '', info.dataset.infoDesc || ''); 892 return; 893 } 894 // Ticket picker button 895 var btn = e.target.closest('.jnp-ticket-picker-btn'); 896 if (!btn) return; 897 e.preventDefault();
898 window.jnpOpenTicketSheet( 899 btn.dataset.eventId, 900 btn.dataset.occurrenceTs, 901 btn.dataset.eventTitle || '', 902 btn.dataset.thumbnail || '', 903 btn.dataset.age || '', 904 btn.dataset.endTime || '' 905 ); 906 }); 907 908 // OTP input â auto-verify at 6 digits 909 document.addEventListener('input', function(e) { 910 if (e.target && e.target.id === 'msOtpInput') { 911 var val = e.target.value.replace(/[^0-9]/g, '').substring(0, 6); 912 e.target.value = val; 913 if (val.length === 6) msOtpVerify(val); 914 } 915 }); 916} 917 918if (document.readyState === 'loading') { 919 document.addEventListener('DOMContentLoaded', jnpInit); 920} else { 921 jnpInit(); 922} 923})();
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.