mirror of
https://github.com/Ichitux/lambada-fiesta-live.git
synced 2026-05-16 02:42:20 +02:00
Changes
This commit is contained in:
211
src/components/BookingSection.tsx
Normal file
211
src/components/BookingSection.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user