Compare commits
8 Commits
f53e271553
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8a0336ae5 | ||
|
|
fce84ff4a6 | ||
|
|
823ca68119 | ||
|
|
7a65e7a1f4 | ||
|
|
cd9e914713 | ||
|
|
18cc2e66af | ||
|
|
2d216b907e | ||
|
|
a97e4f4469 |
2
.github/workflows/node.js.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x, 22.x]
|
||||
node-version: [22.x]
|
||||
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
|
||||
|
||||
steps:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
<!-- README.md -->
|
||||
+ [](https://github.com/Ichitux/lambada-fiesta-live/actions)
|
||||
+ [](https://github.com/Ichitux/lambada-fiesta-live/actions/workflows/node.js.yml)
|
||||
|
||||
|
||||
# Project Name
|
||||
|
||||
2335
package-lock.json
generated
@@ -88,7 +88,7 @@
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.38.0",
|
||||
"vite": "^8.0.3",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 406 KiB |
|
After Width: | Height: | Size: 319 KiB |
BIN
src/assets/gallery/gal1.jpg
Normal file
|
After Width: | Height: | Size: 396 KiB |
BIN
src/assets/gallery/gal2.jpg
Normal file
|
After Width: | Height: | Size: 330 KiB |
BIN
src/assets/gallery/gal3.jpg
Normal file
|
After Width: | Height: | Size: 378 KiB |
BIN
src/assets/gallery/gal4.jpg
Normal file
|
After Width: | Height: | Size: 333 KiB |
BIN
src/assets/gallery/gal5.jpg
Normal file
|
After Width: | Height: | Size: 462 KiB |
BIN
src/assets/gallery/gal6.jpg
Normal file
|
After Width: | Height: | Size: 424 KiB |
@@ -3,6 +3,7 @@ import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useState, useEffect } from "react";
|
||||
import { ChevronUp } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { SECTIONS } from "@/data/event-data";
|
||||
|
||||
/** Botón flotante de reserva + scroll to top */
|
||||
const FloatingButton = () => {
|
||||
@@ -30,7 +31,7 @@ const FloatingButton = () => {
|
||||
className="animate-pulse-glow rounded-full px-6 shadow-elevated"
|
||||
asChild
|
||||
>
|
||||
<a href="#booking" className="inline-flex items-center gap-2">
|
||||
<a href={SECTIONS.booking ? "#booking" : "#mixed-booking"} className="inline-flex items-center gap-2">
|
||||
{/* Inline SVG: dancing couple icon */}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { EVENT_INFO } from "@/data/event-data";
|
||||
import { EVENT_INFO, SECTIONS } from "@/data/event-data";
|
||||
import heroBg from "@/assets/hero-bg.jpg";
|
||||
|
||||
/** Calcula diferencia entre ahora y la fecha del evento */
|
||||
@@ -116,7 +116,7 @@ const HeroSection = () => {
|
||||
transition={{ delay: 1 }}
|
||||
>
|
||||
<Button variant="hero" size="lg" className="text-lg px-10 py-6" asChild>
|
||||
<a href="#booking">{t('hero.bookYourPass')}</a>
|
||||
<a href={SECTIONS.booking ? "#booking" : "#mixed-booking"}>{t('hero.bookYourPass')}</a>
|
||||
</Button>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
213
src/components/MixedBookingSection.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import { useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { MIXED_BOOKING_PACKAGES, ROOM_TYPES, getFullPassPrice } from "@/data/event-data";
|
||||
import type { RoomType } from "@/data/event-data";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Check, Star, Circle, CheckCircle2 } from "lucide-react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
const FEATURED_PASS = "full";
|
||||
const NON_HOTEL_FEE = 50;
|
||||
const PARTY_PRICE = 25;
|
||||
|
||||
const MixedBookingSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const fullPassPricing = getFullPassPrice();
|
||||
const [selectedRooms, setSelectedRooms] = useState<Record<string, RoomType>>({
|
||||
full: "individual",
|
||||
party: "individual",
|
||||
});
|
||||
const [wantsHotel, setWantsHotel] = useState<Record<string, boolean | null>>({
|
||||
full: null,
|
||||
party: null,
|
||||
});
|
||||
|
||||
return (
|
||||
<section
|
||||
id="mixed-booking"
|
||||
className="section-padding bg-background scroll-mt-24 relative z-10 -mt-[40px] pt-[120px]"
|
||||
style={{ borderRadius: "0 100% 0 0 / 0 120px 0 0" }}
|
||||
>
|
||||
<div className="container mx-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: false, amount: 0.15 }}
|
||||
className="text-center mb-12"
|
||||
>
|
||||
<h2 className="font-display text-4xl md:text-5xl lg:text-7xl break-words font-bold pt-4 pb-10 leading-[1.8] text-gradient">
|
||||
{t("mixedBooking.title")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground max-w-2xl mx-auto">{t("mixedBooking.subtitle")}</p>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid sm:grid-cols-2 gap-6 lg:gap-8 max-w-4xl mx-auto items-stretch">
|
||||
{MIXED_BOOKING_PACKAGES.map((pkg, i) => {
|
||||
const selectedRoom = selectedRooms[pkg.id] || "individual";
|
||||
const hasHotel = wantsHotel[pkg.id] === true;
|
||||
// Use dynamic pricing for Full Pass, fixed price for Party Pass
|
||||
// Only Full Pass has the +50€ non-hotel fee
|
||||
let basePrice = pkg.id === "full" ? fullPassPricing.price : PARTY_PRICE;
|
||||
let roomPrice = 0;
|
||||
if (hasHotel) {
|
||||
roomPrice = pkg.roomPrices[selectedRoom as keyof typeof pkg.roomPrices];
|
||||
}
|
||||
|
||||
const passPrice = pkg.id === "full"
|
||||
? (hasHotel ? basePrice : basePrice + NON_HOTEL_FEE)
|
||||
: basePrice;
|
||||
const price = passPrice + roomPrice;
|
||||
|
||||
const isFeatured = pkg.id === FEATURED_PASS;
|
||||
const isLastPrice = pkg.id === "full" && fullPassPricing.isLastPrice;
|
||||
const showNonHotelNote = pkg.id === "full" && !hasHotel && basePrice > 0;
|
||||
const features = t(`mixedBooking.${pkg.id}Features`).split("|").map((f: string) => f.trim());
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
key={pkg.id}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: false, amount: 0.15 }}
|
||||
transition={{ delay: i * 0.1 }}
|
||||
className={`relative flex h-full min-h-[520px] flex-col rounded-2xl overflow-hidden transition-shadow duration-300 ${
|
||||
isFeatured
|
||||
? "shadow-elevated border-2 border-primary/30"
|
||||
: "shadow-card hover:shadow-elevated border border-border"
|
||||
}`}
|
||||
>
|
||||
<div className="absolute top-4 right-4 z-10 flex flex-col gap-2 items-end">
|
||||
{isFeatured && (
|
||||
<span className="inline-flex items-center gap-1 bg-gradient-tropical text-primary-foreground text-xs font-bold px-3 py-1 rounded-full shadow-md">
|
||||
<Star className="w-3 h-3 fill-current" />
|
||||
{t("mixedBooking.popular")}
|
||||
</span>
|
||||
)}
|
||||
{isLastPrice && (
|
||||
<span className="inline-flex items-center gap-1 bg-red-600 text-white text-xs font-bold px-3 py-1 rounded-full shadow-md">
|
||||
{t("mixedBooking.lastPrice")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={`px-6 pt-8 pb-4 text-center min-h-[160px] flex flex-col justify-between ${
|
||||
isFeatured ? "bg-gradient-tropical" : "bg-secondary/10"
|
||||
}`}>
|
||||
<div>
|
||||
<h3 className={`font-display text-2xl md:text-3xl font-bold mb-1 ${
|
||||
isFeatured ? "text-primary-foreground" : "text-foreground"
|
||||
}`}>
|
||||
{t(`mixedBooking.passTypes.${pkg.id}`)}
|
||||
</h3>
|
||||
<p className={`text-sm ${
|
||||
isFeatured ? "text-primary-foreground/80" : "text-muted-foreground"
|
||||
}`}>
|
||||
{t(`mixedBooking.${pkg.id}Description`)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-6 pt-6 pb-4 text-center bg-card">
|
||||
<p className="text-4xl md:text-5xl font-bold text-primary">
|
||||
{price > 0 ? `${price}€` : t("mixedBooking.priceTBD")}
|
||||
</p>
|
||||
<p className="text-sm font-medium text-foreground mt-2">
|
||||
{t(`mixedBooking.passTypes.${pkg.id}`)}: {passPrice}€ {pkg.id === "full" ? t("mixedBooking.perPerson") : t("mixedBooking.perParty")}
|
||||
</p>
|
||||
{showNonHotelNote && (
|
||||
<p className="text-xs text-red-600 font-medium mt-2">
|
||||
{t("mixedBooking.nonHotelNote")}
|
||||
</p>
|
||||
)}
|
||||
{hasHotel && (
|
||||
<p className="text-sm font-medium text-foreground mt-2">
|
||||
{t(`mixedBooking.roomTypes.${selectedRoom}`)}: {roomPrice}€
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-6 pt-4 pb-2 bg-card">
|
||||
<Label className="block text-sm font-medium text-foreground mb-3">
|
||||
{t("mixedBooking.hotelQuestion")}
|
||||
</Label>
|
||||
<div className="flex gap-3 mb-4">
|
||||
<button
|
||||
onClick={() => setWantsHotel((prev) => ({ ...prev, [pkg.id]: true }))}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl border-2 text-sm font-medium transition-all duration-200 ${
|
||||
hasHotel
|
||||
? "border-primary bg-primary/5 text-primary"
|
||||
: "border-input bg-background text-muted-foreground hover:border-primary/30"
|
||||
}`}
|
||||
>
|
||||
<CheckCircle2 className={`w-5 h-5 ${hasHotel ? "text-primary" : "text-muted-foreground"}`} />
|
||||
{t("mixedBooking.hotelYes")}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setWantsHotel((prev) => ({ ...prev, [pkg.id]: false }))}
|
||||
className={`flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl border-2 text-sm font-medium transition-all duration-200 ${
|
||||
!hasHotel
|
||||
? "border-primary bg-primary/5 text-primary"
|
||||
: "border-input bg-background text-muted-foreground hover:border-primary/30"
|
||||
}`}
|
||||
>
|
||||
<Circle className={`w-5 h-5 ${!hasHotel ? "text-primary" : "text-muted-foreground"}`} />
|
||||
{t("mixedBooking.hotelNo")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{hasHotel && (
|
||||
<>
|
||||
<Label className="block text-sm font-medium text-foreground mb-2">
|
||||
{t("mixedBooking.selectRoom")}
|
||||
</Label>
|
||||
<Select
|
||||
value={selectedRoom}
|
||||
onValueChange={(value) =>
|
||||
setSelectedRooms((prev) => ({
|
||||
...prev,
|
||||
[pkg.id]: value as RoomType,
|
||||
}))
|
||||
}
|
||||
>
|
||||
<SelectTrigger className={`w-full rounded-xl border px-4 py-3 text-sm text-foreground shadow-sm transition-colors duration-200 ${
|
||||
isFeatured
|
||||
? "border-primary/30 bg-primary/5 hover:border-primary/50"
|
||||
: "border-input bg-background hover:border-primary/30"
|
||||
}`}>
|
||||
<SelectValue placeholder={t("mixedBooking.selectRoom")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ROOM_TYPES.map((room) => (
|
||||
<SelectItem key={room.id} value={room.id}>
|
||||
{t(`mixedBooking.roomTypes.${room.id}`)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="px-6 pt-4 pb-6 flex-1 flex flex-col justify-between bg-card">
|
||||
<ul className="space-y-2.5 mb-6">
|
||||
{features.map((feature: string, idx: number) => (
|
||||
<li key={idx} className="flex items-start gap-2.5 text-sm text-foreground/80">
|
||||
<Check className={`w-4 h-4 mt-0.5 flex-shrink-0 ${
|
||||
isFeatured ? "text-primary" : "text-secondary"
|
||||
}`} />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default MixedBookingSection;
|
||||
@@ -88,6 +88,16 @@ const PracticalSection = () => {
|
||||
<div key={h.method} className="bg-card rounded-lg p-3">
|
||||
<p className="font-medium text-foreground text-sm">{methodLabel}</p>
|
||||
<p className="text-xs text-muted-foreground">{h.details}</p>
|
||||
{h.link && (
|
||||
<a
|
||||
href={h.link}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block mt-2 px-3 py-1 bg-primary text-primary-foreground text-xs rounded hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
{t("info.viewOnMap")}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -22,18 +22,27 @@ import djklebynho from "@/assets/staff/djklebynho.jpg";
|
||||
import letialex from "@/assets/staff/letialex.jpg";
|
||||
import djcathie from "@/assets/staff/djcathie.jpg";
|
||||
|
||||
import gal1 from "@/assets/gallery/gal1.jpg";
|
||||
import gal2 from "@/assets/gallery/gal2.jpg";
|
||||
import gal3 from "@/assets/gallery/gal3.jpg";
|
||||
import gal4 from "@/assets/gallery/gal4.jpg";
|
||||
import gal5 from "@/assets/gallery/gal5.jpg";
|
||||
import gal6 from "@/assets/gallery/gal6.jpg";
|
||||
|
||||
|
||||
// ---- INFORMACIÓN GENERAL DEL EVENTO ----
|
||||
export const EVENT_INFO = {
|
||||
/** Fecha del evento — formato ISO para el countdown */
|
||||
date: "2026-09-04T12:00:00",
|
||||
venue: "[Nombre del Venue]",
|
||||
venueAddress: "[Dirección del venue, Barcelona]",
|
||||
venue: "Hotel Don Angel",
|
||||
venueAddress: "Carrer de la Riera, 123, 08001 Barcelona, Spain",
|
||||
/** 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",
|
||||
mapEmbedUrl: "https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d2982.0132199519758!2d2.718142676474322!3d41.63384568067116!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x12bb3e942c5ac469%3A0x9153afdd8d15d0c1!2sHotel%20Don%20Angel!5e0!3m2!1ses!2sus!4v1777971058774!5m2!1ses!2sus",
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// ---- WEBHOOK N8N ----
|
||||
/**
|
||||
* CONFIGURACIÓN DEL WEBHOOK:
|
||||
@@ -249,13 +258,11 @@ export const HOTEL_ROOMS = [
|
||||
// ---- 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" },
|
||||
{ name: "Aeropuerto de Barcelona-El Prat (BCN)", distance: "~85 km del Hotel" },
|
||||
{ name: "Aeropuerto de Girona (GRO)", distance: "~48 km del Hotel" },
|
||||
],
|
||||
howToGet: [
|
||||
{ method: "Metro", details: "[Línea y parada más cercana]" },
|
||||
{ method: "Bus", details: "[Líneas de bus cercanas]" },
|
||||
{ method: "Metro", details: "Línea y parada más cercana", link: "https://maps.app.goo.gl/YVyASwX2odX1mW5J6" },
|
||||
{ method: "Taxi/Uber", details: "Disponible desde cualquier punto de Barcelona" },
|
||||
],
|
||||
};
|
||||
@@ -266,21 +273,87 @@ export const PRACTICAL_INFO = {
|
||||
* 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]" },
|
||||
{ src: gal1, alt: "[Descripción foto 1]" },
|
||||
{ src: gal2, alt: "[Descripción foto 2]" },
|
||||
{ src: gal3, alt: "[Descripción foto 3]" },
|
||||
{ src: gal4, alt: "[Descripción foto 4]" },
|
||||
{ src: gal5, alt: "[Descripción foto 5]" },
|
||||
{ src: gal6, alt: "[Descripción foto 6]" },
|
||||
];
|
||||
|
||||
// ---- NAVEGACIÓN ----
|
||||
export const NAV_LINKS = [
|
||||
{ href: "#about" },
|
||||
{ href: "#staff" },
|
||||
{ href: "#schedule" },
|
||||
{ href: "#booking" },
|
||||
{ href: "#hotel" },
|
||||
{ href: "#info" },
|
||||
{ href: "#gallery" },
|
||||
// ---- PAQUETES MIXTOS (Room + Pass) ----
|
||||
export type RoomType = "individual" | "double" | "suite";
|
||||
export type PassType = "full" | "party";
|
||||
|
||||
export const ROOM_TYPES: { id: RoomType }[] = [
|
||||
{ id: "individual" },
|
||||
{ id: "double" },
|
||||
{ id: "suite" },
|
||||
];
|
||||
|
||||
// ---- PRECIOS DINÁMICOS FULL PASS ----
|
||||
/**
|
||||
* Calcula el precio del Full Pass según la fecha actual.
|
||||
* - Hasta 1 de julio: 170€
|
||||
* - 1 de julio - 1 de septiembre: 190€
|
||||
* - Después del inicio del evento: 200€ (último precio)
|
||||
*/
|
||||
export function getFullPassPrice(): { price: number; isLastPrice: boolean } {
|
||||
const now = new Date();
|
||||
const julyFirst = new Date("2026-07-01T00:00:00");
|
||||
const septemberFirst = new Date("2026-09-01T00:00:00");
|
||||
const eventStart = new Date(EVENT_INFO.date);
|
||||
|
||||
if (now < julyFirst) {
|
||||
return { price: 170, isLastPrice: false };
|
||||
} else if (now < septemberFirst) {
|
||||
return { price: 190, isLastPrice: false };
|
||||
} else {
|
||||
// After event starts
|
||||
return { price: 200, isLastPrice: true };
|
||||
}
|
||||
}
|
||||
|
||||
export const MIXED_BOOKING_PACKAGES = [
|
||||
{ id: "full" as PassType, label: "full", roomPrices: { individual: 100, double: 150, suite: 213 } },
|
||||
{ id: "party" as PassType, label: "party", roomPrices: { individual: 100, double: 150, suite: 213 } },
|
||||
];
|
||||
|
||||
// ---- SECCIONES VISIBLES ----
|
||||
export const SECTIONS = {
|
||||
about: true,
|
||||
org: true,
|
||||
staff: true,
|
||||
profesores: true,
|
||||
schedule: true,
|
||||
booking: false,
|
||||
mixed_booking: true,
|
||||
hotel: false,
|
||||
practical: true,
|
||||
gallery: true,
|
||||
};
|
||||
|
||||
// ---- NAVEGACIÓN ----
|
||||
const HREF_TO_SECTION: Record<string, keyof typeof SECTIONS> = {
|
||||
"#about": "about",
|
||||
"#staff": "staff",
|
||||
"#schedule": "schedule",
|
||||
"#booking": "booking",
|
||||
"#mixed-booking": "mixed_booking",
|
||||
"#hotel": "hotel",
|
||||
"#info": "practical",
|
||||
"#gallery": "gallery",
|
||||
};
|
||||
|
||||
export const NAV_LINKS = (
|
||||
[
|
||||
{ href: "#about" },
|
||||
{ href: "#staff" },
|
||||
{ href: "#schedule" },
|
||||
{ href: "#booking" },
|
||||
{ href: "#mixed-booking" },
|
||||
{ href: "#hotel" },
|
||||
{ href: "#info" },
|
||||
{ href: "#gallery" },
|
||||
] as const
|
||||
).filter((link) => SECTIONS[HREF_TO_SECTION[link.href]]);
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
"staff": "Staff",
|
||||
"schedule": "Schedule",
|
||||
"booking": "Book your pass",
|
||||
"mixed-booking": "Packs",
|
||||
"hotel": "Hotel",
|
||||
"info": "Info",
|
||||
"gallery": "Gallery"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Zouk Lambada Barcelona",
|
||||
"subtitle": "International Dance Festival",
|
||||
"dates": "June 20-22, 2025",
|
||||
"subtitle": "Beach Festival",
|
||||
"dates": "04 - 07 September 2026",
|
||||
"location": "Barcelona, Spain",
|
||||
"registerButton": "Register Now",
|
||||
"whatsappButton": "Join WhatsApp Group",
|
||||
@@ -56,9 +57,9 @@
|
||||
"schedule": {
|
||||
"title": "Schedule",
|
||||
"subtitle": "Three days of workshops, shows and social dance.",
|
||||
"friday": "Friday, June 20",
|
||||
"saturday": "Saturday, June 21",
|
||||
"sunday": "Sunday, June 22",
|
||||
"friday": "Friday, September 04",
|
||||
"saturday": "Saturday, September 05",
|
||||
"sunday": "Sunday, September 06",
|
||||
"workshop": "Workshop",
|
||||
"break": "Break",
|
||||
"social": "Social Dance",
|
||||
@@ -123,6 +124,33 @@
|
||||
"description": "[Brief room description]",
|
||||
"bookButton": "Book at hotel"
|
||||
},
|
||||
"mixedBooking": {
|
||||
"title": "Room + Pass Bundles",
|
||||
"subtitle": "Room + Pass bundles for your stay.",
|
||||
"priceTBD": "Price TBA",
|
||||
"selectRoom": "Choose room type",
|
||||
"popular": "Popular",
|
||||
"lastPrice": "Last Price",
|
||||
"perPerson": "/ person",
|
||||
"perParty": "/ party",
|
||||
"hotelQuestion": "Do you need a hotel room?",
|
||||
"hotelYes": "Yes",
|
||||
"hotelNo": "No",
|
||||
"nonHotelNote": "+50€ fee for not staying in hotel",
|
||||
"roomTypes": {
|
||||
"individual": "Single Room",
|
||||
"double": "Double Room",
|
||||
"suite": "Triple Room"
|
||||
},
|
||||
"passTypes": {
|
||||
"full": "Full Pass",
|
||||
"party": "Party Pass"
|
||||
},
|
||||
"fullDescription": "Full access to all workshops, socials, and festival activities",
|
||||
"fullFeatures": "All workshops|All social dances|Live shows|DJ sets",
|
||||
"partyDescription": "Price for each party individually, even pool parties.",
|
||||
"partyFeatures": "Friday party|Saturday party|Sunday farewell party|DJ sets"
|
||||
},
|
||||
"info": {
|
||||
"title": "Practical Information",
|
||||
"airports": "Nearby Airports",
|
||||
@@ -134,7 +162,8 @@
|
||||
"busLines": "[Nearby bus lines]",
|
||||
"taxiDetails": "Available from anywhere in Barcelona",
|
||||
"venue": "Event Venue",
|
||||
"mapTitle": "Event Location"
|
||||
"mapTitle": "Event Location",
|
||||
"viewOnMap": "View on map"
|
||||
},
|
||||
"gallery": {
|
||||
"title": "Gallery"
|
||||
|
||||
@@ -4,14 +4,15 @@
|
||||
"staff": "Staff",
|
||||
"schedule": "Programa",
|
||||
"booking": "Reservar",
|
||||
"mixed-booking": "Packs",
|
||||
"hotel": "Hotel",
|
||||
"info": "Info",
|
||||
"gallery": "Galería"
|
||||
},
|
||||
"hero": {
|
||||
"title": "Zouk Lambada Barcelona",
|
||||
"subtitle": "Festival Internacional de Baile",
|
||||
"dates": "20-22 Junio 2025",
|
||||
"subtitle": "Beach Festival",
|
||||
"dates": "04 - 07 Setiembre 2026",
|
||||
"location": "Barcelona, España",
|
||||
"registerButton": "Registrarse Ahora",
|
||||
"whatsappButton": "Unirse al Grupo de WhatsApp",
|
||||
@@ -56,9 +57,9 @@
|
||||
"schedule": {
|
||||
"title": "Programa",
|
||||
"subtitle": "Tres días de workshops, shows y social dance.",
|
||||
"friday": "Viernes 20 Junio",
|
||||
"saturday": "Sábado 21 Junio",
|
||||
"sunday": "Domingo 22 Junio",
|
||||
"friday": "Viernes 04 Setiembre",
|
||||
"saturday": "Sábado 05 Setiembre",
|
||||
"sunday": "Domingo 06 Setiembre",
|
||||
"workshop": "Workshop",
|
||||
"break": "Pausa",
|
||||
"social": "Social Dance",
|
||||
@@ -123,6 +124,33 @@
|
||||
"description": "[Descripción breve de la habitación]",
|
||||
"bookButton": "Reservar en el hotel"
|
||||
},
|
||||
"mixedBooking": {
|
||||
"title": "Packs de Habitación + Pase",
|
||||
"subtitle": "Combina habitación + pase para tu estancia.",
|
||||
"priceTBD": "Precio por confirmar",
|
||||
"selectRoom": "Elige tipo de habitación",
|
||||
"popular": "Popular",
|
||||
"lastPrice": "Último Precio",
|
||||
"perPerson": "/ persona",
|
||||
"perParty": "/ fiesta",
|
||||
"hotelQuestion": "¿Necesitas habitación de hotel?",
|
||||
"hotelYes": "Sí",
|
||||
"hotelNo": "No",
|
||||
"nonHotelNote": "+50€ por no alojarse en hotel",
|
||||
"roomTypes": {
|
||||
"individual": "Habitación Individual",
|
||||
"double": "Habitación Doble",
|
||||
"suite": "Habitación Triple"
|
||||
},
|
||||
"passTypes": {
|
||||
"full": "Full Pass",
|
||||
"party": "Party Pass"
|
||||
},
|
||||
"fullDescription": "Acceso completo a todos los workshops, sociales y actividades del festival",
|
||||
"fullFeatures": "Todos los workshops|Todas las social dances|Shows en vivo|DJ sets",
|
||||
"partyDescription": "Precio individual por cada fiesta, incluso las pool parties.",
|
||||
"partyFeatures": "Fiesta del viernes|Fiesta del sábado|Fiesta despedida del domingo|DJ sets"
|
||||
},
|
||||
"info": {
|
||||
"title": "Información Práctica",
|
||||
"airports": "Aeropuertos Cercanos",
|
||||
@@ -134,7 +162,8 @@
|
||||
"busLines": "[Líneas de bus cercanas]",
|
||||
"taxiDetails": "Disponible desde cualquier punto de Barcelona",
|
||||
"venue": "Lugar del Evento",
|
||||
"mapTitle": "Ubicación del evento"
|
||||
"mapTitle": "Ubicación del evento",
|
||||
"viewOnMap": "Ver en mapa"
|
||||
},
|
||||
"gallery": {
|
||||
"title": "Galería"
|
||||
@@ -146,7 +175,7 @@
|
||||
"title": "ZoukLambadaBCN",
|
||||
"contact": "Contacto",
|
||||
"followUs": "Síguenos",
|
||||
"email": "[email@zouklambadabcn.com]",
|
||||
"email": "[info@zouklambadabcn.com]",
|
||||
"copyright": "© {{year}} ZoukLambadaBCN. Todos los derechos reservados."
|
||||
},
|
||||
"language": {
|
||||
|
||||
@@ -6,11 +6,13 @@ import StaffSection from "@/components/StaffSection";
|
||||
import ProfesoresSection from "@/components/ProfesoresSection";
|
||||
import ScheduleSection from "@/components/ScheduleSection";
|
||||
import BookingSection from "@/components/BookingSection";
|
||||
import MixedBookingSection from "@/components/MixedBookingSection";
|
||||
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";
|
||||
import { SECTIONS } from "@/data/event-data";
|
||||
|
||||
/**
|
||||
* Landing page — Lambada Festival Barcelona
|
||||
@@ -23,15 +25,16 @@ const Index = () => {
|
||||
<div className="min-h-screen">
|
||||
<Navbar />
|
||||
<HeroSection />
|
||||
<AboutSection />
|
||||
<OrgSection />
|
||||
<StaffSection />
|
||||
<ProfesoresSection />
|
||||
<ScheduleSection />
|
||||
<BookingSection />
|
||||
<HotelSection />
|
||||
<PracticalSection />
|
||||
<GallerySection />
|
||||
{SECTIONS.about && <AboutSection />}
|
||||
{SECTIONS.org && <OrgSection />}
|
||||
{SECTIONS.staff && <StaffSection />}
|
||||
{SECTIONS.profesores && <ProfesoresSection />}
|
||||
{SECTIONS.schedule && <ScheduleSection />}
|
||||
{SECTIONS.booking && <BookingSection />}
|
||||
{SECTIONS.mixed_booking && <MixedBookingSection />}
|
||||
{SECTIONS.hotel && <HotelSection />}
|
||||
{SECTIONS.practical && <PracticalSection />}
|
||||
{SECTIONS.gallery && <GallerySection />}
|
||||
<FooterSection />
|
||||
<FloatingButton />
|
||||
</div>
|
||||
|
||||