This commit is contained in:
gpt-engineer-app[bot]
2026-03-05 15:50:22 +00:00
parent b331aa1a7d
commit a11683b7dc
23 changed files with 2958 additions and 96 deletions

View File

@@ -0,0 +1,211 @@
import { useState } 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";
/**
* FORMULARIO DE RESERVA
*
* Envía un POST al webhook de n8n con los datos del formulario.
* Para configurar:
* 1. En n8n, crea un workflow con nodo "Webhook" (POST)
* 2. Pega la URL generada en WEBHOOK_URL (event-data.ts)
* 3. Conecta el webhook a Google Sheets / Airtable / Email
*/
const bookingSchema = z.object({
name: z.string().trim().min(2, "El nombre 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"),
country: z.string().trim().min(2, "Indica tu país").max(100),
comment: z.string().max(500).optional(),
});
type BookingData = z.infer<typeof bookingSchema>;
const BookingSection = () => {
const [form, setForm] = useState<BookingData>({
name: "", email: "", seats: "1", country: "", comment: "",
});
const [errors, setErrors] = useState<Partial<Record<keyof BookingData, string>>>({});
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
const { name, value } = e.target;
setForm((prev) => ({ ...prev, [name]: value }));
setErrors((prev) => ({ ...prev, [name]: undefined }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setErrors({});
const result = bookingSchema.safeParse(form);
if (!result.success) {
const fieldErrors: typeof errors = {};
result.error.errors.forEach((err) => {
const field = err.path[0] as keyof BookingData;
fieldErrors[field] = err.message;
});
setErrors(fieldErrors);
return;
}
setStatus("loading");
try {
const res = await fetch(WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(result.data),
});
if (!res.ok) throw new Error("Error en la solicitud");
setStatus("success");
} catch {
setStatus("error");
}
};
if (status === "success") {
return (
<section id="booking" className="section-padding bg-background">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
className="max-w-lg mx-auto text-center bg-card rounded-2xl p-10 shadow-elevated"
>
<CheckCircle className="w-16 h-16 text-primary mx-auto mb-4" />
<h3 className="font-display text-2xl font-bold text-foreground mb-2">
¡Reserva recibida!
</h3>
<p className="text-muted-foreground">
Recibirás confirmación por email. ¡Nos vemos en la pista!
</p>
</motion.div>
</section>
);
}
return (
<section id="booking" className="section-padding bg-background">
<div className="container mx-auto max-w-2xl">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-10"
>
<h2 className="font-display text-4xl md:text-5xl font-bold text-gradient mb-4">
Reservar Asiento
</h2>
<p className="text-muted-foreground">
Sin pago online. Solo reserva tu plaza y paga en el evento.
</p>
</motion.div>
<motion.form
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
onSubmit={handleSubmit}
className="bg-card rounded-2xl p-6 md:p-10 shadow-elevated space-y-5"
>
{/* Nombre */}
<div>
<label className="block text-sm font-medium text-foreground mb-1.5">Nombre *</label>
<input
name="name"
value={form.name}
onChange={handleChange}
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"
placeholder="Tu nombre completo"
/>
{errors.name && <p className="text-destructive text-xs mt-1">{errors.name}</p>}
</div>
{/* Email */}
<div>
<label className="block text-sm font-medium text-foreground mb-1.5">Email *</label>
<input
name="email"
type="email"
value={form.email}
onChange={handleChange}
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"
placeholder="tu@email.com"
/>
{errors.email && <p className="text-destructive text-xs mt-1">{errors.email}</p>}
</div>
<div className="grid grid-cols-2 gap-4">
{/* Plazas */}
<div>
<label className="block text-sm font-medium text-foreground mb-1.5">Plazas *</label>
<select
name="seats"
value={form.seats}
onChange={handleChange}
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"
>
{[1, 2, 3, 4, 5].map((n) => (
<option key={n} value={String(n)}>{n}</option>
))}
</select>
{errors.seats && <p className="text-destructive text-xs mt-1">{errors.seats}</p>}
</div>
{/* País */}
<div>
<label className="block text-sm font-medium text-foreground mb-1.5">País *</label>
<input
name="country"
value={form.country}
onChange={handleChange}
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"
placeholder="España"
/>
{errors.country && <p className="text-destructive text-xs mt-1">{errors.country}</p>}
</div>
</div>
{/* Comentarios */}
<div>
<label className="block text-sm font-medium text-foreground mb-1.5">Comentarios</label>
<textarea
name="comment"
value={form.comment}
onChange={handleChange}
rows={3}
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 resize-none"
placeholder="¿Algo que quieras comentarnos?"
/>
</div>
{status === "error" && (
<div className="flex items-center gap-2 text-destructive text-sm bg-destructive/10 rounded-lg p-3">
<AlertCircle className="w-4 h-4" />
Error al enviar. Inténtalo de nuevo.
</div>
)}
<Button
type="submit"
variant="hero"
size="lg"
className="w-full text-base py-6"
disabled={status === "loading"}
>
{status === "loading" ? (
<><Loader2 className="w-4 h-4 animate-spin" /> Enviando...</>
) : (
"Reservar Asiento"
)}
</Button>
</motion.form>
</div>
</section>
);
};
export default BookingSection;