Full LambadaBCN site built
Implemented a complete event landing page with editable data model, Patreon-like structure: - Added modular React components for hero, about, organizers, staff, schedule, booking, hotels, practical info, gallery, and footer - Integrated webhook-enabled booking form with validation and loading/error states - Created data-driven content file (src/data/event-data.ts) to drive all sections (staff, schedule, hotels, etc.) - Wired up responsive, mobile-first layout with tropical theme and smooth interactions - Added images as assets and placeholder data for editable fields - Implemented sticky navbar, floating reserve button, and scroll/anim transitions - Prepared for n8n webhook integration with clear comments and example URL - Updated global styles (Tailwind) and components (buttons, icons) to support new UI - Updated index.html and project structure for static hosting compatibility X-Lovable-Edit-ID: edt-6304ebcd-1b8b-4c6d-a441-4b12094846f1
This commit is contained in:
17
index.html
17
index.html
@@ -1,21 +1,16 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="es">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<!-- TODO: Set the document title to the name of your application -->
|
<title>Lambada Festival Barcelona | ZoukLambadaBCN</title>
|
||||||
<title>Lovable App</title>
|
<meta name="description" content="Festival de Lambada en Barcelona por ZoukLambadaBCN — Workshops, social dance, shows y DJ sets. Reserva tu asiento." />
|
||||||
<meta name="description" content="Lovable Generated Project" />
|
<meta name="author" content="ZoukLambadaBCN" />
|
||||||
<meta name="author" content="Lovable" />
|
<meta property="og:title" content="Lambada Festival Barcelona | ZoukLambadaBCN" />
|
||||||
|
<meta property="og:description" content="Festival de Lambada en Barcelona — Workshops, social dance, shows y DJ sets." />
|
||||||
<!-- TODO: Update og:title to match your application name -->
|
|
||||||
<meta property="og:title" content="Lovable App" />
|
|
||||||
<meta property="og:description" content="Lovable Generated Project" />
|
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
<meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||||
|
|
||||||
<meta name="twitter:card" content="summary_large_image" />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
<meta name="twitter:site" content="@Lovable" />
|
|
||||||
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
<meta name="twitter:image" content="https://lovable.dev/opengraph-image-p98pqg.png" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
1564
package-lock.json
generated
1564
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,7 @@
|
|||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
|
"framer-motion": "^12.35.0",
|
||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"lucide-react": "^0.462.0",
|
"lucide-react": "^0.462.0",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
@@ -65,9 +66,9 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.32.0",
|
"@eslint/js": "^9.32.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@testing-library/jest-dom": "^6.6.0",
|
"@testing-library/jest-dom": "^6.6.0",
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.16.5",
|
||||||
"@types/react": "^18.3.23",
|
"@types/react": "^18.3.23",
|
||||||
"@types/react-dom": "^18.3.7",
|
"@types/react-dom": "^18.3.7",
|
||||||
|
|||||||
BIN
src/assets/about-event.jpg
Normal file
BIN
src/assets/about-event.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
src/assets/community.jpg
Normal file
BIN
src/assets/community.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 87 KiB |
BIN
src/assets/hero-bg.jpg
Normal file
BIN
src/assets/hero-bg.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 243 KiB |
63
src/components/AboutSection.tsx
Normal file
63
src/components/AboutSection.tsx
Normal 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;
|
||||||
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;
|
||||||
46
src/components/FloatingButton.tsx
Normal file
46
src/components/FloatingButton.tsx
Normal 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;
|
||||||
61
src/components/FooterSection.tsx
Normal file
61
src/components/FooterSection.tsx
Normal 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;
|
||||||
49
src/components/GallerySection.tsx
Normal file
49
src/components/GallerySection.tsx
Normal 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;
|
||||||
109
src/components/HeroSection.tsx
Normal file
109
src/components/HeroSection.tsx
Normal 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;
|
||||||
61
src/components/HotelSection.tsx
Normal file
61
src/components/HotelSection.tsx
Normal 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
80
src/components/Navbar.tsx
Normal 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;
|
||||||
81
src/components/OrgSection.tsx
Normal file
81
src/components/OrgSection.tsx
Normal 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;
|
||||||
89
src/components/PracticalSection.tsx
Normal file
89
src/components/PracticalSection.tsx
Normal 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;
|
||||||
74
src/components/ScheduleSection.tsx
Normal file
74
src/components/ScheduleSection.tsx
Normal 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;
|
||||||
89
src/components/StaffSection.tsx
Normal file
89
src/components/StaffSection.tsx
Normal 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;
|
||||||
@@ -15,6 +15,8 @@ const buttonVariants = cva(
|
|||||||
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
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: {
|
size: {
|
||||||
default: "h-10 px-4 py-2",
|
default: "h-10 px-4 py-2",
|
||||||
|
|||||||
237
src/data/event-data.ts
Normal file
237
src/data/event-data.ts
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* ===========================================
|
||||||
|
* DATOS DEL EVENTO — ZoukLambadaBCN
|
||||||
|
* ===========================================
|
||||||
|
*
|
||||||
|
* Este archivo centraliza TODOS los datos editables del evento.
|
||||||
|
* Para modificar cualquier información, simplemente edita las
|
||||||
|
* constantes de este archivo.
|
||||||
|
*
|
||||||
|
* NOTA: Las imágenes deben colocarse en src/assets/ e importarse.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ---- INFORMACIÓN GENERAL DEL EVENTO ----
|
||||||
|
export const EVENT_INFO = {
|
||||||
|
name: "Lambada Festival Barcelona",
|
||||||
|
subtitle: "by ZoukLambadaBCN",
|
||||||
|
/** Fecha del evento — formato ISO para el countdown */
|
||||||
|
date: "2026-06-20T18:00:00",
|
||||||
|
/** Fecha legible para mostrar */
|
||||||
|
dateDisplay: "20–22 Junio 2026",
|
||||||
|
city: "Barcelona, España",
|
||||||
|
venue: "[Nombre del Venue]",
|
||||||
|
venueAddress: "[Dirección del venue, Barcelona]",
|
||||||
|
/** Google Maps embed URL — reemplazar con la URL real */
|
||||||
|
mapEmbedUrl: "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2993.5!2d2.1734!3d41.3851!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x0%3A0x0!2zNDHCsDIzJzA2LjQiTiAywrAxMCcyNC4yIkU!5e0!3m2!1ses!2ses!4v1234567890",
|
||||||
|
totalSeats: 150,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- WEBHOOK N8N ----
|
||||||
|
/**
|
||||||
|
* CONFIGURACIÓN DEL WEBHOOK:
|
||||||
|
*
|
||||||
|
* 1. En n8n, crea un nuevo workflow con un nodo "Webhook"
|
||||||
|
* 2. Configura el webhook como POST
|
||||||
|
* 3. Copia la URL generada y pégala aquí abajo
|
||||||
|
* 4. En n8n, conecta el webhook a tu destino (Google Sheets, Airtable, etc.)
|
||||||
|
*
|
||||||
|
* Ejemplo de payload que se envía:
|
||||||
|
* {
|
||||||
|
* "name": "Juan García",
|
||||||
|
* "email": "juan@example.com",
|
||||||
|
* "seats": "2",
|
||||||
|
* "country": "España",
|
||||||
|
* "comment": "Primera vez en el festival"
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
export const WEBHOOK_URL = "https://YOUR-N8N-INSTANCE/webhook/event-reservation";
|
||||||
|
|
||||||
|
// ---- SOBRE EL EVENTO ----
|
||||||
|
export const ABOUT_EVENT = {
|
||||||
|
title: "Sobre el Evento",
|
||||||
|
description: `[Descripción del evento. Explica qué hace especial esta edición,
|
||||||
|
qué pueden esperar los asistentes, y por qué no se lo pueden perder.]`,
|
||||||
|
lambadaInfo: `La Lambada es un baile brasileño nacido en los años 80,
|
||||||
|
conocido por su sensualidad, conexión y ritmo envolvente.
|
||||||
|
Mezcla influencias de forró, merengue y carimbó,
|
||||||
|
creando una experiencia de baile única y apasionante.`,
|
||||||
|
highlights: [
|
||||||
|
"Workshops con artistas internacionales",
|
||||||
|
"Social dancing durante toda la noche",
|
||||||
|
"Shows en vivo",
|
||||||
|
"DJ sets tropicales",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- SOBRE ZOUKLAMBADABCN ----
|
||||||
|
export const ABOUT_ORG = {
|
||||||
|
title: "ZoukLambadaBCN",
|
||||||
|
history: `[Historia del grupo organizador. Cuándo se fundó,
|
||||||
|
cómo empezó, qué han logrado hasta ahora.]`,
|
||||||
|
philosophy: `[Filosofía del grupo. Qué valores defienden,
|
||||||
|
qué quieren aportar a la comunidad de baile.]`,
|
||||||
|
socials: {
|
||||||
|
instagram: "https://instagram.com/zouklambadabcn",
|
||||||
|
facebook: "https://facebook.com/zouklambadabcn",
|
||||||
|
youtube: "https://youtube.com/@zouklambadabcn",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- STAFF DEL EVENTO ----
|
||||||
|
/**
|
||||||
|
* Para añadir un nuevo miembro del staff,
|
||||||
|
* simplemente agrega un nuevo objeto al array.
|
||||||
|
*/
|
||||||
|
export const STAFF = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "[Nombre Instructor 1]",
|
||||||
|
role: "Instructor" as const,
|
||||||
|
description: "[Breve biografía del instructor]",
|
||||||
|
/** Reemplazar con ruta a foto real */
|
||||||
|
image: "",
|
||||||
|
socials: {
|
||||||
|
instagram: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "[Nombre Instructor 2]",
|
||||||
|
role: "Instructor" as const,
|
||||||
|
description: "[Breve biografía del instructor]",
|
||||||
|
image: "",
|
||||||
|
socials: {
|
||||||
|
instagram: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
name: "[Nombre DJ]",
|
||||||
|
role: "DJ" as const,
|
||||||
|
description: "[Breve biografía del DJ]",
|
||||||
|
image: "",
|
||||||
|
socials: {
|
||||||
|
instagram: "",
|
||||||
|
soundcloud: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "4",
|
||||||
|
name: "[Nombre Organizador]",
|
||||||
|
role: "Organizador" as const,
|
||||||
|
description: "[Breve biografía del organizador]",
|
||||||
|
image: "",
|
||||||
|
socials: {
|
||||||
|
instagram: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---- PROGRAMA DEL EVENTO ----
|
||||||
|
export const SCHEDULE = [
|
||||||
|
{
|
||||||
|
day: "Viernes 20 Junio",
|
||||||
|
events: [
|
||||||
|
{ time: "18:00 – 19:30", title: "[Workshop 1]", type: "workshop" as const },
|
||||||
|
{ time: "19:30 – 20:00", title: "Pausa", type: "break" as const },
|
||||||
|
{ time: "20:00 – 21:30", title: "[Workshop 2]", type: "workshop" as const },
|
||||||
|
{ time: "22:00 – 03:00", title: "Social Dance + DJ Set", type: "social" as const },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: "Sábado 21 Junio",
|
||||||
|
events: [
|
||||||
|
{ time: "12:00 – 13:30", title: "[Workshop 3]", type: "workshop" as const },
|
||||||
|
{ time: "14:00 – 15:30", title: "[Workshop 4]", type: "workshop" as const },
|
||||||
|
{ time: "16:00 – 17:30", title: "[Workshop 5]", type: "workshop" as const },
|
||||||
|
{ time: "21:00 – 22:00", title: "Shows en Vivo", type: "show" as const },
|
||||||
|
{ time: "22:00 – 04:00", title: "Social Dance + DJ Sets", type: "social" as const },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
day: "Domingo 22 Junio",
|
||||||
|
events: [
|
||||||
|
{ time: "12:00 – 13:30", title: "[Workshop 6]", type: "workshop" as const },
|
||||||
|
{ time: "14:00 – 15:30", title: "[Workshop 7]", type: "workshop" as const },
|
||||||
|
{ time: "17:00 – 22:00", title: "Farewell Party", type: "social" as const },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---- HABITACIONES DE HOTEL ----
|
||||||
|
/**
|
||||||
|
* Para añadir habitaciones, agrega objetos al array.
|
||||||
|
* El botón "Reservar" abre el link en nueva pestaña.
|
||||||
|
*/
|
||||||
|
export const HOTEL_ROOMS = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
name: "[Habitación Individual]",
|
||||||
|
price: "[XX€/noche]",
|
||||||
|
description: "[Descripción breve de la habitación]",
|
||||||
|
/** Reemplazar con URL de imagen real */
|
||||||
|
image: "",
|
||||||
|
link: "https://hotel-example.com/booking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "2",
|
||||||
|
name: "[Habitación Doble]",
|
||||||
|
price: "[XX€/noche]",
|
||||||
|
description: "[Descripción breve de la habitación]",
|
||||||
|
image: "",
|
||||||
|
link: "https://hotel-example.com/booking",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "3",
|
||||||
|
name: "[Suite]",
|
||||||
|
price: "[XX€/noche]",
|
||||||
|
description: "[Descripción breve de la habitación]",
|
||||||
|
image: "",
|
||||||
|
link: "https://hotel-example.com/booking",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---- INFORMACIÓN PRÁCTICA ----
|
||||||
|
export const PRACTICAL_INFO = {
|
||||||
|
airports: [
|
||||||
|
{ name: "Aeropuerto de Barcelona-El Prat (BCN)", distance: "~15 km del venue" },
|
||||||
|
{ name: "Aeropuerto de Girona (GRO)", distance: "~100 km del venue" },
|
||||||
|
{ name: "Aeropuerto de Reus (REU)", distance: "~110 km del venue" },
|
||||||
|
],
|
||||||
|
howToGet: [
|
||||||
|
{ method: "Metro", details: "[Línea y parada más cercana]" },
|
||||||
|
{ method: "Bus", details: "[Líneas de bus cercanas]" },
|
||||||
|
{ method: "Taxi/Uber", details: "Disponible desde cualquier punto de Barcelona" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- GALERÍA ----
|
||||||
|
/**
|
||||||
|
* Añade URLs o importaciones de imágenes.
|
||||||
|
* Para importar: import img from "@/assets/gallery/photo1.jpg"
|
||||||
|
*/
|
||||||
|
export const GALLERY_IMAGES = [
|
||||||
|
{ src: "", alt: "[Descripción foto 1]" },
|
||||||
|
{ src: "", alt: "[Descripción foto 2]" },
|
||||||
|
{ src: "", alt: "[Descripción foto 3]" },
|
||||||
|
{ src: "", alt: "[Descripción foto 4]" },
|
||||||
|
{ src: "", alt: "[Descripción foto 5]" },
|
||||||
|
{ src: "", alt: "[Descripción foto 6]" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---- NAVEGACIÓN ----
|
||||||
|
export const NAV_LINKS = [
|
||||||
|
{ label: "Sobre", href: "#about" },
|
||||||
|
{ label: "Staff", href: "#staff" },
|
||||||
|
{ label: "Programa", href: "#schedule" },
|
||||||
|
{ label: "Reservar", href: "#booking" },
|
||||||
|
{ label: "Hotel", href: "#hotel" },
|
||||||
|
{ label: "Info", href: "#info" },
|
||||||
|
{ label: "Galería", href: "#gallery" },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---- FOOTER ----
|
||||||
|
export const FOOTER = {
|
||||||
|
email: "[email@zouklambadabcn.com]",
|
||||||
|
copyright: `© ${new Date().getFullYear()} ZoukLambadaBCN. Todos los derechos reservados.`,
|
||||||
|
};
|
||||||
156
src/index.css
156
src/index.css
@@ -1,88 +1,89 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700;800&family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/* Definition of the design system. All colors, gradients, fonts, etc should be defined here.
|
/* ===========================================
|
||||||
All colors MUST be HSL.
|
DESIGN SYSTEM — ZoukLambadaBCN Event
|
||||||
*/
|
Tropical palette: orange, pink, violet
|
||||||
|
All colors in HSL
|
||||||
|
=========================================== */
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
/* Tropical warm palette */
|
||||||
--foreground: 222.2 84% 4.9%;
|
--background: 30 30% 97%;
|
||||||
|
--foreground: 280 30% 12%;
|
||||||
|
|
||||||
--card: 0 0% 100%;
|
--card: 30 25% 95%;
|
||||||
--card-foreground: 222.2 84% 4.9%;
|
--card-foreground: 280 30% 12%;
|
||||||
|
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222.2 84% 4.9%;
|
--popover-foreground: 280 30% 12%;
|
||||||
|
|
||||||
--primary: 222.2 47.4% 11.2%;
|
/* Primary: vibrant orange */
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary: 24 95% 55%;
|
||||||
|
--primary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--secondary: 210 40% 96.1%;
|
/* Secondary: tropical pink */
|
||||||
--secondary-foreground: 222.2 47.4% 11.2%;
|
--secondary: 340 82% 60%;
|
||||||
|
--secondary-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--muted: 210 40% 96.1%;
|
/* Accent: rich violet */
|
||||||
--muted-foreground: 215.4 16.3% 46.9%;
|
--accent: 280 65% 50%;
|
||||||
|
--accent-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--accent: 210 40% 96.1%;
|
--muted: 30 20% 92%;
|
||||||
--accent-foreground: 222.2 47.4% 11.2%;
|
--muted-foreground: 280 15% 40%;
|
||||||
|
|
||||||
--destructive: 0 84.2% 60.2%;
|
--destructive: 0 84% 60%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 100%;
|
||||||
|
|
||||||
--border: 214.3 31.8% 91.4%;
|
--border: 30 20% 85%;
|
||||||
--input: 214.3 31.8% 91.4%;
|
--input: 30 20% 85%;
|
||||||
--ring: 222.2 84% 4.9%;
|
--ring: 24 95% 55%;
|
||||||
|
|
||||||
--radius: 0.5rem;
|
--radius: 0.75rem;
|
||||||
|
|
||||||
|
/* Custom tokens */
|
||||||
|
--gradient-tropical: linear-gradient(135deg, hsl(24 95% 55%), hsl(340 82% 60%), hsl(280 65% 50%));
|
||||||
|
--gradient-warm: linear-gradient(180deg, hsl(30 30% 97%), hsl(30 25% 93%));
|
||||||
|
--shadow-glow: 0 0 40px hsl(24 95% 55% / 0.25);
|
||||||
|
--shadow-card: 0 8px 30px hsl(280 30% 12% / 0.08);
|
||||||
|
--shadow-elevated: 0 20px 50px hsl(280 30% 12% / 0.12);
|
||||||
|
|
||||||
|
/* Sidebar (unused but required) */
|
||||||
--sidebar-background: 0 0% 98%;
|
--sidebar-background: 0 0% 98%;
|
||||||
|
|
||||||
--sidebar-foreground: 240 5.3% 26.1%;
|
--sidebar-foreground: 240 5.3% 26.1%;
|
||||||
|
|
||||||
--sidebar-primary: 240 5.9% 10%;
|
--sidebar-primary: 240 5.9% 10%;
|
||||||
|
|
||||||
--sidebar-primary-foreground: 0 0% 98%;
|
--sidebar-primary-foreground: 0 0% 98%;
|
||||||
|
|
||||||
--sidebar-accent: 240 4.8% 95.9%;
|
--sidebar-accent: 240 4.8% 95.9%;
|
||||||
|
|
||||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||||
|
|
||||||
--sidebar-border: 220 13% 91%;
|
--sidebar-border: 220 13% 91%;
|
||||||
|
|
||||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.2 84% 4.9%;
|
--background: 280 25% 8%;
|
||||||
--foreground: 210 40% 98%;
|
--foreground: 30 20% 95%;
|
||||||
|
--card: 280 20% 12%;
|
||||||
--card: 222.2 84% 4.9%;
|
--card-foreground: 30 20% 95%;
|
||||||
--card-foreground: 210 40% 98%;
|
--popover: 280 20% 12%;
|
||||||
|
--popover-foreground: 30 20% 95%;
|
||||||
--popover: 222.2 84% 4.9%;
|
--primary: 24 95% 55%;
|
||||||
--popover-foreground: 210 40% 98%;
|
--primary-foreground: 0 0% 100%;
|
||||||
|
--secondary: 340 82% 60%;
|
||||||
--primary: 210 40% 98%;
|
--secondary-foreground: 0 0% 100%;
|
||||||
--primary-foreground: 222.2 47.4% 11.2%;
|
--accent: 280 65% 55%;
|
||||||
|
--accent-foreground: 0 0% 100%;
|
||||||
--secondary: 217.2 32.6% 17.5%;
|
--muted: 280 15% 18%;
|
||||||
--secondary-foreground: 210 40% 98%;
|
--muted-foreground: 30 15% 65%;
|
||||||
|
|
||||||
--muted: 217.2 32.6% 17.5%;
|
|
||||||
--muted-foreground: 215 20.2% 65.1%;
|
|
||||||
|
|
||||||
--accent: 217.2 32.6% 17.5%;
|
|
||||||
--accent-foreground: 210 40% 98%;
|
|
||||||
|
|
||||||
--destructive: 0 62.8% 30.6%;
|
--destructive: 0 62.8% 30.6%;
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 100%;
|
||||||
|
--border: 280 15% 20%;
|
||||||
--border: 217.2 32.6% 17.5%;
|
--input: 280 15% 20%;
|
||||||
--input: 217.2 32.6% 17.5%;
|
--ring: 24 95% 55%;
|
||||||
--ring: 212.7 26.8% 83.9%;
|
|
||||||
--sidebar-background: 240 5.9% 10%;
|
--sidebar-background: 240 5.9% 10%;
|
||||||
--sidebar-foreground: 240 4.8% 95.9%;
|
--sidebar-foreground: 240 4.8% 95.9%;
|
||||||
--sidebar-primary: 224.3 76.3% 48%;
|
--sidebar-primary: 224.3 76.3% 48%;
|
||||||
@@ -100,6 +101,49 @@ All colors MUST be HSL.
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground font-body antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
@apply font-display;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth scroll */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
/* Tropical gradient text */
|
||||||
|
.text-gradient {
|
||||||
|
background: var(--gradient-tropical);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tropical gradient background */
|
||||||
|
.bg-gradient-tropical {
|
||||||
|
background: var(--gradient-tropical);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glow effect for buttons */
|
||||||
|
.shadow-glow {
|
||||||
|
box-shadow: var(--shadow-glow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card shadow */
|
||||||
|
.shadow-card {
|
||||||
|
box-shadow: var(--shadow-card);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-elevated {
|
||||||
|
box-shadow: var(--shadow-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section padding utility */
|
||||||
|
.section-padding {
|
||||||
|
@apply px-4 py-16 md:px-8 md:py-24 lg:px-16;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,37 @@
|
|||||||
// Update this page (the content is just a fallback if you fail to update the page)
|
import Navbar from "@/components/Navbar";
|
||||||
|
import HeroSection from "@/components/HeroSection";
|
||||||
|
import AboutSection from "@/components/AboutSection";
|
||||||
|
import OrgSection from "@/components/OrgSection";
|
||||||
|
import StaffSection from "@/components/StaffSection";
|
||||||
|
import ScheduleSection from "@/components/ScheduleSection";
|
||||||
|
import BookingSection from "@/components/BookingSection";
|
||||||
|
import HotelSection from "@/components/HotelSection";
|
||||||
|
import PracticalSection from "@/components/PracticalSection";
|
||||||
|
import GallerySection from "@/components/GallerySection";
|
||||||
|
import FooterSection from "@/components/FooterSection";
|
||||||
|
import FloatingButton from "@/components/FloatingButton";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Landing page — Lambada Festival Barcelona
|
||||||
|
* by ZoukLambadaBCN
|
||||||
|
*
|
||||||
|
* Todos los datos editables están en: src/data/event-data.ts
|
||||||
|
*/
|
||||||
const Index = () => {
|
const Index = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-background">
|
<div className="min-h-screen">
|
||||||
<div className="text-center">
|
<Navbar />
|
||||||
<h1 className="mb-4 text-4xl font-bold">Welcome to Your Blank App</h1>
|
<HeroSection />
|
||||||
<p className="text-xl text-muted-foreground">Start building your amazing project here!</p>
|
<AboutSection />
|
||||||
</div>
|
<OrgSection />
|
||||||
|
<StaffSection />
|
||||||
|
<ScheduleSection />
|
||||||
|
<BookingSection />
|
||||||
|
<HotelSection />
|
||||||
|
<PracticalSection />
|
||||||
|
<GallerySection />
|
||||||
|
<FooterSection />
|
||||||
|
<FloatingButton />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
display: ['"Playfair Display"', "Georgia", "serif"],
|
||||||
|
body: ['"Inter"', "system-ui", "sans-serif"],
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
border: "hsl(var(--border))",
|
border: "hsl(var(--border))",
|
||||||
input: "hsl(var(--input))",
|
input: "hsl(var(--input))",
|
||||||
@@ -65,25 +69,22 @@ export default {
|
|||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
"accordion-down": {
|
"accordion-down": {
|
||||||
from: {
|
from: { height: "0" },
|
||||||
height: "0",
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
},
|
|
||||||
to: {
|
|
||||||
height: "var(--radix-accordion-content-height)",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"accordion-up": {
|
"accordion-up": {
|
||||||
from: {
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
height: "var(--radix-accordion-content-height)",
|
to: { height: "0" },
|
||||||
},
|
},
|
||||||
to: {
|
"pulse-glow": {
|
||||||
height: "0",
|
"0%, 100%": { boxShadow: "0 0 20px hsl(24 95% 55% / 0.3)" },
|
||||||
},
|
"50%": { boxShadow: "0 0 40px hsl(24 95% 55% / 0.6)" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"accordion-down": "accordion-down 0.2s ease-out",
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
"accordion-up": "accordion-up 0.2s ease-out",
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
"pulse-glow": "pulse-glow 2s ease-in-out infinite",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user