Merge branch 'master' into feature/tracking
This commit is contained in:
@@ -2,8 +2,7 @@
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import FacebookIcon from "@/components/Icons/Facebook"
|
||||
import InstagramIcon from "@/components/Icons/Instagram"
|
||||
import { FacebookIcon, InstagramIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
@@ -76,6 +76,9 @@ export default function BedType({ bedTypes }: BedTypeProps) {
|
||||
subtitle={width}
|
||||
title={roomType.description}
|
||||
value={roomType.value}
|
||||
handleSelectedOnClick={
|
||||
bedType === roomType.value ? completeStep : undefined
|
||||
}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -97,6 +97,9 @@ export default function Breakfast({ packages }: BreakfastProps) {
|
||||
})}
|
||||
title={intl.formatMessage({ id: "Breakfast buffet" })}
|
||||
value={pkg.code}
|
||||
handleSelectedOnClick={
|
||||
breakfast === pkg.code ? completeStep : undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<RadioCard
|
||||
@@ -113,6 +116,9 @@ export default function Breakfast({ packages }: BreakfastProps) {
|
||||
})}
|
||||
title={intl.formatMessage({ id: "No breakfast" })}
|
||||
value="false"
|
||||
handleSelectedOnClick={
|
||||
breakfast === "false" ? completeStep : undefined
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
</FormProvider>
|
||||
|
||||
@@ -55,4 +55,8 @@ export const signedInDetailsSchema = z.object({
|
||||
firstName: z.string().optional(),
|
||||
lastName: z.string().optional(),
|
||||
phoneNumber: z.string().optional(),
|
||||
join: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.transform((_) => false),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
import { detailsStorageName } from "@/stores/details"
|
||||
|
||||
import { createQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
import { DetailsState } from "@/types/stores/details"
|
||||
|
||||
export default function PaymentCallback({
|
||||
returnUrl,
|
||||
searchObject,
|
||||
}: {
|
||||
returnUrl: string
|
||||
searchObject: URLSearchParams
|
||||
}) {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const bookingData = window.sessionStorage.getItem(detailsStorageName)
|
||||
|
||||
if (bookingData) {
|
||||
const detailsStorage: Record<
|
||||
"state",
|
||||
Pick<DetailsState, "data">
|
||||
> = JSON.parse(bookingData)
|
||||
const searchParams = createQueryParamsForEnterDetails(
|
||||
detailsStorage.state.data.booking,
|
||||
searchObject
|
||||
)
|
||||
|
||||
if (searchParams.size > 0) {
|
||||
router.replace(`${returnUrl}?${searchParams.toString()}`)
|
||||
}
|
||||
}
|
||||
}, [returnUrl, router, searchObject])
|
||||
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
@@ -51,6 +51,7 @@ function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
|
||||
}
|
||||
|
||||
export default function Payment({
|
||||
user,
|
||||
roomPrice,
|
||||
otherPaymentOptions,
|
||||
savedCreditCards,
|
||||
@@ -59,7 +60,6 @@ export default function Payment({
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const queryParams = useSearchParams()
|
||||
const { booking, ...userData } = useDetailsStore((state) => state.data)
|
||||
const setIsSubmittingDisabled = useDetailsStore(
|
||||
(state) => state.actions.setIsSubmittingDisabled
|
||||
@@ -163,9 +163,6 @@ export default function Payment({
|
||||
])
|
||||
|
||||
function handleSubmit(data: PaymentFormData) {
|
||||
const allQueryParams =
|
||||
queryParams.size > 0 ? `?${queryParams.toString()}` : ""
|
||||
|
||||
// set payment method to card if saved card is submitted
|
||||
const paymentMethod = isPaymentMethodEnum(data.paymentMethod)
|
||||
? data.paymentMethod
|
||||
@@ -175,6 +172,8 @@ export default function Payment({
|
||||
(card) => card.id === data.paymentMethod
|
||||
)
|
||||
|
||||
const paymentRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback`
|
||||
|
||||
initiateBooking.mutate({
|
||||
hotelId: hotel,
|
||||
checkInDate: fromDate,
|
||||
@@ -185,7 +184,8 @@ export default function Payment({
|
||||
age: child.age,
|
||||
bedType: bedTypeMap[parseInt(child.bed.toString())],
|
||||
})),
|
||||
rateCode: room.rateCode,
|
||||
rateCode:
|
||||
user || join || membershipNo ? room.counterRateCode : room.rateCode,
|
||||
roomTypeCode: bedType!.roomTypeCode, // A selection has been made in order to get to this step.
|
||||
guest: {
|
||||
title: "",
|
||||
@@ -222,9 +222,9 @@ export default function Payment({
|
||||
}
|
||||
: undefined,
|
||||
|
||||
success: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/success`,
|
||||
error: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/error${allQueryParams}`,
|
||||
cancel: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/cancel${allQueryParams}`,
|
||||
success: `${paymentRedirectUrl}/success`,
|
||||
error: `${paymentRedirectUrl}/error`,
|
||||
cancel: `${paymentRedirectUrl}/cancel`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"use client"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
import { hotelreservation } from "@/constants/routes/hotelReservation"
|
||||
import { detailsStorageName } from "@/stores/details"
|
||||
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
/**
|
||||
* Cleanup component to make sure no stale data is left
|
||||
* from previous booking when user is not in the booking
|
||||
* flow anymore
|
||||
*/
|
||||
export default function StorageCleaner() {
|
||||
const lang = useLang()
|
||||
const pathname = usePathname()
|
||||
|
||||
useEffect(() => {
|
||||
if (!pathname.startsWith(hotelreservation(lang))) {
|
||||
sessionStorage.removeItem(detailsStorageName)
|
||||
}
|
||||
}, [lang, pathname])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export function SummaryBottomSheet({ children }: PropsWithChildren) {
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(totalPrice.local.price),
|
||||
amount: intl.formatNumber(totalPrice.local.amount),
|
||||
currency: totalPrice.local.currency,
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { ChevronDown } from "react-feather"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
@@ -33,6 +33,8 @@ function storeSelector(state: DetailsState) {
|
||||
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||
setTotalPrice: state.actions.setTotalPrice,
|
||||
totalPrice: state.totalPrice,
|
||||
join: state.data.join,
|
||||
membershipNo: state.data.membershipNo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +53,8 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
toDate,
|
||||
toggleSummaryOpen,
|
||||
totalPrice,
|
||||
join,
|
||||
membershipNo,
|
||||
} = useDetailsStore(storeSelector)
|
||||
|
||||
const diff = dt(toDate).diff(fromDate, "days")
|
||||
@@ -60,10 +64,8 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
{ totalNights: diff }
|
||||
)
|
||||
|
||||
let color: "uiTextHighContrast" | "red" = "uiTextHighContrast"
|
||||
if (showMemberPrice) {
|
||||
color = "red"
|
||||
}
|
||||
const color = useRef<"uiTextHighContrast" | "red">("uiTextHighContrast")
|
||||
const [price, setPrice] = useState(room.prices.public)
|
||||
|
||||
const additionalPackageCost = room.packages?.reduce(
|
||||
(acc, curr) => {
|
||||
@@ -74,11 +76,23 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
{ local: 0, euro: 0 }
|
||||
) || { local: 0, euro: 0 }
|
||||
|
||||
const roomsPriceLocal = room.localPrice.price + additionalPackageCost.local
|
||||
const roomsPriceEuro = room.euroPrice
|
||||
? room.euroPrice.price + additionalPackageCost.euro
|
||||
const roomsPriceLocal = price.local.amount + additionalPackageCost.local
|
||||
const roomsPriceEuro = price.euro
|
||||
? price.euro.amount + additionalPackageCost.euro
|
||||
: undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (showMemberPrice || join || membershipNo) {
|
||||
color.current = "red"
|
||||
if (room.prices.member) {
|
||||
setPrice(room.prices.member)
|
||||
}
|
||||
} else {
|
||||
color.current = "uiTextHighContrast"
|
||||
setPrice(room.prices.public)
|
||||
}
|
||||
}, [showMemberPrice, join, membershipNo, room.prices])
|
||||
|
||||
useEffect(() => {
|
||||
setChosenBed(bedType)
|
||||
|
||||
@@ -87,30 +101,30 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
if (breakfast === false) {
|
||||
setTotalPrice({
|
||||
local: {
|
||||
price: roomsPriceLocal,
|
||||
currency: room.localPrice.currency,
|
||||
amount: roomsPriceLocal,
|
||||
currency: price.local.currency,
|
||||
},
|
||||
euro:
|
||||
room.euroPrice && roomsPriceEuro
|
||||
price.euro && roomsPriceEuro
|
||||
? {
|
||||
price: roomsPriceEuro,
|
||||
currency: room.euroPrice.currency,
|
||||
amount: roomsPriceEuro,
|
||||
currency: price.euro.currency,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
} else {
|
||||
setTotalPrice({
|
||||
local: {
|
||||
price: roomsPriceLocal + parseInt(breakfast.localPrice.totalPrice),
|
||||
currency: room.localPrice.currency,
|
||||
amount: roomsPriceLocal + parseInt(breakfast.localPrice.totalPrice),
|
||||
currency: price.local.currency,
|
||||
},
|
||||
euro:
|
||||
room.euroPrice && roomsPriceEuro
|
||||
price.euro && roomsPriceEuro
|
||||
? {
|
||||
price:
|
||||
amount:
|
||||
roomsPriceEuro +
|
||||
parseInt(breakfast.requestedPrice.totalPrice),
|
||||
currency: room.euroPrice.currency,
|
||||
currency: price.euro.currency,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
@@ -120,8 +134,8 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
bedType,
|
||||
breakfast,
|
||||
roomsPriceLocal,
|
||||
room.localPrice.currency,
|
||||
room.euroPrice,
|
||||
price.local.currency,
|
||||
price.euro,
|
||||
roomsPriceEuro,
|
||||
setTotalPrice,
|
||||
])
|
||||
@@ -151,12 +165,12 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
<div>
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">{room.roomType}</Body>
|
||||
<Caption color={color}>
|
||||
<Caption color={color.current}>
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(room.localPrice.price),
|
||||
currency: room.localPrice.currency,
|
||||
amount: intl.formatNumber(price.local.amount),
|
||||
currency: price.local.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
@@ -229,7 +243,7 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{ amount: "0", currency: room.localPrice.currency }
|
||||
{ amount: "0", currency: price.local.currency }
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
@@ -243,7 +257,7 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{ amount: "0", currency: room.localPrice.currency }
|
||||
{ amount: "0", currency: price.local.currency }
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
@@ -279,22 +293,24 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(totalPrice.local.price),
|
||||
currency: totalPrice.local.currency,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
{totalPrice.euro && (
|
||||
{totalPrice.local.amount > 0 && (
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(totalPrice.local.amount),
|
||||
currency: totalPrice.local.currency,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
)}
|
||||
{totalPrice.euro && totalPrice.euro.amount > 0 && (
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Approx." })}{" "}
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(totalPrice.euro.price),
|
||||
amount: intl.formatNumber(totalPrice.euro.amount),
|
||||
currency: totalPrice.euro.currency,
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { ErrorCircleIcon } from "@/components/Icons"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import styles from "../hotelPriceList.module.css"
|
||||
|
||||
export default function NoPriceAvailableCard() {
|
||||
const intl = useIntl()
|
||||
return (
|
||||
<div className={styles.priceCard}>
|
||||
<div className={styles.noRooms}>
|
||||
<div>
|
||||
<ErrorCircleIcon color="red" />
|
||||
</div>
|
||||
<Body>
|
||||
{intl.formatMessage({
|
||||
id: "There are no rooms available that match your request.",
|
||||
})}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -40,6 +40,6 @@
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.prices {
|
||||
max-width: 260px;
|
||||
width: 260px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
import HotelPriceCard from "./HotelPriceCard"
|
||||
import NoPriceAvailableCard from "./NoPriceAvailableCard"
|
||||
|
||||
import styles from "./hotelPriceList.module.css"
|
||||
|
||||
@@ -48,18 +49,7 @@ export default function HotelPriceList({
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.priceCard}>
|
||||
<div className={styles.noRooms}>
|
||||
<div>
|
||||
<ErrorCircleIcon color="red" />
|
||||
</div>
|
||||
<Body>
|
||||
{intl.formatMessage({
|
||||
id: "There are no rooms available that match your request.",
|
||||
})}
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
<NoPriceAvailableCard />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -122,11 +122,6 @@
|
||||
margin-bottom: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.pageListing .prices {
|
||||
align-items: center;
|
||||
width: 260px;
|
||||
}
|
||||
|
||||
.pageListing .button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ import { useParams } from "next/dist/client/components/navigation"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { selectHotelMap, selectRate } from "@/constants/routes/hotelReservation"
|
||||
import { selectHotelMap } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
min-width: 201px;
|
||||
min-width: 220px;
|
||||
padding: var(--Spacing-x-one-and-half);
|
||||
gap: var(--Spacing-x1);
|
||||
display: flex;
|
||||
@@ -67,12 +67,32 @@
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.prices {
|
||||
.priceCard {
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
padding: var(--Spacing-x-half) var(--Spacing-x1);
|
||||
background: var(--Base-Surface-Secondary-light-Normal);
|
||||
}
|
||||
|
||||
.prices {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.imagePlaceholder {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
background-image: linear-gradient(45deg, #000000 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #000000 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
||||
background-size: 120px 120px;
|
||||
background-position:
|
||||
0 0,
|
||||
0 60px,
|
||||
60px -60px,
|
||||
-60px 0;
|
||||
}
|
||||
|
||||
.perNight {
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import { useParams } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { selectRate } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import { CloseLargeIcon } from "@/components/Icons"
|
||||
import TripAdvisorIcon from "@/components/Icons/TripAdvisor"
|
||||
import { CloseLargeIcon, TripAdvisorIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Chip from "@/components/TempDesignSystem/Chip"
|
||||
@@ -17,6 +17,8 @@ import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import NoPriceAvailableCard from "../HotelCard/HotelPriceList/NoPriceAvailableCard"
|
||||
|
||||
import styles from "./hotelCardDialog.module.css"
|
||||
|
||||
import type { HotelCardDialogProps } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
@@ -29,6 +31,7 @@ export default function HotelCardDialog({
|
||||
const params = useParams()
|
||||
const lang = params.lang as Lang
|
||||
const intl = useIntl()
|
||||
const [imageError, setImageError] = useState(false)
|
||||
|
||||
if (!data) {
|
||||
return null
|
||||
@@ -57,7 +60,16 @@ export default function HotelCardDialog({
|
||||
height={16}
|
||||
/>
|
||||
<div className={styles.imageContainer}>
|
||||
<Image src={firstImage} alt={altText} fill />
|
||||
{!firstImage || imageError ? (
|
||||
<div className={styles.imagePlaceholder} />
|
||||
) : (
|
||||
<Image
|
||||
src={firstImage}
|
||||
alt={altText}
|
||||
fill
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.tripAdvisor}>
|
||||
<Chip intent="secondary" className={styles.tripAdvisor}>
|
||||
<TripAdvisorIcon color="burgundy" />
|
||||
@@ -85,32 +97,50 @@ export default function HotelCardDialog({
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.prices}>
|
||||
<Caption type="bold">{intl.formatMessage({ id: "From" })}</Caption>
|
||||
<Subtitle type="two">
|
||||
{publicPrice} {currency}
|
||||
<Body asChild>
|
||||
<span>/{intl.formatMessage({ id: "night" })}</span>
|
||||
</Body>
|
||||
</Subtitle>
|
||||
{memberPrice && (
|
||||
<Subtitle type="two" color="red" className={styles.memberPrice}>
|
||||
{memberPrice} {currency}
|
||||
<Body asChild color="red">
|
||||
<span>/{intl.formatMessage({ id: "night" })}</span>
|
||||
</Body>
|
||||
</Subtitle>
|
||||
{publicPrice || memberPrice ? (
|
||||
<>
|
||||
<div className={styles.priceCard}>
|
||||
<Caption type="bold">
|
||||
{intl.formatMessage({ id: "From" })}
|
||||
</Caption>
|
||||
<Subtitle type="two">
|
||||
{publicPrice} {currency}
|
||||
<Body asChild>
|
||||
<span>/{intl.formatMessage({ id: "night" })}</span>
|
||||
</Body>
|
||||
</Subtitle>
|
||||
{memberPrice && (
|
||||
<Subtitle
|
||||
type="two"
|
||||
color="red"
|
||||
className={styles.memberPrice}
|
||||
>
|
||||
{memberPrice} {currency}
|
||||
<Body asChild color="red">
|
||||
<span>/{intl.formatMessage({ id: "night" })}</span>
|
||||
</Body>
|
||||
</Subtitle>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
asChild
|
||||
theme="base"
|
||||
size="small"
|
||||
className={styles.button}
|
||||
>
|
||||
<Link
|
||||
href={`${selectRate(lang)}?hotel=${data.operaId}`}
|
||||
color="none"
|
||||
keepSearchParams
|
||||
>
|
||||
{intl.formatMessage({ id: "See rooms" })}
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<NoPriceAvailableCard />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button asChild theme="base" size="small" className={styles.button}>
|
||||
<Link
|
||||
href={`${selectRate(lang)}?hotel=${data.operaId}`}
|
||||
color="none"
|
||||
keepSearchParams
|
||||
>
|
||||
{intl.formatMessage({ id: "See rooms" })}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
@@ -60,7 +60,7 @@ export default function HotelCardDialogListing({
|
||||
const elements = document.querySelectorAll("[data-name]")
|
||||
setTimeout(() => {
|
||||
elements.forEach((el) => observerRef.current?.observe(el))
|
||||
}, 500)
|
||||
}, 1000)
|
||||
}
|
||||
}, [activeCard])
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] {
|
||||
hotel.hotelData.hotelContent.images,
|
||||
...(hotel.hotelData.gallery?.heroImages ?? []),
|
||||
],
|
||||
amenities: hotel.hotelData.detailedFacilities.slice(0, 3),
|
||||
amenities: hotel.hotelData.detailedFacilities
|
||||
.map((facility) => ({
|
||||
...facility,
|
||||
icon: facility.icon ?? "None",
|
||||
}))
|
||||
.slice(0, 3),
|
||||
ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null,
|
||||
operaId: hotel.hotelData.operaId,
|
||||
}))
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||
|
||||
import HotelCard from "../HotelCard"
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
type HotelData,
|
||||
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter"
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
|
||||
export default function HotelCardListing({
|
||||
hotelData,
|
||||
@@ -28,6 +31,7 @@ export default function HotelCardListing({
|
||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
||||
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
|
||||
const intl = useIntl()
|
||||
|
||||
const sortBy = useMemo(
|
||||
() => searchParams.get("sort") ?? DEFAULT_SORT,
|
||||
@@ -69,7 +73,6 @@ export default function HotelCardListing({
|
||||
|
||||
const hotels = useMemo(() => {
|
||||
if (activeFilters.length === 0) {
|
||||
setResultCount(sortedHotels.length)
|
||||
return sortedHotels
|
||||
}
|
||||
|
||||
@@ -81,9 +84,8 @@ export default function HotelCardListing({
|
||||
)
|
||||
)
|
||||
|
||||
setResultCount(filteredHotels.length)
|
||||
return filteredHotels
|
||||
}, [activeFilters, sortedHotels, setResultCount])
|
||||
}, [activeFilters, sortedHotels])
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
@@ -95,23 +97,33 @@ export default function HotelCardListing({
|
||||
return () => window.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setResultCount(hotels ? hotels.length : 0)
|
||||
}, [hotels, setResultCount])
|
||||
|
||||
function scrollToTop() {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.hotelCards}>
|
||||
{hotels?.length
|
||||
? hotels.map((hotel) => (
|
||||
<HotelCard
|
||||
key={hotel.hotelData.operaId}
|
||||
hotel={hotel}
|
||||
type={type}
|
||||
state={hotel.hotelData.name === activeCard ? "active" : "default"}
|
||||
onHotelCardHover={onHotelCardHover}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
{hotels?.length ? (
|
||||
hotels.map((hotel) => (
|
||||
<HotelCard
|
||||
key={hotel.hotelData.operaId}
|
||||
hotel={hotel}
|
||||
type={type}
|
||||
state={hotel.hotelData.name === activeCard ? "active" : "default"}
|
||||
onHotelCardHover={onHotelCardHover}
|
||||
/>
|
||||
))
|
||||
) : activeFilters ? (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({ id: "filters.nohotel.heading" })}
|
||||
text={intl.formatMessage({ id: "filters.nohotel.text" })}
|
||||
/>
|
||||
) : null}
|
||||
{showBackToTop && <BackToTopButton onClick={scrollToTop} />}
|
||||
</section>
|
||||
)
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
height: 280px;
|
||||
height: 100%;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useMediaQuery } from "usehooks-ts"
|
||||
|
||||
import { selectHotel } from "@/constants/routes/hotelReservation"
|
||||
|
||||
import { ArrowUpIcon, CloseIcon, CloseLargeIcon } from "@/components/Icons"
|
||||
import { CloseIcon, CloseLargeIcon } from "@/components/Icons"
|
||||
import InteractiveMap from "@/components/Maps/InteractiveMap"
|
||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
@@ -15,7 +15,6 @@ import useLang from "@/hooks/useLang"
|
||||
|
||||
import FilterAndSortModal from "../FilterAndSortModal"
|
||||
import HotelListing from "./HotelListing"
|
||||
import { getCentralCoordinates } from "./utils"
|
||||
|
||||
import styles from "./selectHotelMap.module.css"
|
||||
|
||||
@@ -27,6 +26,7 @@ export default function SelectHotelMap({
|
||||
mapId,
|
||||
hotels,
|
||||
filterList,
|
||||
cityCoordinates,
|
||||
}: SelectHotelMapProps) {
|
||||
const searchParams = useSearchParams()
|
||||
const router = useRouter()
|
||||
@@ -36,15 +36,13 @@ export default function SelectHotelMap({
|
||||
const [activeHotelPin, setActiveHotelPin] = useState<string | null>(null)
|
||||
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
|
||||
|
||||
const centralCoordinates = getCentralCoordinates(hotelPins)
|
||||
|
||||
const coordinates = isAboveMobile
|
||||
? centralCoordinates
|
||||
: { ...centralCoordinates, lat: centralCoordinates.lat - 0.006 }
|
||||
|
||||
const selectHotelParams = new URLSearchParams(searchParams.toString())
|
||||
const selectedHotel = selectHotelParams.get("selectedHotel")
|
||||
|
||||
const coordinates = isAboveMobile
|
||||
? cityCoordinates
|
||||
: { ...cityCoordinates, lat: cityCoordinates.lat - 0.006 }
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedHotel) {
|
||||
setActiveHotelPin(selectedHotel)
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { HotelPin } from "@/types/components/hotelReservation/selectHotel/map"
|
||||
|
||||
export function getCentralCoordinates(hotels: HotelPin[]) {
|
||||
const centralCoordinates = hotels.reduce(
|
||||
(acc, pin) => {
|
||||
acc.lat += pin.coordinates.lat
|
||||
acc.lng += pin.coordinates.lng
|
||||
return acc
|
||||
},
|
||||
{ lat: 0, lng: 0 }
|
||||
)
|
||||
|
||||
centralCoordinates.lat /= hotels.length
|
||||
centralCoordinates.lng /= hotels.length
|
||||
|
||||
return centralCoordinates
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
.hotelAlert {
|
||||
max-width: var(--max-width-navigation);
|
||||
margin: 0 auto;
|
||||
padding-top: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { dt } from "@/lib/dt"
|
||||
import { getRoomAvailability } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { safeTry } from "@/utils/safeTry"
|
||||
|
||||
import { generateChildrenString } from "../RoomSelection/utils"
|
||||
|
||||
import styles from "./NoRoomsAlert.module.css"
|
||||
|
||||
import { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
|
||||
type Props = {
|
||||
hotelId: number
|
||||
lang: Lang
|
||||
adultCount: number
|
||||
childArray: Child[]
|
||||
fromDate: Date
|
||||
toDate: Date
|
||||
}
|
||||
|
||||
export async function NoRoomsAlert({
|
||||
hotelId,
|
||||
fromDate,
|
||||
toDate,
|
||||
childArray,
|
||||
adultCount,
|
||||
lang,
|
||||
}: Props) {
|
||||
const [availability, availabilityError] = await safeTry(
|
||||
getRoomAvailability({
|
||||
hotelId: hotelId,
|
||||
roomStayStartDate: dt(fromDate).format("YYYY-MM-DD"),
|
||||
roomStayEndDate: dt(toDate).format("YYYY-MM-DD"),
|
||||
adults: adultCount,
|
||||
children: generateChildrenString(childArray), // TODO: Handle multiple rooms,
|
||||
})
|
||||
)
|
||||
|
||||
if (!availability || availabilityError) {
|
||||
return null
|
||||
}
|
||||
|
||||
const noRoomsAvailable = availability.roomConfigurations.reduce(
|
||||
(acc, room) => {
|
||||
return acc && room.status === "NotAvailable"
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
if (!noRoomsAvailable) {
|
||||
return null
|
||||
}
|
||||
|
||||
const intl = await getIntl(lang)
|
||||
return (
|
||||
<div className={styles.hotelAlert}>
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
text={intl.formatMessage({
|
||||
id: "There are no rooms available that match your request",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client"
|
||||
import { useEffect } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
import { Suspense } from "react"
|
||||
|
||||
import useRoomAvailableStore from "@/stores/roomAvailability"
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { getHotelData } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
@@ -11,39 +10,42 @@ import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import ReadMore from "../../ReadMore"
|
||||
import TripAdvisorChip from "../../TripAdvisorChip"
|
||||
import { NoRoomsAlert } from "./NoRoomsAlert"
|
||||
|
||||
import styles from "./hotelInfoCard.module.css"
|
||||
|
||||
import type { HotelInfoCardProps } from "@/types/components/hotelReservation/selectRate/hotelInfoCardProps"
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
import { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
|
||||
type Props = {
|
||||
hotelId: number
|
||||
lang: Lang
|
||||
fromDate: Date
|
||||
toDate: Date
|
||||
adultCount: number
|
||||
childArray: Child[]
|
||||
}
|
||||
|
||||
export default async function HotelInfoCard({
|
||||
hotelId,
|
||||
lang,
|
||||
...props
|
||||
}: Props) {
|
||||
const hotelData = await getHotelData({
|
||||
hotelId: hotelId.toString(),
|
||||
language: lang,
|
||||
})
|
||||
|
||||
export default function HotelInfoCard({
|
||||
hotelData,
|
||||
noAvailability = false,
|
||||
}: HotelInfoCardProps) {
|
||||
const hotelAttributes = hotelData?.data.attributes
|
||||
const intl = useIntl()
|
||||
|
||||
const noRoomsAvailable = useRoomAvailableStore(
|
||||
(state) => state.noRoomsAvailable
|
||||
)
|
||||
const setNoRoomsAvailable = useRoomAvailableStore(
|
||||
(state) => state.setNoRoomsAvailable
|
||||
)
|
||||
const intl = await getIntl()
|
||||
|
||||
const sortedFacilities = hotelAttributes?.detailedFacilities
|
||||
.sort((a, b) => b.sortOrder - a.sortOrder)
|
||||
.slice(0, 5)
|
||||
|
||||
useEffect(() => {
|
||||
if (noAvailability) {
|
||||
setNoRoomsAvailable()
|
||||
}
|
||||
}, [noAvailability, setNoRoomsAvailable])
|
||||
|
||||
return (
|
||||
<article className={styles.container}>
|
||||
{hotelAttributes && (
|
||||
@@ -117,16 +119,10 @@ export default function HotelInfoCard({
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{noRoomsAvailable ? (
|
||||
<div className={styles.hotelAlert}>
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
text={intl.formatMessage({
|
||||
id: "There are no rooms available that match your request",
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Suspense fallback={null} key={hotelId}>
|
||||
<NoRoomsAlert hotelId={hotelId} lang={lang} {...props} />
|
||||
</Suspense>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,15 +46,10 @@ export default function FlexibilityOption({
|
||||
const { public: publicPrice, member: memberPrice } = product.productType
|
||||
|
||||
function onChange() {
|
||||
const rate = {
|
||||
roomTypeCode,
|
||||
roomType,
|
||||
priceName: name,
|
||||
public: publicPrice,
|
||||
member: memberPrice,
|
||||
features: petRoomPackage ? features : [],
|
||||
}
|
||||
handleSelectRate(rate)
|
||||
handleSelectRate({
|
||||
publicRateCode: publicPrice.rateCode,
|
||||
roomTypeCode: roomTypeCode,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
.card {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #fff;
|
||||
border-radius: var(--Corner-radius-Large);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
position: relative;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
min-height: 200px;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
aspect-ratio: 16/9;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.priceVariants {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: var(--Spacing-x2);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
|
||||
import styles from "./RoomCardSkeleton.module.css"
|
||||
|
||||
export function RoomCardSkeleton() {
|
||||
return (
|
||||
<article className={styles.card}>
|
||||
{/* image container */}
|
||||
<div className={styles.imageContainer}>
|
||||
<SkeletonShimmer width={"100%"} height="100%" />
|
||||
</div>
|
||||
|
||||
<div className={styles.priceVariants}>
|
||||
{/* price variants */}
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<SkeletonShimmer key={index} height={"100px"} />
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -176,7 +176,7 @@ export default function RoomCard({
|
||||
<Subtitle className={styles.name} type="two">
|
||||
{roomConfiguration.roomType}
|
||||
</Subtitle>
|
||||
{/* Out of scope for now
|
||||
{/* Out of scope for now
|
||||
<Body>{descriptions?.short}</Body>
|
||||
*/}
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,7 @@ export default function RoomSelection({
|
||||
user,
|
||||
availablePackages,
|
||||
selectedPackages,
|
||||
setRateSummary,
|
||||
setRateCode,
|
||||
rateSummary,
|
||||
}: RoomSelectionProps) {
|
||||
const router = useRouter()
|
||||
@@ -70,7 +70,7 @@ export default function RoomSelection({
|
||||
rateDefinitions={rateDefinitions}
|
||||
roomConfiguration={roomConfiguration}
|
||||
roomCategories={roomCategories}
|
||||
handleSelectRate={setRateSummary}
|
||||
handleSelectRate={setRateCode}
|
||||
selectedPackages={selectedPackages}
|
||||
packages={availablePackages}
|
||||
/>
|
||||
|
||||
@@ -50,6 +50,54 @@ export function getQueryParamsForEnterDetails(
|
||||
roomTypeCode: room.roomtype,
|
||||
rateCode: room.ratecode,
|
||||
packages: room.packages?.split(",") as RoomPackageCodeEnum[],
|
||||
counterRateCode: room.counterratecode,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function createQueryParamsForEnterDetails(
|
||||
bookingData: BookingData,
|
||||
intitalSearchParams: URLSearchParams
|
||||
) {
|
||||
const { hotel, fromDate, toDate, rooms } = bookingData
|
||||
|
||||
const bookingSearchParams = new URLSearchParams({ hotel, fromDate, toDate })
|
||||
const searchParams = new URLSearchParams([
|
||||
...intitalSearchParams,
|
||||
...bookingSearchParams,
|
||||
])
|
||||
|
||||
rooms.forEach((item, index) => {
|
||||
if (item?.adults) {
|
||||
searchParams.set(`room[${index}].adults`, item.adults.toString())
|
||||
}
|
||||
if (item?.children) {
|
||||
item.children.forEach((child, childIndex) => {
|
||||
searchParams.set(
|
||||
`room[${index}].child[${childIndex}].age`,
|
||||
child.age.toString()
|
||||
)
|
||||
searchParams.set(
|
||||
`room[${index}].child[${childIndex}].bed`,
|
||||
child.bed.toString()
|
||||
)
|
||||
})
|
||||
}
|
||||
if (item?.roomTypeCode) {
|
||||
searchParams.set(`room[${index}].roomtype`, item.roomTypeCode)
|
||||
}
|
||||
if (item?.rateCode) {
|
||||
searchParams.set(`room[${index}].ratecode`, item.rateCode)
|
||||
}
|
||||
|
||||
if (item?.counterRateCode) {
|
||||
searchParams.set(`room[${index}].counterratecode`, item.counterRateCode)
|
||||
}
|
||||
|
||||
if (item.packages && item.packages.length > 0) {
|
||||
searchParams.set(`room[${index}].packages`, item.packages.join(","))
|
||||
}
|
||||
})
|
||||
|
||||
return searchParams
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { dt } from "@/lib/dt"
|
||||
import {
|
||||
getHotelData,
|
||||
getPackages,
|
||||
getProfileSafely,
|
||||
getRoomAvailability,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import { safeTry } from "@/utils/safeTry"
|
||||
|
||||
import { generateChildrenString } from "../RoomSelection/utils"
|
||||
import Rooms from "."
|
||||
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
|
||||
export type Props = {
|
||||
hotelId: number
|
||||
fromDate: Date
|
||||
toDate: Date
|
||||
adultCount: number
|
||||
childArray: Child[]
|
||||
lang: Lang
|
||||
}
|
||||
|
||||
export async function RoomsContainer({
|
||||
hotelId,
|
||||
fromDate,
|
||||
toDate,
|
||||
adultCount,
|
||||
childArray,
|
||||
lang,
|
||||
}: Props) {
|
||||
const user = await getProfileSafely()
|
||||
|
||||
const fromDateString = dt(fromDate).format("YYYY-MM-DD")
|
||||
const toDateString = dt(toDate).format("YYYY-MM-DD")
|
||||
|
||||
const hotelDataPromise = safeTry(
|
||||
getHotelData({ hotelId: hotelId.toString(), language: lang })
|
||||
)
|
||||
|
||||
const packagesPromise = safeTry(
|
||||
getPackages({
|
||||
hotelId: hotelId.toString(),
|
||||
startDate: fromDateString,
|
||||
endDate: toDateString,
|
||||
adults: adultCount,
|
||||
children: childArray.length > 0 ? childArray.length : undefined,
|
||||
packageCodes: [
|
||||
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||
RoomPackageCodeEnum.PET_ROOM,
|
||||
RoomPackageCodeEnum.ALLERGY_ROOM,
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
const roomsAvailabilityPromise = safeTry(
|
||||
getRoomAvailability({
|
||||
hotelId: hotelId,
|
||||
roomStayStartDate: fromDateString,
|
||||
roomStayEndDate: toDateString,
|
||||
adults: adultCount,
|
||||
children:
|
||||
childArray.length > 0 ? generateChildrenString(childArray) : undefined,
|
||||
})
|
||||
)
|
||||
|
||||
const [hotelData, hotelDataError] = await hotelDataPromise
|
||||
const [packages, packagesError] = await packagesPromise
|
||||
const [roomsAvailability, roomsAvailabilityError] =
|
||||
await roomsAvailabilityPromise
|
||||
|
||||
if (packagesError) {
|
||||
// TODO: Log packages error
|
||||
console.error("[RoomsContainer] unable to fetch packages")
|
||||
}
|
||||
|
||||
if (roomsAvailabilityError) {
|
||||
// TODO: show proper error component
|
||||
console.error("[RoomsContainer] unable to fetch room availability")
|
||||
return null
|
||||
}
|
||||
|
||||
if (!roomsAvailability) {
|
||||
// HotelInfoCard has the logic for displaying when there are no rooms available
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Rooms
|
||||
user={user}
|
||||
availablePackages={packages ?? []}
|
||||
roomsAvailability={roomsAvailability}
|
||||
roomCategories={hotelData?.included ?? []}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
.container {
|
||||
padding: var(--Spacing-x2);
|
||||
margin: 0 auto;
|
||||
max-width: var(--max-width);
|
||||
}
|
||||
|
||||
.filterContainer {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.skeletonContainer {
|
||||
display: grid;
|
||||
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
/* used to hide overflowing rows */
|
||||
grid-template-rows: auto;
|
||||
grid-auto-rows: 0;
|
||||
overflow: hidden;
|
||||
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
margin-top: 20px;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { RoomCardSkeleton } from "../RoomSelection/RoomCard/RoomCardSkeleton"
|
||||
|
||||
import styles from "./RoomsContainerSkeleton.module.css"
|
||||
|
||||
type Props = {
|
||||
count?: number
|
||||
}
|
||||
|
||||
export async function RoomsContainerSkeleton({ count = 4 }: Props) {
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.filterContainer}></div>
|
||||
<div className={styles.skeletonContainer}>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<RoomCardSkeleton key={index} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useCallback, useState } from "react"
|
||||
|
||||
import useRoomAvailableStore from "@/stores/roomAvailability"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
|
||||
import RoomFilter from "../RoomFilter"
|
||||
import RoomSelection from "../RoomSelection"
|
||||
@@ -17,10 +15,7 @@ import {
|
||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
|
||||
import type { Rate } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||
import type {
|
||||
RoomConfiguration,
|
||||
RoomsAvailability,
|
||||
} from "@/server/routers/hotels/output"
|
||||
import type { RoomConfiguration } from "@/server/routers/hotels/output"
|
||||
|
||||
export default function Rooms({
|
||||
roomsAvailability,
|
||||
@@ -30,24 +25,12 @@ export default function Rooms({
|
||||
}: SelectRateProps) {
|
||||
const visibleRooms: RoomConfiguration[] =
|
||||
filterDuplicateRoomTypesByLowestPrice(roomsAvailability.roomConfigurations)
|
||||
const [rateSummary, setRateSummary] = useState<Rate | null>(null)
|
||||
const [rooms, setRooms] = useState<RoomsAvailability>({
|
||||
...roomsAvailability,
|
||||
roomConfigurations: visibleRooms,
|
||||
})
|
||||
const [selectedRate, setSelectedRate] = useState<
|
||||
{ publicRateCode: string; roomTypeCode: string } | undefined
|
||||
>(undefined)
|
||||
const [selectedPackages, setSelectedPackages] = useState<RoomPackageCodes[]>(
|
||||
[]
|
||||
)
|
||||
const noRoomsAvailable = useRoomAvailableStore(
|
||||
(state) => state.noRoomsAvailable
|
||||
)
|
||||
const setNoRoomsAvailable = useRoomAvailableStore(
|
||||
(state) => state.setNoRoomsAvailable
|
||||
)
|
||||
const setRoomsAvailable = useRoomAvailableStore(
|
||||
(state) => state.setRoomsAvailable
|
||||
)
|
||||
|
||||
const defaultPackages: DefaultFilterOptions[] = [
|
||||
{
|
||||
code: RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||
@@ -79,75 +62,78 @@ export default function Rooms({
|
||||
) as RoomPackageCodeEnum[]
|
||||
|
||||
setSelectedPackages(filteredPackages)
|
||||
|
||||
if (filteredPackages.length === 0) {
|
||||
setRooms({
|
||||
...roomsAvailability,
|
||||
roomConfigurations: visibleRooms,
|
||||
})
|
||||
|
||||
if (!!rateSummary) {
|
||||
setRateSummary({
|
||||
...rateSummary,
|
||||
features: [],
|
||||
})
|
||||
}
|
||||
|
||||
if (noRoomsAvailable) {
|
||||
setRoomsAvailable()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const filteredRooms = visibleRooms.filter((room) =>
|
||||
filteredPackages.every((filteredPackage) =>
|
||||
room.features.some((feature) => feature.code === filteredPackage)
|
||||
)
|
||||
)
|
||||
|
||||
setRooms({
|
||||
...roomsAvailability,
|
||||
roomConfigurations: [...filteredRooms],
|
||||
})
|
||||
|
||||
if (filteredRooms.length == 0) {
|
||||
setNoRoomsAvailable()
|
||||
} else if (noRoomsAvailable) {
|
||||
setRoomsAvailable()
|
||||
}
|
||||
|
||||
const petRoomPackage =
|
||||
(filteredPackages.includes(RoomPackageCodeEnum.PET_ROOM) &&
|
||||
availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)) ||
|
||||
undefined
|
||||
|
||||
const features = filteredRooms.find((room) =>
|
||||
room.features.some(
|
||||
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)
|
||||
)?.features
|
||||
|
||||
if (!!rateSummary) {
|
||||
setRateSummary({
|
||||
...rateSummary,
|
||||
features: petRoomPackage && features ? features : [],
|
||||
})
|
||||
}
|
||||
},
|
||||
[
|
||||
roomsAvailability,
|
||||
visibleRooms,
|
||||
rateSummary,
|
||||
availablePackages,
|
||||
noRoomsAvailable,
|
||||
setNoRoomsAvailable,
|
||||
setRoomsAvailable,
|
||||
]
|
||||
[]
|
||||
)
|
||||
|
||||
const filteredRooms = useMemo(() => {
|
||||
return visibleRooms.filter((room) =>
|
||||
selectedPackages.every((filteredPackage) =>
|
||||
room.features.some((feature) => feature.code === filteredPackage)
|
||||
)
|
||||
)
|
||||
}, [visibleRooms, selectedPackages])
|
||||
|
||||
const rooms = useMemo(() => {
|
||||
if (selectedPackages.length === 0) {
|
||||
return {
|
||||
...roomsAvailability,
|
||||
roomConfigurations: visibleRooms,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...roomsAvailability,
|
||||
roomConfigurations: [...filteredRooms],
|
||||
}
|
||||
}, [roomsAvailability, visibleRooms, selectedPackages, filteredRooms])
|
||||
|
||||
const rateSummary: Rate | null = useMemo(() => {
|
||||
const room = filteredRooms.find(
|
||||
(room) => room.roomTypeCode === selectedRate?.roomTypeCode
|
||||
)
|
||||
|
||||
if (!room) return null
|
||||
|
||||
const product = room.products.find(
|
||||
(product) =>
|
||||
product.productType.public.rateCode === selectedRate?.publicRateCode
|
||||
)
|
||||
|
||||
if (!product) return null
|
||||
|
||||
const petRoomPackage =
|
||||
(selectedPackages.includes(RoomPackageCodeEnum.PET_ROOM) &&
|
||||
availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)) ||
|
||||
undefined
|
||||
|
||||
const features = filteredRooms.find((room) =>
|
||||
room.features.some(
|
||||
(feature) => feature.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)
|
||||
)?.features
|
||||
|
||||
const rateSummary: Rate = {
|
||||
features: petRoomPackage && features ? features : [],
|
||||
priceName: room.roomType,
|
||||
public: product.productType.public,
|
||||
member: product.productType.member,
|
||||
roomType: room.roomType,
|
||||
roomTypeCode: room.roomTypeCode,
|
||||
}
|
||||
|
||||
return rateSummary
|
||||
}, [filteredRooms, availablePackages, selectedPackages, selectedRate])
|
||||
|
||||
useEffect(() => {
|
||||
if (rateSummary) return
|
||||
if (!selectedRate) return
|
||||
|
||||
setSelectedRate(undefined)
|
||||
}, [rateSummary, selectedRate])
|
||||
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<RoomFilter
|
||||
@@ -161,7 +147,7 @@ export default function Rooms({
|
||||
user={user}
|
||||
availablePackages={availablePackages}
|
||||
selectedPackages={selectedPackages}
|
||||
setRateSummary={setRateSummary}
|
||||
setRateCode={setSelectedRate}
|
||||
rateSummary={rateSummary}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import TripAdvisorIcon from "@/components/Icons/TripAdvisor"
|
||||
import { TripAdvisorIcon } from "@/components/Icons"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import styles from "./tripAdvisorChip.module.css"
|
||||
|
||||
Reference in New Issue
Block a user