1/* jnp-whatsapp widget â frontend */ 2(function () { 3 'use strict'; 4 5 const API = jnpWa.restUrl; 6 const i18n = jnpWa.i18n || {}; 7 const NONCE = jnpWa.nonce; 8 const STRINGS = jnpWa.strings || {}; 9 10 // ââ Session ID (persists in sessionStorage so page nav keeps context) ââââââ 11 function getSessionId() { 12 let id = sessionStorage.getItem('jnp_wa_session'); 13 if (!id) { 14 id = 'ws_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8); 15 sessionStorage.setItem('jnp_wa_session', id); 16 } 17 return id; 18 } 19 20 // ââ State ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 21 const state = { 22 open: false, 23 loading: false, 24 linked: false, // phone number confirmed 25 messages: [], 26 sessionId: getSessionId(), 27 }; 28 29 // ââ DOM refs âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 30 let widget, bubble, panel, messagesEl, inputEl, sendBtn, phoneBar, phoneInput; 31 32 // ââ Build DOM ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 33 function buildWidget() { 34 widget = document.createElement('div'); 35 widget.id = 'jnp-wa-widget'; 36 widget.setAttribute('aria-live', 'polite'); 37 38 widget.innerHTML = ` 39 <div id="jnp-wa-bubble" role="button" tabindex="0" aria-label="Open chat"> 40 <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> 41 <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> 42 </svg> 43 <svg id="jnp-wa-close-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" aria-hidden="true"> 44 <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/> 45 </svg> 46 </div> 47 48 <div id="jnp-wa-panel" role="dialog" aria-label="Juniper Club chat" aria-modal="true"> 49 <div id="jnp-wa-header"> 50 <div id="jnp-wa-header-info"> 51 <div id="jnp-wa-avatar" aria-hidden="true">JC</div> 52 <div> 53 <div id="jnp-wa-header-name">Juniper Club</div> 54 <div id="jnp-wa-header-status"> 55 <span id="jnp-wa-status-dot" aria-hidden="true"></span> 56 <span id="jnp-wa-status-text">Online</span> 57 </div> 58 </div> 59 </div> 60 </div> 61 62 <div id="jnp-wa-messages" role="log" aria-label="Conversation"></div> 63 64 <div id="jnp-wa-phone-bar"> 65 <p id="jnp-wa-phone-label">Continue on WhatsApp â share your number and we'll pick up there too.</p> 66 <div id="jnp-wa-phone-row"> 67 <input id="jnp-wa-phone-input" type="tel" placeholder="+39 345 678 9012" aria-label="Your WhatsApp number" autocomplete="tel"> 68 <button id="jnp-wa-phone-btn" aria-label="Link WhatsApp number">Link</button> 69 </div> 70 <button id="jnp-wa-phone-skip">Skip</button> 71 </div> 72 73 <div id="jnp-wa-composer"> 74 <textarea id="jnp-wa-input" rows="1" placeholder="Type a messageâ¦" aria-label="Message" autocomplete="off"></textarea> 75 <button id="jnp-wa-send" aria-label="Send message" disabled> 76 <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"> 77 <line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/> 78 </svg> 79 </button> 80 </div> 81 </div> 82 `; 83 84 document.body.appendChild(widget); 85 86 bubble = widget.querySelector('#jnp-wa-bubble'); 87 panel = widget.querySelector('#jnp-wa-panel'); 88 messagesEl= widget.querySelector('#jnp-wa-messages'); 89 inputEl = widget.querySelector('#jnp-wa-input'); 90 sendBtn = widget.querySelector('#jnp-wa-send'); 91 phoneBar = widget.querySelector('#jnp-wa-phone-bar'); 92 phoneInput= widget.querySelector('#jnp-wa-phone-input'); 93 94 bindEvents(); 95 } 96 97 // ââ Events âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 98 function bindEvents() { 99 bubble.addEventListener('click', togglePanel); 100 bubble.addEventListener('keydown', e => { if (e.key === 'Enter' || e.key === ' ') togglePanel(); }); 101 102 sendBtn.addEventListener('click', submitMessage); 103 inputEl.addEventListener('keydown', e => { 104 if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submitMessage(); } 105 }); 106 inputEl.addEventListener('input', () => { 107 sendBtn.disabled = inputEl.value.trim() === ''; 108 autoResize(); 109 }); 110 111 // Phone bar 112 widget.querySelector('#jnp-wa-phone-btn').addEventListener('click', linkPhone); 113 widget.querySelector('#jnp-wa-phone-skip').addEventListener('click', () => { 114 phoneBar.style.display = 'none'; 115 }); 116 phoneInput.addEventListener('keydown', e => { if (e.key === 'Enter') linkPhone(); }); 117 118 // Close on outside click 119 document.addEventListener('click', e => { 120 if (state.open && !widget.contains(e.target)) closePanel(); 121 }); 122 document.addEventListener('keydown', e => { if (e.key === 'Escape' && state.open) closePanel(); }); 123 } 124 125 function autoResize() { 126 inputEl.style.height = 'auto'; 127 inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px'; 128 } 129 130 // ââ Panel open / close âââââââââââââââââââââââââââââââââââââââââââââââââââââ
131 function togglePanel() { state.open ? closePanel() : openPanel(); } 132 133 function openPanel(intent) { 134 state.open = true; 135 panel.classList.add('jnp-wa-open'); 136 widget.querySelector('#jnp-wa-close-icon').style.display = 'block'; 137 widget.querySelector('svg:first-child').style.display = 'none'; 138 bubble.setAttribute('aria-expanded', 'true'); 139 140 // Lock body scroll (same as Bootstrap modal) 141 state._scrollY = window.scrollY; 142 document.body.style.overflow = 'hidden'; 143 document.body.style.position = 'fixed'; 144 document.body.style.top = '-' + state._scrollY + 'px'; 145 document.body.style.width = '100%'; 146 147 if (state.messages.length === 0) { 148 showGreeting(intent); 149 } else if (intent) { 150 injectIntent(intent); 151 } 152 153 setTimeout(() => inputEl.focus(), 300); 154 } 155 156 function closePanel() { 157 state.open = false; 158 panel.classList.remove('jnp-wa-open'); 159 widget.querySelector('#jnp-wa-close-icon').style.display = 'none'; 160 widget.querySelector('svg:first-child').style.display = 'block'; 161 bubble.setAttribute('aria-expanded', 'false'); 162 163 // Restore body scroll 164 document.body.style.overflow = ''; 165 document.body.style.position = ''; 166 document.body.style.top = ''; 167 document.body.style.width = ''; 168 window.scrollTo(0, state._scrollY || 0); 169 } 170 171 // ââ Greeting & intent injection ââââââââââââââââââââââââââââââââââââââââââââ 172 const intentMessages = { 173 reservation: "Hi! I'd like to book a table.", 174 tickets: "Hi! I'd like to buy tickets for an event.", 175 info: "Hi! I have a question about the club.", 176 vip: "Hi! I'm interested in a VIP table package.", 177 }; 178 179 function showGreeting(intent) { 180 const greet = STRINGS.greeting || "Hi! ð I'm the Juniper Club assistant. How can I help you tonight?"; 181 appendMessage('bot', greet); 182 if (intent && intentMessages[intent]) { 183 setTimeout(() => { 184 inputEl.value = intentMessages[intent]; 185 sendBtn.disabled = false; 186 autoResize(); 187 inputEl.focus(); 188 }, 600); 189 } 190 } 191 192 function injectIntent(intent) { 193 if (intent && intentMessages[intent]) { 194 inputEl.value = intentMessages[intent]; 195 sendBtn.disabled = false; 196 autoResize(); 197 } 198 } 199 200 // ââ Send message âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 201 async function submitMessage() { 202 const text = inputEl.value.trim(); 203 if (!text || state.loading) return; 204 205 inputEl.value = ''; 206 inputEl.style.height = 'auto'; 207 sendBtn.disabled = true; 208 appendMessage('user', text); 209 showTyping(); 210 211 state.loading = true; 212 213 try { 214 const res = await fetch(API + 'jnp-wa/v1/chat', { 215 method: 'POST', 216 headers: { 217 'Content-Type': 'application/json', 218 'X-WP-Nonce': NONCE, 219 }, 220 body: JSON.stringify({ 221 session_id: state.sessionId, 222 message: text, 223 lang: jnpWa.lang || 'it', 224 }), 225 }); 226 227 const data = await res.json(); 228 hideTyping(); 229 230 if (data.reply) { 231 appendMessage('bot', data.reply); 232 if (data.action) renderCard(data.action); 233 } 234 } catch (err) { 235 hideTyping(); 236 appendMessage('bot', "Sorry, I couldn't connect. Please try again."); 237 } 238 239 state.loading = false; 240 } 241 242 // ââ Phone linking ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 243 async function linkPhone() { 244 const num = phoneInput.value.trim(); 245 if (!num) return; 246 247 phoneInput.disabled = true; 248 widget.querySelector('#jnp-wa-phone-btn').disabled = true; 249 250 try { 251 const res = await fetch(API + 'jnp-wa/v1/link-phone', { 252 method: 'POST', 253 headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': NONCE }, 254 body: JSON.stringify({ session_id: state.sessionId, phone: num }), 255 }); 256 const data = await res.json(); 257 258 if (data.success) { 259 state.linked = true; 260 phoneBar.innerHTML = '<p id="jnp-wa-phone-label" style="text-align:center;">â Linked! Continue this conversation on WhatsApp anytime.</p>'; 261 setTimeout(() => { phoneBar.style.display = 'none'; }, 3000); 262 } else { 263 phoneInput.disabled = false;
264 widget.querySelector('#jnp-wa-phone-btn').disabled = false; 265 phoneInput.placeholder = data.message || 'Invalid number, try again'; 266 } 267 } catch { 268 phoneInput.disabled = false; 269 widget.querySelector('#jnp-wa-phone-btn').disabled = false; 270 } 271 } 272 273 // ââ Message rendering ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 274 function appendMessage(role, text) { 275 const div = document.createElement('div'); 276 div.className = 'jnp-wa-msg jnp-wa-msg-' + role; 277 div.innerHTML = '<p>' + escHtml(text).replace(/\n/g, '<br>').replace(/\*(.*?)\*/g, '<strong>$1</strong>') + '</p>'; 278 messagesEl.appendChild(div); 279 messagesEl.scrollTop = messagesEl.scrollHeight; 280 state.messages.push({ role, text }); 281 } 282 283 function showTyping() { 284 const div = document.createElement('div'); 285 div.className = 'jnp-wa-msg jnp-wa-msg-bot jnp-wa-typing'; 286 div.id = 'jnp-wa-typing'; 287 div.innerHTML = '<span></span><span></span><span></span>'; 288 messagesEl.appendChild(div); 289 messagesEl.scrollTop = messagesEl.scrollHeight; 290 } 291 292 function hideTyping() { 293 const el = document.getElementById('jnp-wa-typing'); 294 if (el) el.remove(); 295 } 296 297 function escHtml(s) { 298 return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); 299 } 300 301 // ââ Public trigger API âââââââââââââââââââââââââââââââââââââââââââââââââââââ 302 // Called by data-jnp-chat attributes and jnp_wa_chat_trigger() PHP helper 303 function handleTriggerClick(e) { 304 const el = e.currentTarget; 305 const intent = el.dataset.jnpIntent || null; 306 openPanel(intent); 307 } 308 309 function attachTriggers() { 310 document.querySelectorAll('[data-jnp-chat]').forEach(el => { 311 el.addEventListener('click', handleTriggerClick); 312 }); 313 } 314 315 // ââ Init âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 316 function init() { 317 buildWidget(); 318 attachTriggers(); 319 320 // Re-attach after Bricks / Elementor / AJAX page updates 321 document.addEventListener('jnp_page_updated', attachTriggers); 322 } 323 324 if (document.readyState === 'loading') { 325 document.addEventListener('DOMContentLoaded', init); 326 } else { 327 init(); 328 } 329 330 // Expose for external calls (e.g. from custom JS) 331 window.jnpChat = { 332 open: (intent) =>
332 openPanel(intent), 333 close: closePanel, 334 toggle: togglePanel, 335 }; 336 337 338 // ââ Cards system ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 339 340 let cardsData = null; 341 342 async function loadCardsData() { 343 // Always fetch fresh â ensures i18n is applied correctly 344 try { 345 const res = await fetch(API + 'jnp-wa/v1/cards', { 346 headers: { 'X-WP-Nonce': NONCE }, 347 }); 348 cardsData = await res.json(); 349 } catch (e) { 350 cardsData = {}; 351 } 352 return cardsData; 353 } 354 355 async function renderCard(action) { 356 const data = await loadCardsData(); 357 if (!data) return; 358 359 switch (action) { 360 case 'show_hours': appendHoursCard(data.hours); break; 361 case 'show_venue': appendVenueCard(data.venue); break; 362 case 'show_events': appendEventsCard(data.events, data.ev_settings); break; 363 case 'show_contact_form': appendContactForm(data.contact_form); break; 364 } 365 } 366 367 function appendHoursCard(hours) { 368 if (!hours || !hours.enabled) return; 369 const days = ['monday','tuesday','wednesday','thursday','friday','saturday','sunday']; 370 const labels = i18n.days || {monday:'Mon',tuesday:'Tue',wednesday:'Wed',thursday:'Thu',friday:'Fri',saturday:'Sat',sunday:'Sun'}; 371 const todayIdx = (new Date().getDay() + 6) % 7; 372 const todayKey = days[todayIdx]; 373 374 let rows = ''; 375 days.forEach((day, i) => { 376 const d = hours.days[day] || {}; 377 const isToday = day === todayKey; 378 const isClosed = d.closed || !d.open; 379 const dayLabel = (!isClosed && d.date) ? `${labels[day]} ${escHtml(d.date)}` : labels[day]; 380 const todayBadge = isToday ? ` <span class="jnp-wa-badge">${i18n.today||'Today'}</span>` : ''; 381 const evBadge = (!isClosed && d.event) ? ` <span class="jnp-wa-badge jnp-wa-badge-event">${escHtml(d.event)}</span>` : ''; 382 rows += `<div class="jnp-wa-card-row${isToday ? ' jnp-wa-today' : ''}${isToday && isClosed ? ' jnp-wa-closed-today' : ''}"> 383 <span class="jnp-wa-card-day">${dayLabel}${todayBadge}${evBadge}</span> 384 <span class="jnp-wa-card-time">${isClosed ? (i18n.closed||'Closed') : `${d.open} â ${d.close || '05:00'}`}</span> 385 </div>`; 386 }); 387 388 const note = hours.note ? `<p class="jnp-wa-card-note">${escHtml(hours.note)}</p>` : ''; 389 const lastEntry = hours.last_entry ? `<p class="jnp-wa-card-note">Last entry: ${escHtml(hours.last_entry)}</p>` : ''; 390 391 appendCard(`<div class="jnp-wa-ic"> 392 <div class="jnp-wa-ic-head">ð ${i18n.opening_hours||'Opening hours'}</div> 393 ${rows} 394 ${note}${lastEntry} 395 </div>`); 396 } 397 398 function appendVenueCard(venue) { 399 if (!venue || !venue.enabled) return; 400 const rows = [ 401 venue.address ? `<div class="jnp-wa-card-row"><span class="jnp-wa-card-day">ð Address</span><span class="jnp-wa-card-time">${escHtml(venue.address)}${venue.city ? ', ' + escHtml(venue.city) : ''}</span></div>` : '', 402 venue.phone ? `<div class="jnp-wa-card-row"><span class="jnp-wa-card-day">ð Phone</span><span class="jnp-wa-card-time">${escHtml(venue.phone)}</span></div>` : '', 403 venue.email ? `<div class="jnp-wa-card-row"><span class="jnp-wa-card-day">âï¸ Email</span><span class="jnp-wa-card-time">${escHtml(venue.email)}</span></div>` : '', 404 venue.instagram ? `<div class="jnp-wa-card-row"><span class="jnp-wa-card-day">ð¸ Instagram</span><span class="jnp-wa-card-time">${escHtml(venue.instagram)}</span></div>` : '', 405 venue.dress_code ? `<div class="jnp-wa-card-row"><span class="jnp-wa-card-day">ð Dress code</span><span class="jnp-wa-card-time">${escHtml(venue.dress_code)}</span></div>` : '', 406 venue.age_minimum ? `<div class="jnp-wa-card-row"><span class="jnp-wa-card-day">𪪠Min age</span><span class="jnp-wa-card-time">${escHtml(venue.age_minimum)}</span></div>` : '', 407 ].filter(Boolean).join(''); 408 409 const mapsBtn = venue.maps_url 410 ? `<a href="${escAttr(venue.maps_url)}" target="_blank" class="jnp-wa-card-btn">Get directions â</a>` 411 : ''; 412 413 appendCard(`<div class="jnp-wa-ic"> 414 <div class="jnp-wa-ic-head">
414ð ${escHtml(venue.name || 'Juniper Club')}</div> 415 ${rows} 416 ${mapsBtn} 417 </div>`); 418 } 419 420 function appendEventsCard(events, settings) { 421 if (!events || !events.length) return; 422 const btnPrimary = settings.btn_primary || 'Get tickets'; 423 const btnSecondary = settings.btn_secondary || 'Book a table'; 424 const boxoffice = 'juniperclub'; 425 426 const cards = events.map(ev => { 427 const thumb = ev.thumb 428 ? `<div class="jnp-wa-ev-thumb" style="background-image:url('${escAttr(ev.thumb)}')"></div>` 429 : `<div class="jnp-wa-ev-thumb jnp-wa-ev-thumb-placeholder"></div>`; 430 431 // ââ Ticket button ââââââââââââââââââââââââââââââââââââââââââââââ 432 // Priority: TTWidget (tt_occurrence_id) â jnp-ticket-picker-btn â URL fallback 433 let ticketBtn = ''; 434 if (ev.tt_occurrence_id) { 435 // TTWidget (TicketTailor) â same as event-card.php jnp-tt-ticket-btn 436 ticketsBtn = `<button class="jnp-wa-card-btn jnp-wa-ev-tt-btn" 437 data-boxoffice="${escAttr(boxoffice)}" 438 data-tt-id="${escAttr(ev.tt_occurrence_id)}" 439 data-href="${escAttr(ev.tickets_url)}">${escHtml(btnPrimary)}</button>`; 440 ticketsBtn = ticketsBtn; // rename fix below 441 ticketBtn = ticketsBtn; 442 } else { 443 // jnp-ticket-picker-btn â opens the quick events modal with occurrence pre-selected 444 // same pattern as quick-events-modal.php and event-card.php 445 ticketBtn = `<button class="jnp-wa-card-btn jnp-ticket-picker-btn" 446 data-event-id="${escAttr(String(ev.id))}" 447 data-occurrence-ts="${escAttr(String(ev.ts))}" 448 data-event-title="${escAttr(ev.title)}" 449 data-thumbnail="${escAttr(ev.thumb)}">${escHtml(btnPrimary)}</button>`; 450 } 451 452 // ââ Tables button ââââââââââââââââââââââââââââââââââââââââââââââ 453 // If tt_tables_id present â TTWidget; else â open in-chat reservation flow 454 let tablesBtn = ''; 455 if (ev.tt_tables_id) { 456 tablesBtn = `<button class="jnp-wa-card-btn jnp-wa-card-btn-outline jnp-wa-ev-tt-btn" 457 data-boxoffice="${escAttr(boxoffice)}" 458 data-tt-id="${escAttr(ev.tt_tables_id)}">${escHtml(btnSecondary)}</button>`; 459 } else { 460 // Fall back to in-chat reservation flow 461 tablesBtn = `<button class="jnp-wa-card-btn jnp-wa-card-btn-outline jnp-wa-ev-reserve-btn" 462 data-event="${escHtml(ev.title)}" 463 data-date="${escHtml(ev.date)}">${escHtml(btnSecondary)}</button>`; 464 } 465 466 return `<div class="jnp-wa-ev-card"> 467 ${thumb} 468 <div class="jnp-wa-ev-body"> 469 <div class="jnp-wa-ev-title">${escHtml(ev.title)}</div> 470 <div class="jnp-wa-ev-date">${escHtml(ev.date)} · ${escHtml(ev.time)}</div> 471 <div class="jnp-wa-ev-btns"> 472 ${ticketBtn} 473 ${tablesBtn} 474 </div> 475 </div> 476 </div>`; 477 }).join(''); 478 479 const wrap = document.createElement('div'); 480 wrap.className = 'jnp-wa-card-wrap'; 481 wrap.innerHTML = `<div class="jnp-wa-ic"><div class="jnp-wa-ic-head">ð ${i18n.upcoming_events||'Upcoming events'}</div>${cards}</div>`; 482 483 // TTWidget buttons (when tt_occurrence_id is present) 484 wrap.querySelectorAll('.jnp-wa-ev-tt-btn').forEach(btn => { 485 btn.addEventListener('click', () => { 486 const bo = btn.dataset.boxoffice; 487 const ttId = parseInt(btn.dataset.ttId, 10); 488 if (window.TTWidget && typeof TTWidget.loadEventOccurrence === 'function' && bo && ttId) { 489 TTWidget.loadEventOccurrence(bo, ttId); 490 } else if (btn.dataset.href) { 491 window.open(btn.dataset.href, '_blank'); 492 } 493 }); 494 }); 495 496 // jnp-ticket-picker-btn â ticket-modal.js handles these via delegated events 497 // so no extra binding needed here. The buttons just need the correct 498 // data-event-id, data-occurrence-ts, data-event-title, data-thumbnail attributes. 499 500 // Bind in-chat reservation buttons 501 wrap.querySelectorAll('.jnp-wa-ev-reserve-btn').forEach(btn => { 502 btn.addEventListener('click', () => { 503 const eventName = btn.dataset.event; 504 const eventDate = btn.dataset.date; 505 // Pre-fill the input with a reservation request for this specific event 506 inputEl.value = `${i18n.reserve_prefix||'Vorrei prenotare un tavolo per'} ${eventName} (${eventDate})`; 507 sendBtn.disabled = false;
508 inputEl.focus(); 509 }); 510 }); 511 512 messagesEl.appendChild(wrap); 513 messagesEl.scrollTop = messagesEl.scrollHeight; 514 } 515 516 function appendContactForm(cfg) { 517 if (!cfg || !cfg.enabled) return; 518 const fields = cfg.fields || ['name','phone','message']; 519 const fieldHtml = fields.map(f => { 520 if (f === 'message') { 521 return `<div class="jnp-wa-cf-field"> 522 <label class="jnp-wa-cf-label">${f.charAt(0).toUpperCase()+f.slice(1)}</label> 523 <input class="jnp-wa-cf-input" type="text" data-field="${f}" placeholder="â¦"> 524 </div>`; 525 } 526 const type = f === 'email' ? 'email' : f === 'phone' ? 'tel' : 'text'; 527 return `<div class="jnp-wa-cf-field"> 528 <label class="jnp-wa-cf-label">${f.charAt(0).toUpperCase()+f.slice(1)}</label> 529 <input class="jnp-wa-cf-input" type="${type}" data-field="${f}" placeholder=""> 530 </div>`; 531 }).join(''); 532 533 const formId = 'jnp-cf-' + Date.now(); 534 appendCard(`<div class="jnp-wa-ic" id="${formId}"> 535 <div class="jnp-wa-ic-head">âï¸ ${escHtml(cfg.title || 'Contact us')}</div> 536 ${fieldHtml} 537 <button class="jnp-wa-card-btn jnp-wa-cf-submit">Send â</button> 538 <p class="jnp-wa-cf-result"></p> 539 </div>`); 540 541 // Bind submit after DOM insertion 542 setTimeout(() => { 543 const form = document.getElementById(formId); 544 if (!form) return; 545 form.querySelector('.jnp-wa-cf-submit').addEventListener('click', async () => { 546 const payload = { session_id: state.sessionId }; 547 form.querySelectorAll('.jnp-wa-cf-input').forEach(inp => {
548 payload[inp.dataset.field] = inp.value.trim(); 549 }); 550 const btn = form.querySelector('.jnp-wa-cf-submit'); 551 btn.disabled = true; 552 btn.textContent = i18n.sending||'Sendingâ¦'; 553 try { 554 const res = await fetch(API + 'jnp-wa/v1/contact-submit', { 555 method: 'POST', 556 headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': NONCE }, 557 body: JSON.stringify(payload), 558 }); 559 const data = await res.json(); 560 form.querySelector('.jnp-wa-cf-result').textContent = data.message || i18n.sent || 'Sent!'; 561 btn.style.display = 'none'; 562 } catch { 563 btn.disabled = false; 564 btn.textContent = i18n.send||'Send â'; 565 form.querySelector('.jnp-wa-cf-result').textContent = i18n.send_error||'Error â please try again.'; 566 } 567 }); 568 }, 100); 569 } 570 571 function appendCard(html) { 572 const div = document.createElement('div'); 573 div.className = 'jnp-wa-card-wrap'; 574 div.innerHTML = html; 575 messagesEl.appendChild(div); 576 messagesEl.scrollTop = messagesEl.scrollHeight; 577 } 578 579 function escAttr(s) { 580 return String(s).replace(/"/g, '"').replace(/'/g, '''); 581 } 582 583 584})();
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.