export const RsvFormSender = { get_form_url(form_id) { return ReservairServiceAPI.restUrl + '/form/' + form_id; }, clear_feedback(form) { form.querySelectorAll('.rsv-field-error').forEach(el => el.remove()); form.querySelectorAll('.rsv-invalid').forEach(el => el.classList.remove('rsv-invalid')); form.querySelector('.rsv-error-summary')?.remove(); }, show_errors(form, errors) { this.clear_feedback(form); const ul = document.createElement('ul'); for (const err of errors) { const li = document.createElement('li'); li.textContent = err.message; ul.appendChild(li); if (err.element) { const field = form.querySelector(`[name="${err.element}"]`); if (field) { field.classList.add('rsv-invalid'); const msg = document.createElement('span'); msg.classList.add('rsv-field-error'); msg.textContent = err.message; field.insertAdjacentElement('afterend', msg); } } } const summary = document.createElement('div'); summary.classList.add('rsv-error-summary'); summary.appendChild(ul); form.prepend(summary); }, show_success(form, _data) { const s = ReservairStrings.form; const wrapper = form.parentElement; const existing = Array.from(wrapper.children); const svgNS = 'http://www.w3.org/2000/svg'; const path = document.createElementNS(svgNS, 'path'); path.setAttribute('d', 'M6 14l6 6L22 8'); path.setAttribute('stroke', '#16a34a'); path.setAttribute('stroke-width', '2.5'); path.setAttribute('stroke-linecap', 'round'); path.setAttribute('stroke-linejoin', 'round'); const svg = document.createElementNS(svgNS, 'svg'); svg.setAttribute('width', '28'); svg.setAttribute('height', '28'); svg.setAttribute('viewBox', '0 0 28 28'); svg.setAttribute('fill', 'none'); svg.appendChild(path); const icon = document.createElement('div'); icon.className = 'rsv-success-icon'; icon.appendChild(svg); const body = this.build_success_body(form, s); const state = document.createElement('div'); state.className = 'rsv-success-state'; state.append(icon, body); const msg = document.createElement('div'); msg.appendChild(state); existing.forEach(child => child.style.display = 'none'); wrapper.appendChild(msg); // The form catches every data-rsv-reset button in the card and links it to // its cleanup — so new buttons just need the marker, no wiring here. const reset = () => { msg.remove(); form.reset(); // Native reset leaves custom controls untouched; reset the reservation // selectors so their slots, hidden inputs and the summary clear, the date // stays selected and availability reloads with the just-booked slots. form.querySelectorAll('rsv-reservation-selector').forEach(sel => sel.reset()); this.clear_feedback(form); existing.forEach(child => child.style.display = ''); }; state.querySelectorAll('[data-rsv-reset]').forEach(btn => btn.addEventListener('click', reset)); }, // Body of the success card. Uses the admin-configured template when the form // ships one, filling the .rsv-success-summary placeholder (expanded server-side // from ) with a snapshot of the selected slots; otherwise // falls back to the default text. build_success_body(form, strings) { const tpl = form.parentElement?.querySelector('template.rsv-form-success'); if (!tpl) { const subtitle = document.createElement('p'); subtitle.className = 'rsv-success-msg'; subtitle.textContent = strings.success_subtitle; return subtitle; } const body = document.createElement('div'); body.className = 'rsv-success-msg'; body.appendChild(tpl.content.cloneNode(true)); const placeholder = body.querySelector('.rsv-success-summary'); if (placeholder) { const summary = form.querySelector('rsv-reservation-summary'); if (summary && typeof summary.snapshot === 'function') { placeholder.replaceWith(summary.snapshot()); } else { placeholder.remove(); } } return body; }, set_loading(form, is_loading) { const btn = form.querySelector('button[type="submit"], button:not([type])'); if (!btn) return; btn.disabled = is_loading; btn.classList.toggle('rsv-loading', is_loading); }, encode_to_json(form) { const fields = form.querySelectorAll('.rsv-form-field'); const body = {}; fields.forEach(field => { const name = field.name ?? field.getAttribute('name'); try { body[name] = JSON.parse(field.value); } catch { body[name] = field.value; } }); return body; }, send_form(event) { event.preventDefault(); const form = event.target; this.clear_feedback(form); this.set_loading(form, true); const body = this.encode_to_json(form); fetch(this.get_form_url(form.id), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) .then(async response => { const data = await response.json().catch(() => null); if (!response.ok) throw { status: response.status, body: data }; return data; }) .then(data => { this.show_success(form, data); }) .catch(error => { const errors = error?.body?.errors ?? [{ element: '', message: ReservairStrings.form.error_generic }]; this.show_errors(form, errors); }) .finally(() => { this.set_loading(form, false); }); }, };