From a0855bb203f11b009cc8178bdd812b844f881f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoni=20Nu=C3=B1ez=20Romeu?= Date: Wed, 11 Mar 2026 18:23:04 +0100 Subject: [PATCH] Design modifications --- src/components/BookingSection.tsx | 336 ++++++++++++++++++++++++------ src/components/FloatingButton.tsx | 17 +- src/components/HeroSection.tsx | 2 +- src/components/Navbar.tsx | 2 +- src/components/StaffSection.tsx | 53 +++-- 5 files changed, 322 insertions(+), 88 deletions(-) diff --git a/src/components/BookingSection.tsx b/src/components/BookingSection.tsx index e4502f4..5dfb736 100644 --- a/src/components/BookingSection.tsx +++ b/src/components/BookingSection.tsx @@ -1,9 +1,9 @@ -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { motion } from "framer-motion"; import { z } from "zod"; import { Button } from "@/components/ui/button"; import { WEBHOOK_URL } from "@/data/event-data"; -import { CheckCircle, Loader2, AlertCircle } from "lucide-react"; +import { CheckCircle, Loader2, AlertCircle, Search } from "lucide-react"; /** * FORMULARIO DE RESERVA @@ -15,29 +15,98 @@ import { CheckCircle, Loader2, AlertCircle } from "lucide-react"; * 3. Conecta el webhook a Google Sheets / Airtable / Email */ +// Define pass types with prices +const PASS_TYPES = [ + { id: "full", name: "Full Pass", price: 150 }, + { id: "party", name: "Party Pass", price: 80 }, + { id: "single", name: "Single Day Pass", price: 40 }, +]; + const bookingSchema = z.object({ + requestId: z.string().optional(), name: z.string().trim().min(2, "El nombre debe tener al menos 2 caracteres").max(100), + surname: z.string().trim().min(2, "El apellido debe tener al menos 2 caracteres").max(100), email: z.string().trim().email("Email no válido").max(255), - seats: z.string().min(1, "Selecciona el número de plazas"), + passType: z.string().min(1, "Selecciona un tipo de pass"), + amount: z.string().min(1, "Selecciona la cantidad"), + price: z.number().optional(), country: z.string().trim().min(2, "Indica tu país").max(100), - comment: z.string().max(500).optional(), }); type BookingData = z.infer; const BookingSection = () => { const [form, setForm] = useState({ - name: "", email: "", seats: "1", country: "", comment: "", + requestId: "", name: "", surname: "", email: "", passType: "", amount: "1", price: 0, country: "", }); const [errors, setErrors] = useState>>({}); const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle"); + const [countrySearch, setCountrySearch] = useState(""); + const [showCountryDropdown, setShowCountryDropdown] = useState(false); + const countryDropdownRef = useRef(null); + const countryInputRef = useRef(null); + // Generate unique requestId on component mount + useEffect(() => { + const uniqueId = `REQ-${Date.now()}-${Math.floor(Math.random() * 10000)}`; + setForm(prev => ({ ...prev, requestId: uniqueId })); + }, []); const handleChange = (e: React.ChangeEvent) => { const { name, value } = e.target; - setForm((prev) => ({ ...prev, [name]: value })); + + // Calculate price when passType or amount changes + if (name === "passType" || name === "amount") { + // Update the form state first + setForm((prev) => ({ ...prev, [name]: value })); + + // Get the updated form values + const updatedForm = { ...form, [name]: value }; + + // Calculate new price if both passType and amount are set + if ((updatedForm.passType || name === "passType") && (updatedForm.amount || name === "amount")) { + const selectedPass = PASS_TYPES.find(pass => pass.id === (name === "passType" ? value : updatedForm.passType)); + if (selectedPass) { + const amountValue = name === "amount" ? parseInt(value) : parseInt(updatedForm.amount); + const newPrice = selectedPass.price * amountValue; + + // Update the form with the calculated price + setForm((prev) => ({ ...prev, [name]: value, price: newPrice })); + } + } + } else { + setForm((prev) => ({ ...prev, [name]: value })); + } + setErrors((prev) => ({ ...prev, [name]: undefined })); }; + const handleCountrySelect = (country: string) => { + setForm((prev) => ({ ...prev, country })); + setCountrySearch(country); + setShowCountryDropdown(false); + setErrors((prev) => ({ ...prev, country: undefined })); + }; + + const filteredCountries = COUNTRIES.filter(country => + country.toLowerCase().includes(countrySearch.toLowerCase()) + ).slice(0, 10); // Limit to 10 results for performance + + // Close dropdown when clicking outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (showCountryDropdown && countryDropdownRef.current && countryInputRef.current) { + const target = e.target as Node; + + if (!countryDropdownRef.current.contains(target) && !countryInputRef.current.contains(target)) { + setShowCountryDropdown(false); + } + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [showCountryDropdown]); + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setErrors({}); @@ -100,10 +169,10 @@ const BookingSection = () => { className="text-center mb-10" >

- Reservar Asiento + Reserva tu pase

- Sin pago online. Solo reserva tu plaza y paga en el evento. + Selecciona tu pase y cantidad. Sin pago online, paga en el evento.

@@ -114,17 +183,35 @@ const BookingSection = () => { onSubmit={handleSubmit} className="bg-card rounded-2xl p-6 md:p-10 shadow-elevated space-y-5" > - {/* Nombre */} -
- - - {errors.name &&

{errors.name}

} + {/* Request ID - Hidden field for internal tracking */} + + +
+ {/* Nombre */} +
+ + + {errors.name &&

{errors.name}

} +
+ + {/* Apellido */} +
+ + + {errors.surname &&

{errors.surname}

} +
{/* Email */} @@ -141,50 +228,154 @@ const BookingSection = () => { {errors.email &&

{errors.email}

}
-
- {/* Plazas */} -
- - - {errors.seats &&

{errors.seats}

} -
- - {/* País */} -
- - + {/* País - Searchable Selector */} +
+ +
+
+ { + setCountrySearch(e.target.value); + setShowCountryDropdown(true); + }} + onFocus={() => setShowCountryDropdown(true)} + placeholder="Buscar país..." + className="w-full rounded-lg border border-input bg-background px-4 py-3 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring pr-10" + /> +
+ +
+
+ + {showCountryDropdown && ( + + {filteredCountries.length > 0 ? ( + filteredCountries.map((country) => ( +
handleCountrySelect(country)} + className="px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground cursor-pointer" + > + {country} +
+ )) + ) : ( +
+ No se encontraron países +
+ )} +
+ )} + {errors.country &&

{errors.country}

}
- {/* Comentarios */} -
- -