diff --git a/assets/controllers/form_controller.js b/assets/controllers/form_controller.js index 5c81119..88485c3 100644 --- a/assets/controllers/form_controller.js +++ b/assets/controllers/form_controller.js @@ -2,7 +2,7 @@ import { Controller } from "@hotwired/stimulus"; import { loadStripe } from "@stripe/stripe-js"; export default class extends Controller { - static targets = ["key", "submit", "firstname", "lastname", "email", "phone"]; + static targets = ["key", "submit", "firstname", "lastname", "email", "phone", "ticketType", "foodType", "note"]; stripe; @@ -13,39 +13,64 @@ export default class extends Controller { addEntry(event) { event.preventDefault(); - const formClone = this.element.querySelector("form").cloneNode(true); - - formClone.querySelectorAll("select").forEach((input) => { - input.value = "1"; + const template = this.element.querySelector(".forms").firstElementChild.cloneNode(true); + + template.querySelectorAll('select, input').forEach(element => { + element.value = ''; + if (element.id) { + element.id = `${element.id}_${Date.now()}`; + } }); - formClone.querySelector("input").value = ""; - this.element.querySelector(".forms").appendChild(formClone); + template.querySelectorAll('label').forEach(label => { + const forAttribute = label.getAttribute('for'); + if (forAttribute) { + label.setAttribute('for', `${forAttribute}_${Date.now()}`); + } + }); + + template.classList.add('opacity-0'); + this.element.querySelector(".forms").appendChild(template); + + requestAnimationFrame(() => { + template.classList.remove('opacity-0'); + template.classList.add('transition-all', 'duration-300'); + }); } removeEntry(event) { event.preventDefault(); - if (document.querySelector(".forms").childElementCount === 1) { + const forms = this.element.querySelector(".forms"); + if (forms.childElementCount <= 1) { return; } - event.target.closest("form").remove(); + const ticketElement = event.currentTarget.closest(".bg-white\\/80"); + if (!ticketElement) return; + + ticketElement.classList.add('opacity-0', 'transition-all', 'duration-300'); + setTimeout(() => { + ticketElement.remove(); + }, 300); } submit(event) { event.preventDefault(); + if (!this.validateForm()) { + return; + } this.submitTarget.querySelector("span").classList.add("hidden"); this.submitTarget.querySelector("svg").classList.remove("hidden"); - const forms = document.querySelectorAll("form"); + const tickets = this.element.querySelectorAll(".forms > div"); const personalData = this.getPersonalData(); - const ticketData = this.getTicketData(forms); + const ticketData = this.getTicketData(tickets); const formData = { personal: personalData, - tickets: ticketData, + tickets: ticketData }; fetch("/ticket/submit", { @@ -58,7 +83,7 @@ export default class extends Controller { if (!response.ok) { this.submitTarget.querySelector("svg").classList.add("hidden"); this.submitTarget.querySelector("span").classList.remove("hidden"); - alert("An error occurred"); + this.showError("Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut."); } else { response.json().then((data) => { this.stripe.redirectToCheckout({ @@ -69,29 +94,80 @@ export default class extends Controller { }); } - getTicketData(forms) { - const formData = []; - forms.forEach((form) => { - formData.push(this.extractFormData(form)); + validateForm() { + let isValid = true; + + ['firstname', 'lastname', 'email'].forEach(field => { + const target = this[`${field}Target`]; + if (!target.value.trim()) { + this.showFieldError(target); + isValid = false; + } }); - return formData; + this.element.querySelectorAll(".forms > div").forEach(ticket => { + const selects = ticket.querySelectorAll('select[required]'); + selects.forEach(select => { + if (!select.value) { + this.showFieldError(select); + isValid = false; + } + }); + }); + + return isValid; + } + + showFieldError(element) { + element.classList.add('border-red-500', 'focus:border-red-500', 'focus:ring-red-200'); + element.addEventListener('input', () => { + element.classList.remove('border-red-500', 'focus:border-red-500', 'focus:ring-red-200'); + }, { once: true }); + } + + showError(message) { + const errorDiv = document.createElement('div'); + errorDiv.className = 'fixed top-4 right-4 bg-red-50 border-l-4 border-red-500 p-4 rounded shadow-lg transform transition-all duration-500 opacity-0 translate-x-4'; + errorDiv.innerHTML = ` +
+
+ +
+
+

${message}

+
+
+ `; + + document.body.appendChild(errorDiv); + requestAnimationFrame(() => { + errorDiv.classList.remove('opacity-0', 'translate-x-4'); + }); + + setTimeout(() => { + errorDiv.classList.add('opacity-0', 'translate-x-4'); + setTimeout(() => errorDiv.remove(), 500); + }, 5000); + } + + getTicketData(tickets) { + return Array.from(tickets).map(ticket => this.extractTicketData(ticket)); } getPersonalData() { return { - firstname: this.firstnameTarget.value, - lastname: this.lastnameTarget.value, - email: this.emailTarget.value, - phone: this.phoneTarget.value, + firstname: this.firstnameTarget.value.trim(), + lastname: this.lastnameTarget.value.trim(), + email: this.emailTarget.value.trim(), + phone: this.phoneTarget.value.trim(), }; } - extractFormData(form) { + extractTicketData(ticket) { return { - ticket: parseInt(form.querySelector('select[name="ticket"]').value), - food: parseInt(form.querySelector('select[name="food"]').value), - note: form.querySelector('input[name="note"]').value, + ticket: parseInt(ticket.querySelector('select[name="ticket"]').value), + food: parseInt(ticket.querySelector('select[name="food"]').value), + note: ticket.querySelector('input[name="note"]').value.trim(), }; } } diff --git a/templates/ticket/_partials/_form.html.twig b/templates/ticket/_partials/_form.html.twig index 346d0ce..50dd28f 100644 --- a/templates/ticket/_partials/_form.html.twig +++ b/templates/ticket/_partials/_form.html.twig @@ -1,46 +1,66 @@ -
-
-
- - -
- -
-
- -
- - -
- +
+
+
+ +
+ + +
+ +
- - + +
+ + +
+ +
+
-
-
+ +
+