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,63 @@
import { motion } from "framer-motion";
import { ABOUT_EVENT } from "@/data/event-data";
import aboutImg from "@/assets/about-event.jpg";
import { Music, Users, Sparkles, PartyPopper } from "lucide-react";
const iconMap = [Music, Users, Sparkles, PartyPopper];
const AboutSection = () => (
<section id="about" className="section-padding bg-background">
<div className="container mx-auto">
<div className="grid md:grid-cols-2 gap-12 items-center">
{/* Imagen */}
<motion.div
initial={{ opacity: 0, x: -40 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<img
src={aboutImg}
alt="Evento de Lambada"
className="rounded-2xl shadow-elevated w-full object-cover aspect-square"
/>
</motion.div>
{/* Texto */}
<motion.div
initial={{ opacity: 0, x: 40 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="font-display text-4xl md:text-5xl font-bold mb-6 text-gradient">
{ABOUT_EVENT.title}
</h2>
<p className="text-muted-foreground mb-6 leading-relaxed whitespace-pre-line">
{ABOUT_EVENT.description}
</p>
<p className="text-foreground/90 mb-8 leading-relaxed italic border-l-4 border-primary pl-4">
{ABOUT_EVENT.lambadaInfo}
</p>
{/* Highlights */}
<div className="grid grid-cols-2 gap-4">
{ABOUT_EVENT.highlights.map((item, i) => {
const Icon = iconMap[i % iconMap.length];
return (
<div key={i} className="flex items-center gap-3 bg-card rounded-lg p-3">
<div className="bg-gradient-tropical rounded-full p-2 shrink-0">
<Icon className="w-4 h-4 text-primary-foreground" />
</div>
<span className="text-sm font-medium text-foreground">{item}</span>
</div>
);
})}
</div>
</motion.div>
</div>
</div>
</section>
);
export default AboutSection;

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;

View File

@@ -0,0 +1,46 @@
import { Button } from "@/components/ui/button";
import { motion, AnimatePresence } from "framer-motion";
import { useState, useEffect } from "react";
import { ChevronUp } from "lucide-react";
/** Botón flotante de reserva + scroll to top */
const FloatingButton = () => {
const [visible, setVisible] = useState(false);
useEffect(() => {
const onScroll = () => setVisible(window.scrollY > 400);
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
<AnimatePresence>
{visible && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 20 }}
className="fixed bottom-6 right-6 z-50 flex flex-col gap-2"
>
<Button
variant="hero"
size="lg"
className="animate-pulse-glow rounded-full px-6 shadow-elevated"
asChild
>
<a href="#booking">🎶 Reservar</a>
</Button>
<button
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
className="self-center bg-foreground/20 backdrop-blur-sm p-2 rounded-full hover:bg-foreground/30 transition-colors"
aria-label="Volver arriba"
>
<ChevronUp className="w-4 h-4 text-foreground" />
</button>
</motion.div>
)}
</AnimatePresence>
);
};
export default FloatingButton;

View File

@@ -0,0 +1,61 @@
import { ABOUT_ORG, FOOTER } from "@/data/event-data";
import { Instagram, Facebook, Youtube, Mail } from "lucide-react";
const FooterSection = () => (
<footer className="bg-foreground text-primary-foreground">
<div className="container mx-auto px-4 py-12">
<div className="grid md:grid-cols-3 gap-8 mb-8">
{/* Brand */}
<div>
<h3 className="font-display text-2xl font-bold mb-3">ZoukLambadaBCN</h3>
<p className="text-primary-foreground/70 text-sm">
Comunidad de baile en Barcelona.
</p>
</div>
{/* Contacto */}
<div>
<h4 className="font-display text-lg font-semibold mb-3">Contacto</h4>
<a
href={`mailto:${FOOTER.email}`}
className="flex items-center gap-2 text-sm text-primary-foreground/70 hover:text-primary transition-colors"
>
<Mail className="w-4 h-4" />
{FOOTER.email}
</a>
</div>
{/* Redes */}
<div>
<h4 className="font-display text-lg font-semibold mb-3">Síguenos</h4>
<div className="flex gap-3">
{ABOUT_ORG.socials.instagram && (
<a href={ABOUT_ORG.socials.instagram} target="_blank" rel="noopener noreferrer" aria-label="Instagram"
className="bg-primary-foreground/10 p-2.5 rounded-full hover:bg-primary-foreground/20 transition-colors">
<Instagram className="w-5 h-5" />
</a>
)}
{ABOUT_ORG.socials.facebook && (
<a href={ABOUT_ORG.socials.facebook} target="_blank" rel="noopener noreferrer" aria-label="Facebook"
className="bg-primary-foreground/10 p-2.5 rounded-full hover:bg-primary-foreground/20 transition-colors">
<Facebook className="w-5 h-5" />
</a>
)}
{ABOUT_ORG.socials.youtube && (
<a href={ABOUT_ORG.socials.youtube} target="_blank" rel="noopener noreferrer" aria-label="YouTube"
className="bg-primary-foreground/10 p-2.5 rounded-full hover:bg-primary-foreground/20 transition-colors">
<Youtube className="w-5 h-5" />
</a>
)}
</div>
</div>
</div>
<div className="border-t border-primary-foreground/10 pt-6 text-center">
<p className="text-sm text-primary-foreground/50">{FOOTER.copyright}</p>
</div>
</div>
</footer>
);
export default FooterSection;

View File

@@ -0,0 +1,49 @@
import { motion } from "framer-motion";
import { GALLERY_IMAGES } from "@/data/event-data";
import { ImageIcon } from "lucide-react";
const GallerySection = () => (
<section id="gallery" className="section-padding bg-card">
<div className="container mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="font-display text-4xl md:text-5xl font-bold text-gradient mb-4">
Galería
</h2>
</motion.div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 max-w-5xl mx-auto">
{GALLERY_IMAGES.map((img, i) => (
<motion.div
key={i}
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ delay: i * 0.08 }}
className="aspect-square rounded-xl overflow-hidden bg-muted flex items-center justify-center"
>
{img.src ? (
<img
src={img.src}
alt={img.alt}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-500"
loading="lazy"
/>
) : (
<div className="text-center text-muted-foreground/40">
<ImageIcon className="w-10 h-10 mx-auto mb-2" />
<p className="text-xs">{img.alt}</p>
</div>
)}
</motion.div>
))}
</div>
</div>
</section>
);
export default GallerySection;

View File

@@ -0,0 +1,109 @@
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { EVENT_INFO } from "@/data/event-data";
import heroBg from "@/assets/hero-bg.jpg";
/** Calcula diferencia entre ahora y la fecha del evento */
const getTimeLeft = (target: string) => {
const diff = new Date(target).getTime() - Date.now();
if (diff <= 0) return { days: 0, hours: 0, minutes: 0, seconds: 0 };
return {
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
hours: Math.floor((diff / (1000 * 60 * 60)) % 24),
minutes: Math.floor((diff / (1000 * 60)) % 60),
seconds: Math.floor((diff / 1000) % 60),
};
};
const HeroSection = () => {
const [timeLeft, setTimeLeft] = useState(getTimeLeft(EVENT_INFO.date));
useEffect(() => {
const timer = setInterval(() => {
setTimeLeft(getTimeLeft(EVENT_INFO.date));
}, 1000);
return () => clearInterval(timer);
}, []);
const countdownItems = [
{ value: timeLeft.days, label: "Días" },
{ value: timeLeft.hours, label: "Horas" },
{ value: timeLeft.minutes, label: "Min" },
{ value: timeLeft.seconds, label: "Seg" },
];
return (
<section className="relative min-h-screen flex items-center justify-center overflow-hidden">
{/* Background image */}
<div
className="absolute inset-0 bg-cover bg-center"
style={{ backgroundImage: `url(${heroBg})` }}
/>
{/* Overlay */}
<div className="absolute inset-0 bg-gradient-to-b from-foreground/70 via-foreground/50 to-foreground/80" />
<div className="relative z-10 text-center px-4 max-w-4xl mx-auto">
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
className="text-primary font-body text-sm uppercase tracking-[0.3em] mb-4"
>
{EVENT_INFO.dateDisplay} · {EVENT_INFO.city}
</motion.p>
<motion.h1
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.4 }}
className="font-display text-5xl md:text-7xl lg:text-8xl font-bold text-primary-foreground mb-4 leading-tight"
>
{EVENT_INFO.name}
</motion.h1>
<motion.p
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.6 }}
className="text-primary-foreground/80 font-body text-lg md:text-xl mb-10"
>
{EVENT_INFO.subtitle}
</motion.p>
{/* Countdown */}
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.8 }}
className="flex justify-center gap-4 md:gap-8 mb-10"
>
{countdownItems.map((item) => (
<div key={item.label} className="text-center">
<div className="bg-primary-foreground/10 backdrop-blur-sm border border-primary-foreground/20 rounded-lg px-4 py-3 md:px-6 md:py-4 min-w-[60px] md:min-w-[80px]">
<span className="text-2xl md:text-4xl font-display font-bold text-primary-foreground">
{String(item.value).padStart(2, "0")}
</span>
</div>
<p className="text-primary-foreground/60 text-xs mt-2 uppercase tracking-wider">
{item.label}
</p>
</div>
))}
</motion.div>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1 }}
>
<Button variant="hero" size="lg" className="text-lg px-10 py-6" asChild>
<a href="#booking">Reservar Asiento</a>
</Button>
</motion.div>
</div>
</section>
);
};
export default HeroSection;

View File

@@ -0,0 +1,61 @@
import { motion } from "framer-motion";
import { HOTEL_ROOMS } from "@/data/event-data";
import { Button } from "@/components/ui/button";
import { BedDouble, ExternalLink } from "lucide-react";
const HotelSection = () => (
<section id="hotel" className="section-padding bg-card">
<div className="container mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="font-display text-4xl md:text-5xl font-bold text-gradient mb-4">
Alojamiento
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
Habitaciones recomendadas cerca del venue.
</p>
</motion.div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6 max-w-5xl mx-auto">
{HOTEL_ROOMS.map((room, i) => (
<motion.div
key={room.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.1 }}
className="bg-background rounded-2xl overflow-hidden shadow-card hover:shadow-elevated transition-shadow"
>
{/* Imagen */}
<div className="aspect-video bg-muted flex items-center justify-center overflow-hidden">
{room.image ? (
<img src={room.image} alt={room.name} className="w-full h-full object-cover" />
) : (
<BedDouble className="w-12 h-12 text-muted-foreground/40" />
)}
</div>
<div className="p-5">
<h3 className="font-display text-lg font-bold text-foreground mb-1">
{room.name}
</h3>
<p className="text-primary font-bold text-xl mb-2">{room.price}</p>
<p className="text-sm text-muted-foreground mb-4">{room.description}</p>
<Button variant="outline" className="w-full" asChild>
<a href={room.link} target="_blank" rel="noopener noreferrer">
Reservar en el hotel <ExternalLink className="w-4 h-4 ml-1" />
</a>
</Button>
</div>
</motion.div>
))}
</div>
</div>
</section>
);
export default HotelSection;

80
src/components/Navbar.tsx Normal file
View File

@@ -0,0 +1,80 @@
import { useState, useEffect } from "react";
import { motion } from "framer-motion";
import { Menu, X } from "lucide-react";
import { NAV_LINKS } from "@/data/event-data";
/**
* Navbar sticky con menú responsive.
* Los links se definen en event-data.ts
*/
const Navbar = () => {
const [scrolled, setScrolled] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
useEffect(() => {
const onScroll = () => setScrolled(window.scrollY > 50);
window.addEventListener("scroll", onScroll);
return () => window.removeEventListener("scroll", onScroll);
}, []);
return (
<nav
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
scrolled
? "bg-background/95 backdrop-blur-md shadow-card"
: "bg-transparent"
}`}
>
<div className="container mx-auto flex items-center justify-between px-4 py-3">
{/* Logo */}
<a href="#" className="font-display text-xl font-bold text-gradient">
ZoukLambadaBCN
</a>
{/* Desktop links */}
<div className="hidden md:flex items-center gap-6">
{NAV_LINKS.map((link) => (
<a
key={link.href}
href={link.href}
className="text-sm font-medium text-foreground/80 hover:text-primary transition-colors"
>
{link.label}
</a>
))}
</div>
{/* Mobile toggle */}
<button
className="md:hidden text-foreground"
onClick={() => setMenuOpen(!menuOpen)}
aria-label="Toggle menu"
>
{menuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
{/* Mobile menu */}
{menuOpen && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
className="md:hidden bg-background/98 backdrop-blur-md border-t border-border px-4 pb-4"
>
{NAV_LINKS.map((link) => (
<a
key={link.href}
href={link.href}
onClick={() => setMenuOpen(false)}
className="block py-3 text-sm font-medium text-foreground/80 hover:text-primary transition-colors border-b border-border/50"
>
{link.label}
</a>
))}
</motion.div>
)}
</nav>
);
};
export default Navbar;

View File

@@ -0,0 +1,81 @@
import { motion } from "framer-motion";
import { ABOUT_ORG } from "@/data/event-data";
import communityImg from "@/assets/community.jpg";
import { Instagram, Facebook, Youtube } from "lucide-react";
const OrgSection = () => (
<section className="section-padding bg-card">
<div className="container mx-auto">
<div className="grid md:grid-cols-2 gap-12 items-center">
{/* Texto */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6 }}
>
<h2 className="font-display text-4xl md:text-5xl font-bold mb-6 text-gradient">
{ABOUT_ORG.title}
</h2>
<div className="space-y-4 text-muted-foreground leading-relaxed">
<p className="whitespace-pre-line">{ABOUT_ORG.history}</p>
<p className="whitespace-pre-line">{ABOUT_ORG.philosophy}</p>
</div>
{/* Redes sociales */}
<div className="flex gap-4 mt-8">
{ABOUT_ORG.socials.instagram && (
<a
href={ABOUT_ORG.socials.instagram}
target="_blank"
rel="noopener noreferrer"
className="bg-gradient-tropical p-3 rounded-full hover:opacity-80 transition-opacity"
aria-label="Instagram"
>
<Instagram className="w-5 h-5 text-primary-foreground" />
</a>
)}
{ABOUT_ORG.socials.facebook && (
<a
href={ABOUT_ORG.socials.facebook}
target="_blank"
rel="noopener noreferrer"
className="bg-gradient-tropical p-3 rounded-full hover:opacity-80 transition-opacity"
aria-label="Facebook"
>
<Facebook className="w-5 h-5 text-primary-foreground" />
</a>
)}
{ABOUT_ORG.socials.youtube && (
<a
href={ABOUT_ORG.socials.youtube}
target="_blank"
rel="noopener noreferrer"
className="bg-gradient-tropical p-3 rounded-full hover:opacity-80 transition-opacity"
aria-label="YouTube"
>
<Youtube className="w-5 h-5 text-primary-foreground" />
</a>
)}
</div>
</motion.div>
{/* Imagen */}
<motion.div
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<img
src={communityImg}
alt="Comunidad ZoukLambadaBCN"
className="rounded-2xl shadow-elevated w-full object-cover aspect-square"
/>
</motion.div>
</div>
</div>
</section>
);
export default OrgSection;

View File

@@ -0,0 +1,89 @@
import { motion } from "framer-motion";
import { EVENT_INFO, PRACTICAL_INFO } from "@/data/event-data";
import { MapPin, Plane, Train } from "lucide-react";
const PracticalSection = () => (
<section id="info" className="section-padding bg-background">
<div className="container mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="font-display text-4xl md:text-5xl font-bold text-gradient mb-4">
Información Práctica
</h2>
</motion.div>
<div className="grid md:grid-cols-2 gap-10 max-w-5xl mx-auto">
{/* Mapa */}
<motion.div
initial={{ opacity: 0, x: -30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
>
<div className="rounded-2xl overflow-hidden shadow-card mb-4 aspect-video">
<iframe
src={EVENT_INFO.mapEmbedUrl}
width="100%"
height="100%"
style={{ border: 0 }}
allowFullScreen
loading="lazy"
referrerPolicy="no-referrer-when-downgrade"
title="Ubicación del evento"
/>
</div>
<div className="flex items-center gap-2 text-foreground">
<MapPin className="w-5 h-5 text-primary" />
<div>
<p className="font-semibold">{EVENT_INFO.venue}</p>
<p className="text-sm text-muted-foreground">{EVENT_INFO.venueAddress}</p>
</div>
</div>
</motion.div>
{/* Info */}
<motion.div
initial={{ opacity: 0, x: 30 }}
whileInView={{ opacity: 1, x: 0 }}
viewport={{ once: true }}
className="space-y-8"
>
{/* Aeropuertos */}
<div>
<h3 className="font-display text-xl font-bold text-foreground mb-4 flex items-center gap-2">
<Plane className="w-5 h-5 text-primary" /> Aeropuertos Cercanos
</h3>
<div className="space-y-3">
{PRACTICAL_INFO.airports.map((a) => (
<div key={a.name} className="bg-card rounded-lg p-3">
<p className="font-medium text-foreground text-sm">{a.name}</p>
<p className="text-xs text-muted-foreground">{a.distance}</p>
</div>
))}
</div>
</div>
{/* Cómo llegar */}
<div>
<h3 className="font-display text-xl font-bold text-foreground mb-4 flex items-center gap-2">
<Train className="w-5 h-5 text-primary" /> Cómo Llegar
</h3>
<div className="space-y-3">
{PRACTICAL_INFO.howToGet.map((h) => (
<div key={h.method} className="bg-card rounded-lg p-3">
<p className="font-medium text-foreground text-sm">{h.method}</p>
<p className="text-xs text-muted-foreground">{h.details}</p>
</div>
))}
</div>
</div>
</motion.div>
</div>
</div>
</section>
);
export default PracticalSection;

View File

@@ -0,0 +1,74 @@
import { motion } from "framer-motion";
import { SCHEDULE } from "@/data/event-data";
import { Clock, Music, Coffee, Star } from "lucide-react";
const typeIcon: Record<string, typeof Clock> = {
workshop: Clock,
social: Music,
break: Coffee,
show: Star,
};
const typeColor: Record<string, string> = {
workshop: "border-primary bg-primary/10 text-primary",
social: "border-secondary bg-secondary/10 text-secondary",
break: "border-muted-foreground bg-muted text-muted-foreground",
show: "border-accent bg-accent/10 text-accent",
};
const ScheduleSection = () => (
<section id="schedule" className="section-padding bg-card">
<div className="container mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="font-display text-4xl md:text-5xl font-bold text-gradient mb-4">
Programa
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
Tres días de workshops, shows y social dance.
</p>
</motion.div>
<div className="grid md:grid-cols-3 gap-8">
{SCHEDULE.map((day, di) => (
<motion.div
key={day.day}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: di * 0.15 }}
>
<h3 className="font-display text-xl font-bold text-foreground mb-6 pb-3 border-b-2 border-primary">
{day.day}
</h3>
<div className="space-y-3">
{day.events.map((event, ei) => {
const Icon = typeIcon[event.type] || Clock;
return (
<div
key={ei}
className={`flex items-start gap-3 p-3 rounded-lg border-l-4 ${
typeColor[event.type] || ""
}`}
>
<Icon className="w-4 h-4 mt-0.5 shrink-0" />
<div>
<p className="text-xs font-medium opacity-70">{event.time}</p>
<p className="text-sm font-semibold">{event.title}</p>
</div>
</div>
);
})}
</div>
</motion.div>
))}
</div>
</div>
</section>
);
export default ScheduleSection;

View File

@@ -0,0 +1,89 @@
import { motion } from "framer-motion";
import { STAFF } from "@/data/event-data";
import { Instagram, User } from "lucide-react";
/** Colores de badge por rol */
const roleBadgeClass: Record<string, string> = {
Instructor: "bg-primary text-primary-foreground",
DJ: "bg-secondary text-secondary-foreground",
Organizador: "bg-accent text-accent-foreground",
};
const StaffSection = () => (
<section id="staff" className="section-padding bg-background">
<div className="container mx-auto">
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
className="text-center mb-12"
>
<h2 className="font-display text-4xl md:text-5xl font-bold text-gradient mb-4">
Staff del Evento
</h2>
<p className="text-muted-foreground max-w-2xl mx-auto">
Conoce a los artistas e instructores que harán de este festival una experiencia inolvidable.
</p>
</motion.div>
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-6">
{STAFF.map((member, i) => (
<motion.div
key={member.id}
initial={{ opacity: 0, y: 30 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.1 }}
className="bg-card rounded-2xl overflow-hidden shadow-card hover:shadow-elevated transition-shadow group"
>
{/* Foto */}
<div className="aspect-square bg-muted flex items-center justify-center overflow-hidden">
{member.image ? (
<img
src={member.image}
alt={member.name}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500"
/>
) : (
<User className="w-16 h-16 text-muted-foreground/40" />
)}
</div>
<div className="p-5">
{/* Badge de rol */}
<span
className={`inline-block text-xs font-semibold px-3 py-1 rounded-full mb-3 ${
roleBadgeClass[member.role] || "bg-muted text-muted-foreground"
}`}
>
{member.role}
</span>
<h3 className="font-display text-lg font-bold text-foreground mb-2">
{member.name}
</h3>
<p className="text-sm text-muted-foreground mb-4">
{member.description}
</p>
{/* Redes sociales */}
{member.socials?.instagram && (
<a
href={member.socials.instagram}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-sm text-primary hover:text-primary/80 transition-colors"
>
<Instagram className="w-4 h-4" />
Instagram
</a>
)}
</div>
</motion.div>
))}
</div>
</div>
</section>
);
export default StaffSection;

View File

@@ -15,6 +15,8 @@ const buttonVariants = cva(
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
hero: "bg-gradient-tropical text-primary-foreground shadow-glow hover:opacity-90 font-semibold text-base",
tropical: "bg-secondary text-secondary-foreground hover:bg-secondary/90 shadow-glow font-semibold",
},
size: {
default: "h-10 px-4 py-2",