mirror of
https://github.com/Ichitux/lambada-fiesta-live.git
synced 2026-05-15 16:12:20 +02:00
Changes in module "mixed reservations"
All checks were successful
Deploy NPM app / Deploy NPM (push) Successful in 1m23s
All checks were successful
Deploy NPM app / Deploy NPM (push) Successful in 1m23s
This commit is contained in:
@@ -1,20 +1,26 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { MIXED_BOOKING_PACKAGES, ROOM_TYPES } from "@/data/event-data";
|
import { MIXED_BOOKING_PACKAGES, ROOM_TYPES, getFullPassPrice } from "@/data/event-data";
|
||||||
import type { RoomType } from "@/data/event-data";
|
import type { RoomType } from "@/data/event-data";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Check, Star } from "lucide-react";
|
import { Check, Star, Circle, CheckCircle2 } from "lucide-react";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
|
||||||
const FEATURED_PASS = "full";
|
const FEATURED_PASS = "full";
|
||||||
|
const NON_HOTEL_FEE = 50;
|
||||||
|
const PARTY_PRICE = 25;
|
||||||
|
|
||||||
const MixedBookingSection = () => {
|
const MixedBookingSection = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const fullPassPricing = getFullPassPrice();
|
||||||
const [selectedRooms, setSelectedRooms] = useState<Record<string, RoomType>>({
|
const [selectedRooms, setSelectedRooms] = useState<Record<string, RoomType>>({
|
||||||
full: "individual",
|
full: "individual",
|
||||||
party: "individual",
|
party: "individual",
|
||||||
single: "individual",
|
});
|
||||||
|
const [wantsHotel, setWantsHotel] = useState<Record<string, boolean | null>>({
|
||||||
|
full: null,
|
||||||
|
party: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -36,11 +42,26 @@ const MixedBookingSection = () => {
|
|||||||
<p className="text-muted-foreground max-w-2xl mx-auto">{t("mixedBooking.subtitle")}</p>
|
<p className="text-muted-foreground max-w-2xl mx-auto">{t("mixedBooking.subtitle")}</p>
|
||||||
</motion.div>
|
</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">
|
<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) => {
|
{MIXED_BOOKING_PACKAGES.map((pkg, i) => {
|
||||||
const selectedRoom = selectedRooms[pkg.id] || "individual";
|
const selectedRoom = selectedRooms[pkg.id] || "individual";
|
||||||
const price = pkg.roomPrices[selectedRoom];
|
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 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());
|
const features = t(`mixedBooking.${pkg.id}Features`).split("|").map((f: string) => f.trim());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,14 +77,19 @@ const MixedBookingSection = () => {
|
|||||||
: "shadow-card hover:shadow-elevated border border-border"
|
: "shadow-card hover:shadow-elevated border border-border"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isFeatured && (
|
<div className="absolute top-4 right-4 z-10 flex flex-col gap-2 items-end">
|
||||||
<div className="absolute top-4 right-4 z-10">
|
{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">
|
<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" />
|
<Star className="w-3 h-3 fill-current" />
|
||||||
{t("mixedBooking.popular")}
|
{t("mixedBooking.popular")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
)}
|
||||||
)}
|
{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 ${
|
<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"
|
isFeatured ? "bg-gradient-tropical" : "bg-secondary/10"
|
||||||
@@ -86,39 +112,81 @@ const MixedBookingSection = () => {
|
|||||||
<p className="text-4xl md:text-5xl font-bold text-primary">
|
<p className="text-4xl md:text-5xl font-bold text-primary">
|
||||||
{price > 0 ? `${price}€` : t("mixedBooking.priceTBD")}
|
{price > 0 ? `${price}€` : t("mixedBooking.priceTBD")}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground mt-1">
|
<p className="text-sm font-medium text-foreground mt-2">
|
||||||
{t("mixedBooking.perPerson")}
|
{t(`mixedBooking.passTypes.${pkg.id}`)}: {passPrice}€ {pkg.id === "full" ? t("mixedBooking.perPerson") : t("mixedBooking.perParty")}
|
||||||
</p>
|
</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>
|
||||||
|
|
||||||
<div className="px-6 pt-4 pb-2 bg-card">
|
<div className="px-6 pt-4 pb-2 bg-card">
|
||||||
<Label className="block text-sm font-medium text-foreground mb-2">
|
<Label className="block text-sm font-medium text-foreground mb-3">
|
||||||
{t("mixedBooking.selectRoom")}
|
{t("mixedBooking.hotelQuestion")}
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<div className="flex gap-3 mb-4">
|
||||||
value={selectedRoom}
|
<button
|
||||||
onValueChange={(value) =>
|
onClick={() => setWantsHotel((prev) => ({ ...prev, [pkg.id]: true }))}
|
||||||
setSelectedRooms((prev) => ({
|
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 ${
|
||||||
...prev,
|
hasHotel
|
||||||
[pkg.id]: value as RoomType,
|
? "border-primary bg-primary/5 text-primary"
|
||||||
}))
|
: "border-input bg-background text-muted-foreground hover:border-primary/30"
|
||||||
}
|
}`}
|
||||||
>
|
>
|
||||||
<SelectTrigger className={`w-full rounded-xl border px-4 py-3 text-sm text-foreground shadow-sm transition-colors duration-200 ${
|
<CheckCircle2 className={`w-5 h-5 ${hasHotel ? "text-primary" : "text-muted-foreground"}`} />
|
||||||
isFeatured
|
{t("mixedBooking.hotelYes")}
|
||||||
? "border-primary/30 bg-primary/5 hover:border-primary/50"
|
</button>
|
||||||
: "border-input bg-background hover:border-primary/30"
|
<button
|
||||||
}`}>
|
onClick={() => setWantsHotel((prev) => ({ ...prev, [pkg.id]: false }))}
|
||||||
<SelectValue placeholder={t("mixedBooking.selectRoom")} />
|
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 ${
|
||||||
</SelectTrigger>
|
!hasHotel
|
||||||
<SelectContent>
|
? "border-primary bg-primary/5 text-primary"
|
||||||
{ROOM_TYPES.map((room) => (
|
: "border-input bg-background text-muted-foreground hover:border-primary/30"
|
||||||
<SelectItem key={room.id} value={room.id}>
|
}`}
|
||||||
{t(`mixedBooking.roomTypes.${room.id}`)}
|
>
|
||||||
</SelectItem>
|
<Circle className={`w-5 h-5 ${!hasHotel ? "text-primary" : "text-muted-foreground"}`} />
|
||||||
))}
|
{t("mixedBooking.hotelNo")}
|
||||||
</SelectContent>
|
</button>
|
||||||
</Select>
|
</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>
|
||||||
|
|
||||||
<div className="px-6 pt-4 pb-6 flex-1 flex flex-col justify-between bg-card">
|
<div className="px-6 pt-4 pb-6 flex-1 flex flex-col justify-between bg-card">
|
||||||
|
|||||||
@@ -276,7 +276,7 @@ export const GALLERY_IMAGES = [
|
|||||||
|
|
||||||
// ---- PAQUETES MIXTOS (Room + Pass) ----
|
// ---- PAQUETES MIXTOS (Room + Pass) ----
|
||||||
export type RoomType = "individual" | "double" | "suite";
|
export type RoomType = "individual" | "double" | "suite";
|
||||||
export type PassType = "full" | "party" | "single";
|
export type PassType = "full" | "party";
|
||||||
|
|
||||||
export const ROOM_TYPES: { id: RoomType }[] = [
|
export const ROOM_TYPES: { id: RoomType }[] = [
|
||||||
{ id: "individual" },
|
{ id: "individual" },
|
||||||
@@ -284,10 +284,32 @@ export const ROOM_TYPES: { id: RoomType }[] = [
|
|||||||
{ id: "suite" },
|
{ 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 = [
|
export const MIXED_BOOKING_PACKAGES = [
|
||||||
{ id: "full" as PassType, label: "full", roomPrices: { individual: 0, double: 0, suite: 0 } },
|
{ id: "full" as PassType, label: "full", roomPrices: { individual: 100, double: 150, suite: 213 } },
|
||||||
{ id: "party" as PassType, label: "party", roomPrices: { individual: 0, double: 0, suite: 0 } },
|
{ id: "party" as PassType, label: "party", roomPrices: { individual: 100, double: 150, suite: 213 } },
|
||||||
{ id: "single" as PassType, label: "single", roomPrices: { individual: 0, double: 0, suite: 0 } },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// ---- SECCIONES VISIBLES ----
|
// ---- SECCIONES VISIBLES ----
|
||||||
@@ -299,7 +321,7 @@ export const SECTIONS = {
|
|||||||
schedule: true,
|
schedule: true,
|
||||||
booking: false,
|
booking: false,
|
||||||
mixed_booking: true,
|
mixed_booking: true,
|
||||||
hotel: true,
|
hotel: false,
|
||||||
practical: true,
|
practical: true,
|
||||||
gallery: true,
|
gallery: true,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -130,7 +130,13 @@
|
|||||||
"priceTBD": "Price TBA",
|
"priceTBD": "Price TBA",
|
||||||
"selectRoom": "Choose room type",
|
"selectRoom": "Choose room type",
|
||||||
"popular": "Popular",
|
"popular": "Popular",
|
||||||
|
"lastPrice": "Last Price",
|
||||||
"perPerson": "/ person",
|
"perPerson": "/ person",
|
||||||
|
"perParty": "/ party",
|
||||||
|
"hotelQuestion": "Do you need a hotel room?",
|
||||||
|
"hotelYes": "Yes",
|
||||||
|
"hotelNo": "No",
|
||||||
|
"nonHotelNote": "+50€ fee for not staying in hotel",
|
||||||
"roomTypes": {
|
"roomTypes": {
|
||||||
"individual": "Single Room",
|
"individual": "Single Room",
|
||||||
"double": "Double Room",
|
"double": "Double Room",
|
||||||
@@ -138,15 +144,12 @@
|
|||||||
},
|
},
|
||||||
"passTypes": {
|
"passTypes": {
|
||||||
"full": "Full Pass",
|
"full": "Full Pass",
|
||||||
"party": "Party Pass",
|
"party": "Party Pass"
|
||||||
"single": "Single Day Pass"
|
|
||||||
},
|
},
|
||||||
"fullDescription": "Full access to all workshops, socials, and festival activities",
|
"fullDescription": "Full access to all workshops, socials, and festival activities",
|
||||||
"fullFeatures": "All workshops|All social dances|Live shows|DJ sets",
|
"fullFeatures": "All workshops|All social dances|Live shows|DJ sets",
|
||||||
"partyDescription": "Access to all parties (Friday, Saturday, and Sunday)",
|
"partyDescription": "Price for each party individually, even pool parties.",
|
||||||
"partyFeatures": "Friday party|Saturday party|Sunday farewell party|DJ sets",
|
"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",
|
||||||
|
|||||||
@@ -130,7 +130,13 @@
|
|||||||
"priceTBD": "Precio por confirmar",
|
"priceTBD": "Precio por confirmar",
|
||||||
"selectRoom": "Elige tipo de habitación",
|
"selectRoom": "Elige tipo de habitación",
|
||||||
"popular": "Popular",
|
"popular": "Popular",
|
||||||
|
"lastPrice": "Último Precio",
|
||||||
"perPerson": "/ persona",
|
"perPerson": "/ persona",
|
||||||
|
"perParty": "/ fiesta",
|
||||||
|
"hotelQuestion": "¿Necesitas habitación de hotel?",
|
||||||
|
"hotelYes": "Sí",
|
||||||
|
"hotelNo": "No",
|
||||||
|
"nonHotelNote": "+50€ por no alojarse en hotel",
|
||||||
"roomTypes": {
|
"roomTypes": {
|
||||||
"individual": "Habitación Individual",
|
"individual": "Habitación Individual",
|
||||||
"double": "Habitación Doble",
|
"double": "Habitación Doble",
|
||||||
@@ -138,15 +144,12 @@
|
|||||||
},
|
},
|
||||||
"passTypes": {
|
"passTypes": {
|
||||||
"full": "Full Pass",
|
"full": "Full Pass",
|
||||||
"party": "Party Pass",
|
"party": "Party Pass"
|
||||||
"single": "Single Day Pass"
|
|
||||||
},
|
},
|
||||||
"fullDescription": "Acceso completo a todos los workshops, sociales y actividades del festival",
|
"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",
|
"fullFeatures": "Todos los workshops|Todas las social dances|Shows en vivo|DJ sets",
|
||||||
"partyDescription": "Acceso a todas las fiestas (Viernes, Sábado y Domingo)",
|
"partyDescription": "Precio individual por cada fiesta, incluso las pool parties.",
|
||||||
"partyFeatures": "Fiesta del viernes|Fiesta del sábado|Fiesta despedida del domingo|DJ sets",
|
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user