1/* jnp-tables frontend widget */ 2(function ($) { 3 'use strict'; 4 5 const i18n = jnpTables.i18n; 6 const restUrl = jnpTables.restUrl; 7 const nonce = jnpTables.nonce; 8 const lang = jnpTables.lang; 9 10 // ========================================================= 11 // INIT â called when the Tables button is clicked 12 // ========================================================= 13 window.jnpTablesOpen = function (eventId, occurrenceTs) { 14 const modal = document.getElementById('jnp-tables-modal'); 15 if (!modal) return; 16 17 // Reset state 18 state = { 19 eventId, 20 occurrenceTs, 21 step: 1, 22 table: null, 23 drinks: [], // { type, id, name, quantity, prebook_price, line_total } 24 form: {}, 25 }; 26 27 modal.style.display = 'flex'; 28 document.body.style.overflow = 'hidden'; 29 renderStep(1); 30 loadFloorPlan(); 31 }; 32 33 window.jnpTablesClose = function () { 34 const modal = document.getElementById('jnp-tables-modal'); 35 if (modal) modal.style.display = 'none'; 36 document.body.style.overflow = ''; 37 }; 38 39 let state = {}; 40 let floorPlanData = null; 41 let drinksData = null; 42 43 // ========================================================= 44 // API 45 // ========================================================= 46 function loadFloorPlan() { 47 setLoading(true); 48 fetch(`${restUrl}floor-plan?event_id=${state.eventId}&occurrence_ts=${state.occurrenceTs}&lang=${lang}`, { 49 headers: { 'X-WP-Nonce': nonce } 50 }) 51 .then(r => r.json()) 52 .then(data => { 53 if (data.code) { showError(data.message); return; } 54 floorPlanData = data; 55 setLoading(false); 56 renderFloorPlan(); 57 }) 58 .catch(() => { showError('Failed to load floor plan.'); setLoading(false); }); 59 } 60 61 function loadDrinks(cb) { 62 if (drinksData) { cb(drinksData); return; } 63 fetch(`${restUrl}drinks?event_id=${state.eventId}&occurrence_ts=${state.occurrenceTs}&lang=${lang}`, { 64 headers: { 'X-WP-Nonce': nonce } 65 }) 66 .then(r => r.json()) 67 .then(data => { drinksData = data; cb(data); }) 68 .catch(() => cb({ categories: [], packages: [] })); 69 } 70 71 // ========================================================= 72 // STEP NAVIGATION 73 // =========================================================
74 function renderStep(step) { 75 state.step = step; 76 updateProgressBar(step); 77 const body = document.getElementById('jnp-modal-body'); 78 if (!body) return; 79 80 if (step === 1) { body.innerHTML = renderStep1Skeleton(); if (floorPlanData) renderFloorPlan(); } 81 if (step === 2) { renderStep(3); return; } // step 2 removed â skip straight to checkout 82 if (step === 3) { body.innerHTML = renderStep3(); } 83 } 84 85 function updateProgressBar(step) { 86 // 2 visual steps: 1 = choose table, 2 = checkout (internal step 3) 87 const el1 = document.getElementById('jnp-step-1'); 88 const el2 = document.getElementById('jnp-step-2'); 89 if (el1) { el1.classList.toggle('active', step === 1); el1.classList.toggle('done', step > 1); } 90 if (el2) { el2.classList.toggle('active', step === 3); el2.classList.toggle('done', false); } 91 } 92 93 // ========================================================= 94 // STEP 1 â FLOOR PLAN 95 // ========================================================= 96 function renderStep1Skeleton() { 97 return ` 98 <div class="jnp-step-header"> 99 <h2 class="jnp-step-title">
99${i18n.step1_title}</h2> 100 <div class="jnp-legend"> 101 <span class="jnp-legend-dot available"></span>${i18n.available} 102 <span class="jnp-legend-dot reserved"></span>${i18n.sold_out} 103 <span class="jnp-legend-dot unavailable"></span>${i18n.unavailable} 104 </div> 105 </div> 106 <div id="jnp-floor-wrap" class="jnp-floor-wrap"> 107 <div class="jnp-loader">${i18n.processing}</div> 108 </div> 109 <!-- popover injected dynamically into canvas --> 110 `; 111 } 112 113 function renderFloorPlan() { 114 const wrap = document.getElementById('jnp-floor-wrap'); 115 if (!wrap || !floorPlanData) return; 116 117 const { image_url, tables, canvas_width } = floorPlanData; 118 119 // Lock canvas to canvas_width if set â prevents dots drifting on resize 120 const canvasStyle = canvas_width > 0 121 ? `max-width:${canvas_width}px;width:100%;margin:0 auto;` 122 : `width:100%;`; 123 124 wrap.innerHTML = ` 125 <div class="jnp-floor-canvas" id="jnp-canvas" style="${canvasStyle}"> 126 ${image_url ? `<img id="jnp-canvas-img" src="${image_url}" style="display:block;width:100%;height:auto;pointer-events:none;">` : ''} 127 ${tables.map(t => ` 128 <button class="jnp-dot status-${t.status}" 129 style="left:${t.pos_x}%;top:${t.pos_y}%;background:${dotColor(t)}" 130 data-id="${t.id}" 131 ${t.status !== 'available' ? 'disabled' : ''} 132 aria-label="${t.label}"> 133 <span class="jnp-dot-pulse" style="background:${dotColor(t)}"></span> 134 </button>`).join('')} 135 </div>`; 136 137 // Click on canvas background closes popover 138 const canvasEl = wrap.querySelector('#jnp-canvas'); 139 if (canvasEl) { 140 canvasEl.addEventListener('click', function(e) { 141 if (e.target === canvasEl || e.target.tagName === 'IMG') closePopover(); 142 }); 143 } 144 145 // Click handlers 146 wrap.querySelectorAll('.jnp-dot[data-id]').forEach(btn => { 147 btn.addEventListener('click', () => { 148 const tableId = parseInt(btn.dataset.id); 149 const t = tables.find(x => x.id === tableId); 150 if (t) showTableDetail(t); 151 }); 152 }); 153 } 154 155 function dotColor(t) { 156 if (t.status === 'available') return '#22c55e'; 157 if (t.status === 'reserved') return '#ef4444'; 158 return '#94a3b8'; 159 } 160 161 function showTableDetail(t) { 162 state.table = t; 163 if (!state.partySize || state.lastTableId !== t.id) state.partySize = t.capacity; 164 state.lastTableId = t.id; 165 166 // Highlight dot 167 document.querySelectorAll('.jnp-dot').forEach(d => d.classList.remove('selected')); 168 const dot = document.querySelector(`.jnp-dot[data-id="${t.id}"]`); 169 if (dot) dot.classList.add('selected'); 170 171 // Get or create the table screen â appended to modal-inner so it covers everything 172 const inner = document.querySelector('.jnp-modal-inner'); 173 if (!inner) return; 174 175 let screen = document.getElementById('jnp-table-screen'); 176 if (!screen) { 177 screen = document.createElement('div'); 178 screen.id = 'jnp-table-screen'; 179 screen.className = 'jnp-table-screen'; 180 inner.appendChild(screen); 181 } 182 183 screen.innerHTML = renderTableCard(t); 184 // Slight delay so CSS transition fires 185 requestAnimationFrame(() => screen.classList.add('visible')); 186 187 // Back button â close screen, return to map 188 screen.querySelector('.jnp-close-detail')?.addEventListener('click', closeTableScreen); 189 190 // Party size stepper 191 screen.querySelector('#jnp-minus')?.addEventListener('click', () => { 192 if (state.partySize > 1) { state.partySize--; showTableDetail(t); } 193 }); 194 screen.querySelector('#jnp-plus')?.addEventListener('click', () => { 195 if (state.partySize < t.capacity) { state.partySize++; showTableDetail(t); } 196 }); 197 198 // Book now â close screen first, then advance 199 screen.querySelector('#jnp-select-table')?.addEventListener('click', () => { 200 const s = document.getElementById('jnp-table-screen'); 201 if (s) s.remove(); 202 renderStep(3); 203 }); 204 } 205 206 function closeTableScreen() { 207 const screen = document.getElementById('jnp-table-screen'); 208 if (screen) { 209 screen.classList.remove('visible'); 210 screen.addEventListener('transitionend', () => screen.remove(), { once: true }); 211 } 212 document.querySelectorAll('.jnp-dot').forEach(d => d.classList.remove('selected')); 213 state.table = null; 214 state.partySize = null; 215 } 216 217 // Keep closePopover as alias so canvas-click handler still works 218 function closePopover() { closeTableScreen(); } 219 220 221 function renderTableCard(t) { 222 const ps = state.partySize || t.capacity; 223 // min_spend is fixed regardless of party size 224 const deposit = t.deposit_type === 'percentage' 225 ? Math.round(t.min_spend * (t.deposit_value / 100) * 100) / 100 226 : t.deposit_value; 227 228 const isOnRequest = (t.booking_type || 'instant') === 'on_request'; 229 const bookingBadge = isOnRequest 230 ? `<span class="jnp-booking-badge on-request">â³ ${i18n.on_request_badge}</span>` 231 : `<span class="jnp-booking-badge instant">â¡ ${i18n.instant_badge}</span>`;
232 233 return ` 234 <div class="jnp-table-card"> 235 <div class="jnp-table-card-header"> 236 <h3>${esc(t.label)}</h3> 237 <button class="jnp-close-detail" aria-label="Back to map">â Back to map</button> 238 </div> 239 ${bookingBadge} 240 ${t.description ? `<p class="jnp-table-desc">${esc(t.description)}</p>` : ''} 241 242 <div class="jnp-party-size-wrap"> 243 <p class="jnp-party-label">${i18n.party_size}</p> 244 <div class="jnp-party-stepper"> 245 <button id="jnp-minus" class="jnp-stepper-btn" ${ps <= 1 ? 'disabled' : ''}>â</button> 246 <span class="jnp-party-count">${ps} <small>${i18n.pax}</small></span> 247 <button id="jnp-plus" class="jnp-stepper-btn ${ps >= t.capacity ? 'disabled' : ''}" ${ps >= t.capacity ? 'disabled' : ''}>+</button> 248 </div> 249 <small class="jnp-capacity-label">${i18n.capacity}: ${t.capacity} ${i18n.pax}</small> 250 </div> 251 252 <div class="jnp-table-pricing"> 253 <div class="jnp-pricing-row"> 254 <span>${i18n.min_spend}</span> 255 <strong>â¬${fmt(t.min_spend)}</strong> 256 </div> 257 <div class="jnp-pricing-row pay-now"> 258 <span>${i18n.pay_now}</span> 259 <strong>â¬${fmt(deposit)}</strong> 260 </div> 261 </div> 262 263 <div class="jnp-redeemable-box"> 264 <div class="jnp-redeemable-title">â ${i18n.fully_redeemable}</div> 265 <div class="jnp-redeemable-body">${i18n.redeemable_conditions}</div> 266 </div> 267 268 ${t.notes ? `<p class="jnp-table-notes">${esc(t.notes)}</p>` : ''} 269 270 ${isOnRequest 271 ? `<div class="jnp-on-request-info">${i18n.on_request_info}</div>` 272 : `<div class="jnp-pricing-row pay-now"> 273 <span>${i18n.pay_now}</span> 274 <strong>â¬\${fmt(deposit)}</strong> 275 </div>` 276 } 277 278 <button id="jnp-select-table" class="jnp-btn-primary jnp-btn-full"> 279 ${isOnRequest ? ('â³ ' + i18n.request_now + ' â') : (i18n.book_now + ' â')} 280 </button> 281 </div>`; 282 } 283 284 // ========================================================= 285 // STEP 2 â skipped (extras removed, goes straight to step 3) 286 // ========================================================= 287 window.jnpTablesRenderStep = renderStep; 288 function calcExtras() { 289 return state.drinks.reduce((sum, d) => sum + d.line_total, 0); 290 } 291 function calcDeposit(t, drinks) { 292 if (!t) return 0; 293 const total = t.min_spend + calcExtras(); 294 return t.deposit_type === 'percentage' 295 ? Math.round(total * (t.deposit_value / 100) * 100) / 100 296 : Math.min(t.deposit_value, total); 297 } 298 function renderOrderSummary() { 299 const t = state.table; 300 if (!t) return ''; 301 const extras = calcExtras(); 302 const total = t.min_spend + extras; 303 const deposit = calcDeposit(t, state.drinks); 304 const balance = total - deposit; 305 306 let drinksHtml = ''; 307 state.drinks.forEach(d => { 308 drinksHtml += `<div class="jnp-summary-row"><span>${esc(d.name)} Ã${d.quantity}</span><span>â¬${fmt(d.line_total)}</span></div>`; 309 }); 310 311 return ` 312 <div class="jnp-order-summary" id="jnp-order-summary"> 313 <p class="jnp-summary-title">${i18n.order_summary}</p> 314 <div class="jnp-summary-row"><span>${esc(t.label)}</span><span>â¬${fmt(t.min_spend)}</span></div> 315 ${drinksHtml} 316 <div class="jnp-summary-divider"></div> 317 <div class="jnp-summary-row jnp-summary-total"><span>${i18n.grand_total}</span><span>â¬${fmt(total)}</span></div> 318 <div class="jnp-summary-row jnp-pay-now-row"><span>${i18n.pay_now}</span><strong>â¬${fmt(deposit)}</strong></div> 319 <div class="jnp-summary-row jnp-balance-row"><span>${i18n.balance_due}</span><span>â¬${fmt(balance)}</span></div> 320 <div class="jnp-redeemable-badge small">â¹ ${i18n.fully_redeemable}</div> 321 </div>`; 322 } 323 function refreshOrderSummary() { 324 const el = document.getElementById('jnp-order-summary'); 325 if (el) el.outerHTML = renderOrderSummary(); 326 } 327 328 // ========================================================= 329 // STEP 3 â CONTACT FORM 330 // ========================================================= 331 // ========================================================= 332 // STEP 3 â CONTACT FORM 333 // =========================================================
334 function renderStep3() { 335 const t = state.table; 336 const deposit = calcDeposit(t, state.drinks); 337 const extras = calcExtras(); 338 const total = t.min_spend + extras; 339 340 return ` 341 <div class="jnp-step-header"> 342 <button class="jnp-back-btn" onclick="jnpTablesRenderStep(1)">â ${i18n.back}</button> 343 <h2 class="jnp-step-title">${i18n.step3_title}</h2> 344 </div> 345 346 <div class="jnp-form-wrap" id="jnp-contact-form"> 347 <div id="jnp-form-errors" class="jnp-errors" style="display:none;"></div> 348 349 <div class="jnp-form-row two-col"> 350 <div class="jnp-field"> 351 <label>${i18n.first_name} *</label> 352 <input type="text" id="jf-first-name" placeholder="${i18n.first_name}" value="${esc(state.form.first_name || '')}"> 353 </div> 354 <div class="jnp-field"> 355 <label>${i18n.last_name} *</label> 356 <input type="text" id="jf-last-name" placeholder="${i18n.last_name}" value="${esc(state.form.last_name || '')}"> 357 </div> 358 </div> 359 <div class="jnp-field"> 360 <label>${i18n.email} *</label> 361 <input type="email" id="jf-email" placeholder="${i18n.email}" value="${esc(state.form.email || '')}"> 362 </div> 363 <div class="jnp-field"> 364 <label>${i18n.phone} *</label> 365 <input type="tel" id="jf-phone" placeholder="+39..." value="${esc(state.form.phone || '')}"> 366 </div> 367 <div class="jnp-field"> 368 <label>${i18n.notes}</label> 369 <textarea id="jf-notes" rows="2" placeholder="${i18n.notes}">${esc(state.form.notes || '')}</textarea> 370 </div> 371 <div class="jnp-field"> 372 <label>${i18n.celebrating}</label> 373 <input type="text" id="jf-celebrating" placeholder="${i18n.celebrating}" value="${esc(state.form.celebrating || '')}"> 374 </div> 375 376 <!-- Info block --> 377 <div class="jnp-info-block"> 378 <p class="jnp-info-title">â¹ ${i18n.info_title}</p> 379 <p class="jnp-info-text" id="jnp-info-text"></p> 380 </div> 381 382 <!-- Summary --> 383 ${state.table && state.table.booking_type === 'on_request' 384 ? `<div class="jnp-on-request-info" style="margin:12px 0;">${i18n.on_request_info}</div>` 385 : renderOrderSummary() 386 } 387 388 <label class="jnp-checkbox-label"> 389 <input type="checkbox" id="jf-tc"> ${i18n.tc_accept} * 390 </label> 391 <label class="jnp-checkbox-label"> 392 <input type="checkbox" id="jf-marketing"> ${i18n.marketing_consent} 393 </label> 394 395 <button class="jnp-btn-primary jnp-btn-full" id="jnp-pay-btn" style="margin-top:16px;"> 396 ${state.table && state.table.booking_type === 'on_request' 397 ? ('â³ ' + i18n.request_submit) 398 : ('ð³ ' + i18n.pay + ' â â¬' + fmt(deposit)) 399 } 400 </button> 401 </div>`; 402 } 403 404 // Load info text from WP options (async) 405 function loadInfoText() { 406 const el = document.getElementById('jnp-info-text'); 407 if (!el) return; 408 // Use localized string if set, otherwise placeholder 409 el.textContent = jnpTables.infoText || (lang === 'it' ? '18+ · Dress code in vigore · Ingresso con documento' : '18+ · Dress code applies · Valid ID required'); 410 } 411 412 // ========================================================= 413 // PAYMENT SUBMISSION 414 // ========================================================= 415 $(document).on('click', '#jnp-pay-btn', function () { 416 const btn = this; 417 const errors = []; 418 419 const first_name = $('#jf-first-name').val().trim(); 420 const last_name = $('#jf-last-name').val().trim(); 421 const email = $('#jf-email').val().trim(); 422 const phone = $('#jf-phone').val().trim(); 423 const notes = $('#jf-notes').val().trim(); 424 const celebrating = $('#jf-celebrating').val().trim(); 425 const tc = $('#jf-tc').is(':checked'); 426 const marketing = $('#jf-marketing').is(':checked'); 427 428 if (!first_name || !last_name || !email || !phone) errors.push(i18n.error_required); 429 if (!tc) errors.push(i18n.error_tc); 430 431 const errEl = document.getElementById('jnp-form-errors'); 432 if (errors.length) { 433 errEl.innerHTML = errors.map(e => `<p>${e}</p>`).join(''); 434 errEl.style.display = 'block'; 435 errEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 436 return; 437 } 438 errEl.style.display = 'none'; 439 440 // Save form state 441 state.form = { first_name, last_name, email, phone, notes, celebrating }; 442
443 $(btn).prop('disabled', true).text(i18n.processing); 444 445 const payload = { 446 table_id: state.table.id, 447 event_id: state.eventId, 448 occurrence_ts: state.occurrenceTs, 449 first_name, last_name, email, phone, notes, celebrating, 450 guests: state.partySize || state.table.capacity, 451 drinks: state.drinks.map(d => ({ type: d.type, id: d.id, quantity: d.quantity })), 452 marketing_consent: marketing ? 1 : 0, 453 lang, 454 }; 455 456 const isOnRequest = state.table && state.table.booking_type === 'on_request'; 457 const endpoint = isOnRequest ? `${restUrl}request-reserve` : `${restUrl}reserve`; 458 459 fetch(endpoint, { 460 method: 'POST', 461 headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': nonce }, 462 body: JSON.stringify(payload), 463 }) 464 .then(r => r.json()) 465 .then(data => { 466 if (isOnRequest && data.success) { 467 // Show success message inline 468 const formWrap = document.getElementById('jnp-contact-form'); 469 if (formWrap) formWrap.innerHTML = `<div class="jnp-request-success"><p>â ${i18n.request_sent}</p></div>`; 470 } else if (data.checkout_url) { 471 window.location.href = data.checkout_url; 472 } else { 473 $(btn).prop('disabled', false).text(isOnRequest ? `â³ ${i18n.request_submit}` : `ð³ ${i18n.pay}`); 474 const errEl = document.getElementById('jnp-form-errors'); 475 errEl.innerHTML = `<p>${data.message || 'An error occurred.'}</p>`; 476 errEl.style.display = 'block'; 477 } 478 }) 479 .catch(() => { 480 $(btn).prop('disabled', false).text(isOnRequest ? `â³ ${i18n.request_submit}` : `ð³ ${i18n.pay}`); 481 }); 482 }); 483 484 // ========================================================= 485 // MODAL INIT â inject modal HTML on page load 486 // ========================================================= 487 $(document).ready(function () { 488 if (!document.getElementById('jnp-tables-modal')) { 489 $('body').append(` 490 <div id="jnp-tables-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:99999;align-items:center;justify-content:center;padding:16px;"> 491 <div class="jnp-modal-inner"> 492 <div class="jnp-modal-header"> 493 <div class="jnp-progress-bar"> 494 <div class="jnp-progress-step active" id="jnp-step-1"><span>1</span></div> 495 <div class="jnp-progress-line"></div> 496 <div class="jnp-progress-step" id="jnp-step-2"><span>2</span></div> 497 </div> 498 <button class="jnp-modal-close" onclick="jnpTablesClose()">â</button> 499 </div> 500 <div class="jnp-modal-body" id="jnp-modal-body"></div> 501 </div> 502 </div>`); 503 } 504 505 // Close on backdrop click 506 $('#jnp-tables-modal').on('click', function (e) { 507 if (e.target === this) jnpTablesClose(); 508 }); 509 }); 510 511 // ========================================================= 512 // HELPERS 513 // ========================================================= 514 function fmt(n) { return parseFloat(n).toLocaleString('it-IT', { minimumFractionDigits: 0, maximumFractionDigits: 2 }); } 515 function esc(s) { return String(s || '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); } 516 function setLoading(on) { 517 const w = document.getElementById('jnp-floor-wrap'); 518 if (!w) return; 519 if (on) w.innerHTML = `<div class="jnp-loader">${i18n.processing}</div>`; 520 } 521 function showError(msg) { 522 const b = document.getElementById('jnp-modal-body'); 523 if (b) b.innerHTML = `<div class="jnp-error-msg">${msg}</div>`; 524 } 525 526})(jQuery);
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.