Files
Reservair/assets/js/forms/RsvFormSender.js
2026-06-12 11:25:57 +02:00

143 lines
4.3 KiB
JavaScript

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 = 'success-icon';
icon.appendChild(svg);
const title = document.createElement('div');
title.className = 'success-title';
title.textContent = s.success_title;
const subtitle = document.createElement('p');
subtitle.className = 'success-msg';
subtitle.textContent = s.success_subtitle;
const reset_btn = document.createElement('button');
reset_btn.className = 'reset-btn';
reset_btn.textContent = s.new_reservation;
const state = document.createElement('div');
state.className = 'success-state';
state.append(icon, title, subtitle, reset_btn);
const msg = document.createElement('div');
msg.appendChild(state);
existing.forEach(child => child.style.display = 'none');
wrapper.appendChild(msg);
reset_btn.addEventListener('click', () => {
msg.remove();
form.reset();
this.clear_feedback(form);
existing.forEach(child => child.style.display = '');
});
},
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);
});
},
};