Design modifications
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { WEBHOOK_URL } from "@/data/event-data";
|
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
|
* FORMULARIO DE RESERVA
|
||||||
@@ -15,29 +15,98 @@ import { CheckCircle, Loader2, AlertCircle } from "lucide-react";
|
|||||||
* 3. Conecta el webhook a Google Sheets / Airtable / Email
|
* 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({
|
const bookingSchema = z.object({
|
||||||
|
requestId: z.string().optional(),
|
||||||
name: z.string().trim().min(2, "El nombre debe tener al menos 2 caracteres").max(100),
|
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),
|
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),
|
country: z.string().trim().min(2, "Indica tu país").max(100),
|
||||||
comment: z.string().max(500).optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type BookingData = z.infer<typeof bookingSchema>;
|
type BookingData = z.infer<typeof bookingSchema>;
|
||||||
|
|
||||||
const BookingSection = () => {
|
const BookingSection = () => {
|
||||||
const [form, setForm] = useState<BookingData>({
|
const [form, setForm] = useState<BookingData>({
|
||||||
name: "", email: "", seats: "1", country: "", comment: "",
|
requestId: "", name: "", surname: "", email: "", passType: "", amount: "1", price: 0, country: "",
|
||||||
});
|
});
|
||||||
const [errors, setErrors] = useState<Partial<Record<keyof BookingData, string>>>({});
|
const [errors, setErrors] = useState<Partial<Record<keyof BookingData, string>>>({});
|
||||||
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
const [status, setStatus] = useState<"idle" | "loading" | "success" | "error">("idle");
|
||||||
|
const [countrySearch, setCountrySearch] = useState("");
|
||||||
|
const [showCountryDropdown, setShowCountryDropdown] = useState(false);
|
||||||
|
const countryDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const countryInputRef = useRef<HTMLInputElement>(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<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||||
const { name, value } = e.target;
|
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 }));
|
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) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setErrors({});
|
setErrors({});
|
||||||
@@ -100,10 +169,10 @@ const BookingSection = () => {
|
|||||||
className="text-center mb-10"
|
className="text-center mb-10"
|
||||||
>
|
>
|
||||||
<h2 className="font-display text-4xl md:text-5xl font-bold text-gradient mb-4">
|
<h2 className="font-display text-4xl md:text-5xl font-bold text-gradient mb-4">
|
||||||
Reservar Asiento
|
Reserva tu pase
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Sin pago online. Solo reserva tu plaza y paga en el evento.
|
Selecciona tu pase y cantidad. Sin pago online, paga en el evento.
|
||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
@@ -114,17 +183,35 @@ const BookingSection = () => {
|
|||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
className="bg-card rounded-2xl p-6 md:p-10 shadow-elevated space-y-5"
|
className="bg-card rounded-2xl p-6 md:p-10 shadow-elevated space-y-5"
|
||||||
>
|
>
|
||||||
{/* Nombre */}
|
{/* Request ID - Hidden field for internal tracking */}
|
||||||
<div>
|
<input type="hidden" name="requestId" value={form.requestId} onChange={handleChange} />
|
||||||
<label className="block text-sm font-medium text-foreground mb-1.5">Nombre *</label>
|
|
||||||
<input
|
<div className="grid grid-cols-2 gap-4">
|
||||||
name="name"
|
{/* Nombre */}
|
||||||
value={form.name}
|
<div>
|
||||||
onChange={handleChange}
|
<label className="block text-sm font-medium text-foreground mb-1.5">Nombre *</label>
|
||||||
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"
|
<input
|
||||||
placeholder="Tu nombre completo"
|
name="name"
|
||||||
/>
|
value={form.name}
|
||||||
{errors.name && <p className="text-destructive text-xs mt-1">{errors.name}</p>}
|
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"
|
||||||
|
/>
|
||||||
|
{errors.name && <p className="text-destructive text-xs mt-1">{errors.name}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Apellido */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-foreground mb-1.5">Apellido *</label>
|
||||||
|
<input
|
||||||
|
name="surname"
|
||||||
|
value={form.surname}
|
||||||
|
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 apellido"
|
||||||
|
/>
|
||||||
|
{errors.surname && <p className="text-destructive text-xs mt-1">{errors.surname}</p>}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Email */}
|
{/* Email */}
|
||||||
@@ -141,50 +228,154 @@ const BookingSection = () => {
|
|||||||
{errors.email && <p className="text-destructive text-xs mt-1">{errors.email}</p>}
|
{errors.email && <p className="text-destructive text-xs mt-1">{errors.email}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
{/* País - Searchable Selector */}
|
||||||
{/* Plazas */}
|
<div className="space-y-2">
|
||||||
<div>
|
<label className="block text-sm font-medium text-foreground">País *</label>
|
||||||
<label className="block text-sm font-medium text-foreground mb-1.5">Plazas *</label>
|
<div className="relative" ref={countryInputRef}>
|
||||||
<select
|
<div className="relative">
|
||||||
name="seats"
|
<input
|
||||||
value={form.seats}
|
type="text"
|
||||||
onChange={handleChange}
|
value={countrySearch}
|
||||||
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"
|
onChange={(e) => {
|
||||||
>
|
setCountrySearch(e.target.value);
|
||||||
{[1, 2, 3, 4, 5].map((n) => (
|
setShowCountryDropdown(true);
|
||||||
<option key={n} value={String(n)}>{n}</option>
|
}}
|
||||||
))}
|
onFocus={() => setShowCountryDropdown(true)}
|
||||||
</select>
|
placeholder="Buscar país..."
|
||||||
{errors.seats && <p className="text-destructive text-xs mt-1">{errors.seats}</p>}
|
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"
|
||||||
</div>
|
/>
|
||||||
|
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||||
|
<Search className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showCountryDropdown && (
|
||||||
|
<motion.div
|
||||||
|
ref={countryDropdownRef}
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="absolute z-10 mt-1 w-full rounded-lg border border-input bg-popover shadow-lg max-h-60 overflow-auto"
|
||||||
|
>
|
||||||
|
{filteredCountries.length > 0 ? (
|
||||||
|
filteredCountries.map((country) => (
|
||||||
|
<div
|
||||||
|
key={country}
|
||||||
|
onClick={() => handleCountrySelect(country)}
|
||||||
|
className="px-4 py-2 text-sm text-foreground hover:bg-accent hover:text-accent-foreground cursor-pointer"
|
||||||
|
>
|
||||||
|
{country}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="px-4 py-2 text-sm text-muted-foreground">
|
||||||
|
No se encontraron países
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.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>}
|
{errors.country && <p className="text-destructive text-xs mt-1">{errors.country}</p>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Comentarios */}
|
{/* Tipo de Pass - Cards */}
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<label className="block text-sm font-medium text-foreground mb-1.5">Comentarios</label>
|
<label className="block text-sm font-medium text-foreground mb-3">Tipo de Pass *</label>
|
||||||
<textarea
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
name="comment"
|
{PASS_TYPES.map((pass) => (
|
||||||
value={form.comment}
|
<motion.div
|
||||||
onChange={handleChange}
|
key={pass.id}
|
||||||
rows={3}
|
whileHover={{ scale: 1.02 }}
|
||||||
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"
|
whileTap={{ scale: 0.98 }}
|
||||||
placeholder="¿Algo que quieras comentarnos?"
|
className={`cursor-pointer rounded-xl border-2 p-4 transition-all duration-200 ${
|
||||||
/>
|
form.passType === pass.id
|
||||||
|
? "border-primary bg-primary/5 shadow-lg"
|
||||||
|
: "border-border hover:border-primary/50 hover:shadow-md"
|
||||||
|
}`}
|
||||||
|
onClick={() => handleChange({ target: { name: "passType", value: pass.id } } as any)}
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<h3 className="font-semibold text-lg text-foreground mb-1">{pass.name}</h3>
|
||||||
|
<p className="text-2xl font-bold text-primary">{pass.price}€</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{errors.passType && <p className="text-destructive text-xs mt-2">{errors.passType}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Cantidad - Add/Subtract Buttons */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="block text-sm font-medium text-foreground">Cantidad *</label>
|
||||||
|
<div className="flex items-center justify-center gap-4">
|
||||||
|
<motion.button
|
||||||
|
type="button"
|
||||||
|
whileHover={{ scale: form.passType ? 1.05 : 1 }}
|
||||||
|
whileTap={{ scale: form.passType ? 0.95 : 1 }}
|
||||||
|
onClick={() => {
|
||||||
|
if (!form.passType) return;
|
||||||
|
const currentAmount = parseInt(form.amount);
|
||||||
|
if (currentAmount > 1) {
|
||||||
|
handleChange({ target: { name: "amount", value: String(currentAmount - 1) } } as any);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`w-12 h-12 rounded-full flex items-center justify-center text-xl font-bold transition-colors ${
|
||||||
|
form.passType
|
||||||
|
? "bg-secondary hover:bg-secondary/80 text-foreground cursor-pointer"
|
||||||
|
: "bg-muted text-muted-foreground cursor-not-allowed"
|
||||||
|
}`}
|
||||||
|
disabled={!form.passType || parseInt(form.amount) <= 1}
|
||||||
|
>
|
||||||
|
−
|
||||||
|
</motion.button>
|
||||||
|
|
||||||
|
<div className={`w-16 h-12 rounded-lg border flex items-center justify-center text-lg font-semibold transition-colors ${
|
||||||
|
form.passType
|
||||||
|
? "bg-background border-input text-foreground"
|
||||||
|
: "bg-muted border-muted text-muted-foreground"
|
||||||
|
}`}>
|
||||||
|
{form.amount}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.button
|
||||||
|
type="button"
|
||||||
|
whileHover={{ scale: form.passType ? 1.05 : 1 }}
|
||||||
|
whileTap={{ scale: form.passType ? 0.95 : 1 }}
|
||||||
|
onClick={() => {
|
||||||
|
if (!form.passType) return;
|
||||||
|
const currentAmount = parseInt(form.amount);
|
||||||
|
if (currentAmount < 10) {
|
||||||
|
handleChange({ target: { name: "amount", value: String(currentAmount + 1) } } as any);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`w-12 h-12 rounded-full flex items-center justify-center text-xl font-bold transition-colors ${
|
||||||
|
form.passType
|
||||||
|
? "bg-secondary hover:bg-secondary/80 text-foreground cursor-pointer"
|
||||||
|
: "bg-muted text-muted-foreground cursor-not-allowed"
|
||||||
|
}`}
|
||||||
|
disabled={!form.passType || parseInt(form.amount) >= 10}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</motion.button>
|
||||||
|
</div>
|
||||||
|
{errors.amount && <p className="text-destructive text-xs text-center mt-2">{errors.amount}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Precio Total - Beautiful bottom display */}
|
||||||
|
{form.price > 0 && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-gradient-to-r from-primary/10 to-primary/5 rounded-xl p-6 border border-primary/20"
|
||||||
|
>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm text-muted-foreground mb-1">Precio Total</p>
|
||||||
|
<p className="text-3xl font-bold text-primary">{form.price.toFixed(2)}€</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-2">Sin pago online • Paga en el evento</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
{status === "error" && (
|
{status === "error" && (
|
||||||
<div className="flex items-center gap-2 text-destructive text-sm bg-destructive/10 rounded-lg p-3">
|
<div className="flex items-center gap-2 text-destructive text-sm bg-destructive/10 rounded-lg p-3">
|
||||||
<AlertCircle className="w-4 h-4" />
|
<AlertCircle className="w-4 h-4" />
|
||||||
@@ -202,7 +393,7 @@ const BookingSection = () => {
|
|||||||
{status === "loading" ? (
|
{status === "loading" ? (
|
||||||
<><Loader2 className="w-4 h-4 animate-spin" /> Enviando...</>
|
<><Loader2 className="w-4 h-4 animate-spin" /> Enviando...</>
|
||||||
) : (
|
) : (
|
||||||
"Reservar Asiento"
|
"Reservar Pass"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</motion.form>
|
</motion.form>
|
||||||
@@ -212,3 +403,32 @@ const BookingSection = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default BookingSection;
|
export default BookingSection;
|
||||||
|
|
||||||
|
// Country data for searchable selector
|
||||||
|
const COUNTRIES = [
|
||||||
|
"Afganistán", "Albania", "Alemania", "Andorra", "Angola", "Antigua y Barbuda", "Arabia Saudita", "Argelia",
|
||||||
|
"Argentina", "Armenia", "Australia", "Austria", "Azerbaiyán", "Bahamas", "Bangladés", "Barbados", "Baréin",
|
||||||
|
"Bélgica", "Belice", "Benín", "Bielorrusia", "Birmania", "Bolivia", "Bosnia y Herzegovina", "Botsuana",
|
||||||
|
"Brasil", "Brunéi", "Bulgaria", "Burkina Faso", "Burundi", "Bután", "Cabo Verde", "Camboya", "Camerún",
|
||||||
|
"Canadá", "Catar", "Chad", "Chile", "China", "Chipre", "Ciudad del Vaticano", "Colombia", "Comoras",
|
||||||
|
"Corea del Norte", "Corea del Sur", "Costa de Marfil", "Costa Rica", "Croacia", "Cuba", "Dinamarca",
|
||||||
|
"Dominica", "Ecuador", "Egipto", "El Salvador", "Emiratos Árabes Unidos", "Eritrea", "Eslovaquia",
|
||||||
|
"Eslovenia", "España", "Estados Unidos", "Estonia", "Etiopía", "Filipinas", "Finlandia", "Fiyi",
|
||||||
|
"Francia", "Gabón", "Gambia", "Georgia", "Ghana", "Granada", "Grecia", "Guatemala", "Guyana",
|
||||||
|
"Guinea", "Guinea-Bisáu", "Guinea Ecuatorial", "Haití", "Honduras", "Hungría", "India", "Indonesia",
|
||||||
|
"Irak", "Irán", "Irlanda", "Islandia", "Islas Marshall", "Islas Salomón", "Israel", "Italia",
|
||||||
|
"Jamaica", "Japón", "Jordania", "Kazajistán", "Kenia", "Kirguistán", "Kiribati", "Kuwait", "Laos",
|
||||||
|
"Lesoto", "Letonia", "Líbano", "Liberia", "Libia", "Liechtenstein", "Lituania", "Luxemburgo",
|
||||||
|
"Madagascar", "Malasia", "Malaui", "Maldivas", "Malí", "Malta", "Marruecos", "Mauricio", "Mauritania",
|
||||||
|
"México", "Micronesia", "Moldavia", "Mónaco", "Mongolia", "Montenegro", "Mozambique", "Namibia",
|
||||||
|
"Nauru", "Nepal", "Nicaragua", "Níger", "Nigeria", "Noruega", "Nueva Zelanda", "Omán", "Países Bajos",
|
||||||
|
"Pakistán", "Palaos", "Panamá", "Papúa Nueva Guinea", "Paraguay", "Perú", "Polonia", "Portugal",
|
||||||
|
"Reino Unido", "República Centroafricana", "República Checa", "República del Congo", "República Democrática del Congo",
|
||||||
|
"República Dominicana", "Ruanda", "Rumanía", "Rusia", "Samoa", "San Cristóbal y Nieves", "San Marino",
|
||||||
|
"San Vicente y las Granadinas", "Santa Lucía", "Santo Tomé y Príncipe", "Senegal", "Serbia",
|
||||||
|
"Seychelles", "Sierra Leona", "Singapur", "Siria", "Somalia", "Sri Lanka", "Suazilandia", "Sudáfrica",
|
||||||
|
"Sudán", "Sudán del Sur", "Suecia", "Suiza", "Surinam", "Tailandia", "Tanzania", "Tayikistán",
|
||||||
|
"Timor Oriental", "Togo", "Tonga", "Trinidad y Tobago", "Túnez", "Turkmenistán", "Turquía",
|
||||||
|
"Tuvalu", "Ucrania", "Uganda", "Uruguay", "Uzbekistán", "Vanuatu", "Venezuela", "Vietnam",
|
||||||
|
"Yemen", "Yibuti", "Zambia", "Zimbabue"
|
||||||
|
];
|
||||||
|
|||||||
@@ -28,7 +28,22 @@ const FloatingButton = () => {
|
|||||||
className="animate-pulse-glow rounded-full px-6 shadow-elevated"
|
className="animate-pulse-glow rounded-full px-6 shadow-elevated"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<a href="#booking">🎶 Reservar</a>
|
<a href="#booking" className="inline-flex items-center gap-2">
|
||||||
|
{/* Inline SVG: dancing couple icon */}
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="w-5 h-5"
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<path d="M7.5 4.75a2.25 2.25 0 1 1 4.5 0 2.25 2.25 0 0 1-4.5 0Z" />
|
||||||
|
<path d="M4.25 20a.75.75 0 0 1-.743-.653l-.23-1.61a3.75 3.75 0 0 1 1.64-3.67l2.41-1.6a1.5 1.5 0 0 0 .67-1.1l.16-1.3a2.25 2.25 0 0 1 2.22-1.97h.2a2.25 2.25 0 0 1 1.98 1.19l.54 1.03 1.66.83a3 3 0 0 1 1.33 1.22l1.76 2.89c.23.38.09.87-.29 1.1a.8.8 0 0 1-1.08-.28l-1.47-2.42a1.5 1.5 0 0 0-.67-.61l-1.63-.81-.25 1.77a2.25 2.25 0 0 1-1.01 1.55l-2.98 2a2.25 2.25 0 0 0-.98 1.64l-.06.92A.75.75 0 0 1 6 20H4.25Z" />
|
||||||
|
<path d="M14.75 6.5a1 1 0 0 1 1.5-.86l2 .99a2.5 2.5 0 0 1 1.12 3.26l-.37.78a.75.75 0 1 1-1.36-.64l.37-.78a1 1 0 0 0-.45-1.3l-1.99-.99a1 1 0 0 1-.32-1.46Z" />
|
||||||
|
</svg>
|
||||||
|
Reservar
|
||||||
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
<button
|
<button
|
||||||
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
|
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ const HeroSection = () => {
|
|||||||
transition={{ delay: 1 }}
|
transition={{ delay: 1 }}
|
||||||
>
|
>
|
||||||
<Button variant="hero" size="lg" className="text-lg px-10 py-6" asChild>
|
<Button variant="hero" size="lg" className="text-lg px-10 py-6" asChild>
|
||||||
<a href="#booking">Reservar Asiento</a>
|
<a href="#booking">Reservar tu pase</a>
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const Navbar = () => {
|
|||||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||||
scrolled
|
scrolled
|
||||||
? "bg-background/95 backdrop-blur-md shadow-card"
|
? "bg-background/95 backdrop-blur-md shadow-card"
|
||||||
: "bg-transparent"
|
: "bg-white/30"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="container mx-auto flex items-center justify-between px-4 py-3">
|
<div className="container mx-auto flex items-center justify-between px-4 py-3">
|
||||||
|
|||||||
@@ -10,15 +10,17 @@ const roleBadgeClass: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const StaffSection = () => (
|
const StaffSection = () => (
|
||||||
<section id="staff" className="section-padding bg-background">
|
<section id="staff" className="section-padding relative overflow-hidden">
|
||||||
<div className="container mx-auto">
|
{/* subtle radial background to match site accents */}
|
||||||
|
<div className="pointer-events-none absolute inset-0 bg-gradient-to-b from-background to-background/60" />
|
||||||
|
<div className="relative container mx-auto w-[90%]">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
className="text-center mb-12"
|
className="text-center mb-12"
|
||||||
>
|
>
|
||||||
<h2 className="font-display text-4xl md:text-5xl font-bold text-gradient mb-4">
|
<h2 className="font-display text-4xl md:text-5xl font-bold bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent mb-4">
|
||||||
Staff del Evento
|
Staff del Evento
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-muted-foreground max-w-2xl mx-auto">
|
<p className="text-muted-foreground max-w-2xl mx-auto">
|
||||||
@@ -26,53 +28,50 @@ const StaffSection = () => (
|
|||||||
</p>
|
</p>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div className="mx-auto grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{STAFF.map((member, i) => (
|
{STAFF.slice(0, 1).map((member, i) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={member.id}
|
key={member.id}
|
||||||
initial={{ opacity: 0, y: 30 }}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
whileInView={{ opacity: 1, y: 0 }}
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
viewport={{ once: true }}
|
viewport={{ once: true }}
|
||||||
transition={{ delay: i * 0.1 }}
|
transition={{ delay: i * 0.1 }}
|
||||||
className="bg-card rounded-2xl overflow-hidden shadow-card hover:shadow-elevated transition-shadow group"
|
className="group rounded-2xl border border-border/50 bg-card/80 backdrop-blur supports-[backdrop-filter]:bg-card/60 shadow-sm hover:shadow-xl transition-all duration-300 mx-auto w-full max-w-xl p-5"
|
||||||
>
|
>
|
||||||
{/* Foto */}
|
{/* Foto */}
|
||||||
<div className="aspect-square bg-muted flex items-center justify-center overflow-hidden">
|
<div className="relative aspect-square rounded-xl overflow-hidden bg-muted">
|
||||||
{member.image ? (
|
{member.image ? (
|
||||||
<img
|
<img
|
||||||
src={member.image}
|
src={member.image}
|
||||||
alt={member.name}
|
alt={member.name}
|
||||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
|
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<User className="w-16 h-16 text-muted-foreground/40" />
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<User className="w-16 h-16 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{/* top gradient sheen */}
|
||||||
|
<div className="pointer-events-none absolute inset-x-0 top-0 h-24 bg-gradient-to-b from-background/40 to-transparent" />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-center mt-5">
|
||||||
<div className="p-5">
|
<div className="mb-2 flex items-center justify-center gap-2">
|
||||||
{/* Badge de rol */}
|
<span className={`inline-flex items-center rounded-full px-3 py-1 text-xs font-medium ${roleBadgeClass[member.role] ?? "bg-muted text-muted-foreground"}`}>
|
||||||
<span
|
{member.role}
|
||||||
className={`inline-block text-xs font-semibold px-3 py-1 rounded-full mb-3 ${
|
</span>
|
||||||
roleBadgeClass[member.role] || "bg-muted text-muted-foreground"
|
</div>
|
||||||
}`}
|
<h3 className="text-xl font-semibold text-foreground mb-1 tracking-tight">
|
||||||
>
|
|
||||||
{member.role}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<h3 className="font-display text-lg font-bold text-foreground mb-2">
|
|
||||||
{member.name}
|
{member.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-muted-foreground mb-4">
|
<p className="text-sm text-muted-foreground mb-4">
|
||||||
{member.description}
|
{member.subtitle ?? ""}
|
||||||
</p>
|
</p>
|
||||||
|
{member.instagram && (
|
||||||
{/* Redes sociales */}
|
|
||||||
{member.socials?.instagram && (
|
|
||||||
<a
|
<a
|
||||||
href={member.socials.instagram}
|
href={member.instagram}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center gap-1.5 text-sm text-primary hover:text-primary/80 transition-colors"
|
className="inline-flex items-center gap-2 rounded-full border border-border px-4 py-2 text-sm text-foreground/90 hover:text-foreground hover:border-primary/50 hover:bg-primary/5 transition-colors"
|
||||||
>
|
>
|
||||||
<Instagram className="w-4 h-4" />
|
<Instagram className="w-4 h-4" />
|
||||||
Instagram
|
Instagram
|
||||||
|
|||||||
Reference in New Issue
Block a user