Changes in module "mixed reservations"
All checks were successful
Deploy NPM app / Deploy NPM (push) Successful in 1m23s

This commit is contained in:
Ichitux
2026-04-24 01:59:50 +02:00
parent 823ca68119
commit fce84ff4a6
4 changed files with 150 additions and 54 deletions

View File

@@ -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"
}`} }`}
> >
<div className="absolute top-4 right-4 z-10 flex flex-col gap-2 items-end">
{isFeatured && ( {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"> <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,12 +112,52 @@ 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-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"> <Label className="block text-sm font-medium text-foreground mb-2">
{t("mixedBooking.selectRoom")} {t("mixedBooking.selectRoom")}
</Label> </Label>
@@ -119,6 +185,8 @@ const MixedBookingSection = () => {
))} ))}
</SelectContent> </SelectContent>
</Select> </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">

View File

@@ -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,
}; };

View File

@@ -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",

View File

@@ -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",