mirror of
https://github.com/Ichitux/lambada-fiesta-live.git
synced 2026-05-15 18:52:20 +02:00
Newer section and modular view
All checks were successful
Deploy NPM app / Deploy NPM (push) Successful in 1m14s
All checks were successful
Deploy NPM app / Deploy NPM (push) Successful in 1m14s
This commit is contained in:
@@ -3,6 +3,7 @@ import { motion, AnimatePresence } from "framer-motion";
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { ChevronUp } from "lucide-react";
|
import { ChevronUp } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { SECTIONS } from "@/data/event-data";
|
||||||
|
|
||||||
/** Botón flotante de reserva + scroll to top */
|
/** Botón flotante de reserva + scroll to top */
|
||||||
const FloatingButton = () => {
|
const FloatingButton = () => {
|
||||||
@@ -30,7 +31,7 @@ const FloatingButton = () => {
|
|||||||
className="animate-pulse-glow rounded-full px-6 shadow-elevated"
|
className="animate-pulse-glow rounded-full px-6 shadow-elevated"
|
||||||
asChild
|
asChild
|
||||||
>
|
>
|
||||||
<a href="#booking" 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 */}
|
{/* Inline SVG: dancing couple icon */}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect } from "react";
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useTranslation } from "react-i18next";
|
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";
|
import heroBg from "@/assets/hero-bg.jpg";
|
||||||
|
|
||||||
/** Calcula diferencia entre ahora y la fecha del evento */
|
/** Calcula diferencia entre ahora y la fecha del evento */
|
||||||
@@ -116,7 +116,7 @@ const HeroSection = () => {
|
|||||||
transition={{ delay: 1 }}
|
transition={{ delay: 1 }}
|
||||||
>
|
>
|
||||||
<Button variant="hero" size="lg" className="text-lg px-10 py-6" asChild>
|
<Button variant="hero" size="lg" className="text-lg px-10 py-6" asChild>
|
||||||
<a href="#booking">{t('hero.bookYourPass')}</a>
|
<a href={SECTIONS.booking ? "#booking" : "#mixed-booking"}>{t('hero.bookYourPass')}</a>
|
||||||
</Button>
|
</Button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
145
src/components/MixedBookingSection.tsx
Normal file
145
src/components/MixedBookingSection.tsx
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { MIXED_BOOKING_PACKAGES, ROOM_TYPES } from "@/data/event-data";
|
||||||
|
import type { RoomType } from "@/data/event-data";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Check, Star } from "lucide-react";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
|
const FEATURED_PASS = "full";
|
||||||
|
|
||||||
|
const MixedBookingSection = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [selectedRooms, setSelectedRooms] = useState<Record<string, RoomType>>({
|
||||||
|
full: "individual",
|
||||||
|
party: "individual",
|
||||||
|
single: "individual",
|
||||||
|
});
|
||||||
|
|
||||||
|
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 lg:grid-cols-3 gap-6 lg:gap-8 max-w-5xl mx-auto items-stretch">
|
||||||
|
{MIXED_BOOKING_PACKAGES.map((pkg, i) => {
|
||||||
|
const selectedRoom = selectedRooms[pkg.id] || "individual";
|
||||||
|
const price = pkg.roomPrices[selectedRoom];
|
||||||
|
const isFeatured = pkg.id === FEATURED_PASS;
|
||||||
|
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"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isFeatured && (
|
||||||
|
<div className="absolute top-4 right-4 z-10">
|
||||||
|
<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>
|
||||||
|
</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-xs text-muted-foreground mt-1">
|
||||||
|
{t("mixedBooking.perPerson")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 pt-4 pb-2 bg-card">
|
||||||
|
<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;
|
||||||
@@ -274,13 +274,57 @@ export const GALLERY_IMAGES = [
|
|||||||
{ src: "", alt: "[Descripción foto 6]" },
|
{ src: "", alt: "[Descripción foto 6]" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// ---- NAVEGACIÓN ----
|
// ---- PAQUETES MIXTOS (Room + Pass) ----
|
||||||
export const NAV_LINKS = [
|
export type RoomType = "individual" | "double" | "suite";
|
||||||
{ href: "#about" },
|
export type PassType = "full" | "party" | "single";
|
||||||
{ href: "#staff" },
|
|
||||||
{ href: "#schedule" },
|
export const ROOM_TYPES: { id: RoomType }[] = [
|
||||||
{ href: "#booking" },
|
{ id: "individual" },
|
||||||
{ href: "#hotel" },
|
{ id: "double" },
|
||||||
{ href: "#info" },
|
{ id: "suite" },
|
||||||
{ href: "#gallery" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const MIXED_BOOKING_PACKAGES = [
|
||||||
|
{ id: "full" as PassType, label: "full", roomPrices: { individual: 0, double: 0, suite: 0 } },
|
||||||
|
{ id: "party" as PassType, label: "party", roomPrices: { individual: 0, double: 0, suite: 0 } },
|
||||||
|
{ id: "single" as PassType, label: "single", roomPrices: { individual: 0, double: 0, suite: 0 } },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---- SECCIONES VISIBLES ----
|
||||||
|
export const SECTIONS = {
|
||||||
|
about: true,
|
||||||
|
org: true,
|
||||||
|
staff: true,
|
||||||
|
profesores: true,
|
||||||
|
schedule: true,
|
||||||
|
booking: false,
|
||||||
|
mixed_booking: true,
|
||||||
|
hotel: true,
|
||||||
|
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,6 +4,7 @@
|
|||||||
"staff": "Staff",
|
"staff": "Staff",
|
||||||
"schedule": "Schedule",
|
"schedule": "Schedule",
|
||||||
"booking": "Book your pass",
|
"booking": "Book your pass",
|
||||||
|
"mixed-booking": "Packs",
|
||||||
"hotel": "Hotel",
|
"hotel": "Hotel",
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"gallery": "Gallery"
|
"gallery": "Gallery"
|
||||||
@@ -123,6 +124,30 @@
|
|||||||
"description": "[Brief room description]",
|
"description": "[Brief room description]",
|
||||||
"bookButton": "Book at hotel"
|
"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",
|
||||||
|
"perPerson": "/ person",
|
||||||
|
"roomTypes": {
|
||||||
|
"individual": "Single Room",
|
||||||
|
"double": "Double Room",
|
||||||
|
"suite": "Triple Room"
|
||||||
|
},
|
||||||
|
"passTypes": {
|
||||||
|
"full": "Full Pass",
|
||||||
|
"party": "Party Pass",
|
||||||
|
"single": "Single Day Pass"
|
||||||
|
},
|
||||||
|
"fullDescription": "Full access to all workshops, socials, and festival activities",
|
||||||
|
"fullFeatures": "All workshops|All social dances|Live shows|DJ sets",
|
||||||
|
"partyDescription": "Access to all parties (Friday, Saturday, and Sunday)",
|
||||||
|
"partyFeatures": "Friday party|Saturday party|Sunday farewell party|DJ sets",
|
||||||
|
"singleDescription": "Access to a single day of the festival",
|
||||||
|
"singleFeatures": "One day access|Workshops|Social dance|DJ set"
|
||||||
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Practical Information",
|
"title": "Practical Information",
|
||||||
"airports": "Nearby Airports",
|
"airports": "Nearby Airports",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"staff": "Staff",
|
"staff": "Staff",
|
||||||
"schedule": "Programa",
|
"schedule": "Programa",
|
||||||
"booking": "Reservar",
|
"booking": "Reservar",
|
||||||
|
"mixed-booking": "Packs",
|
||||||
"hotel": "Hotel",
|
"hotel": "Hotel",
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"gallery": "Galería"
|
"gallery": "Galería"
|
||||||
@@ -123,6 +124,30 @@
|
|||||||
"description": "[Descripción breve de la habitación]",
|
"description": "[Descripción breve de la habitación]",
|
||||||
"bookButton": "Reservar en el hotel"
|
"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",
|
||||||
|
"perPerson": "/ persona",
|
||||||
|
"roomTypes": {
|
||||||
|
"individual": "Habitación Individual",
|
||||||
|
"double": "Habitación Doble",
|
||||||
|
"suite": "Habitación Triple"
|
||||||
|
},
|
||||||
|
"passTypes": {
|
||||||
|
"full": "Full Pass",
|
||||||
|
"party": "Party Pass",
|
||||||
|
"single": "Single Day 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": "Acceso a todas las fiestas (Viernes, Sábado y Domingo)",
|
||||||
|
"partyFeatures": "Fiesta del viernes|Fiesta del sábado|Fiesta despedida del domingo|DJ sets",
|
||||||
|
"singleDescription": "Acceso a un solo día del festival",
|
||||||
|
"singleFeatures": "Acceso un día|Workshops|Social dance|DJ set"
|
||||||
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Información Práctica",
|
"title": "Información Práctica",
|
||||||
"airports": "Aeropuertos Cercanos",
|
"airports": "Aeropuertos Cercanos",
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import StaffSection from "@/components/StaffSection";
|
|||||||
import ProfesoresSection from "@/components/ProfesoresSection";
|
import ProfesoresSection from "@/components/ProfesoresSection";
|
||||||
import ScheduleSection from "@/components/ScheduleSection";
|
import ScheduleSection from "@/components/ScheduleSection";
|
||||||
import BookingSection from "@/components/BookingSection";
|
import BookingSection from "@/components/BookingSection";
|
||||||
|
import MixedBookingSection from "@/components/MixedBookingSection";
|
||||||
import HotelSection from "@/components/HotelSection";
|
import HotelSection from "@/components/HotelSection";
|
||||||
import PracticalSection from "@/components/PracticalSection";
|
import PracticalSection from "@/components/PracticalSection";
|
||||||
import GallerySection from "@/components/GallerySection";
|
import GallerySection from "@/components/GallerySection";
|
||||||
import FooterSection from "@/components/FooterSection";
|
import FooterSection from "@/components/FooterSection";
|
||||||
import FloatingButton from "@/components/FloatingButton";
|
import FloatingButton from "@/components/FloatingButton";
|
||||||
|
import { SECTIONS } from "@/data/event-data";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Landing page — Lambada Festival Barcelona
|
* Landing page — Lambada Festival Barcelona
|
||||||
@@ -23,15 +25,16 @@ const Index = () => {
|
|||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
<AboutSection />
|
{SECTIONS.about && <AboutSection />}
|
||||||
<OrgSection />
|
{SECTIONS.org && <OrgSection />}
|
||||||
<StaffSection />
|
{SECTIONS.staff && <StaffSection />}
|
||||||
<ProfesoresSection />
|
{SECTIONS.profesores && <ProfesoresSection />}
|
||||||
<ScheduleSection />
|
{SECTIONS.schedule && <ScheduleSection />}
|
||||||
<BookingSection />
|
{SECTIONS.booking && <BookingSection />}
|
||||||
<HotelSection />
|
{SECTIONS.mixed_booking && <MixedBookingSection />}
|
||||||
<PracticalSection />
|
{SECTIONS.hotel && <HotelSection />}
|
||||||
<GallerySection />
|
{SECTIONS.practical && <PracticalSection />}
|
||||||
|
{SECTIONS.gallery && <GallerySection />}
|
||||||
<FooterSection />
|
<FooterSection />
|
||||||
<FloatingButton />
|
<FloatingButton />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user