Merge branch 'master' into feat/sw-929-release-preps
This commit is contained in:
@@ -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 { getRoomsAvailability } 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(
|
||||
getRoomsAvailability({
|
||||
hotelId: hotelId,
|
||||
roomStayStartDate: dt(fromDate).format("YYYY-MM-DD"),
|
||||
roomStayEndDate: dt(toDate).format("YYYY-MM-DD"),
|
||||
adults: adultCount,
|
||||
children: childArray ? generateChildrenString(childArray) : undefined, // 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,43 @@ 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 getSingleDecimal from "@/utils/numberFormatting"
|
||||
|
||||
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 && (
|
||||
@@ -67,7 +70,7 @@ export default function HotelInfoCard({
|
||||
</Title>
|
||||
<div className={styles.hotelAddressDescription}>
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{`${hotelAttributes.address.streetAddress}, ${hotelAttributes.address.city} ∙ ${hotelAttributes.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`}
|
||||
{`${hotelAttributes.address.streetAddress}, ${hotelAttributes.address.city} ∙ ${getSingleDecimal(hotelAttributes.location.distanceToCentre / 1000)} ${intl.formatMessage({ id: "km to city center" })}`}
|
||||
</Caption>
|
||||
<Body color="uiTextHighContrast">
|
||||
{hotelAttributes.hotelContent.texts.descriptions.medium}
|
||||
@@ -117,16 +120,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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { FormProvider, useForm } from "react-hook-form"
|
||||
import { useIntl } from "react-intl"
|
||||
import { z } from "zod"
|
||||
|
||||
import { InfoCircleIcon } from "@/components/Icons"
|
||||
import CheckboxChip from "@/components/TempDesignSystem/Form/FilterChip/Checkbox"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
@@ -78,10 +77,14 @@ export default function RoomFilter({
|
||||
</div>
|
||||
<div className={styles.infoMobile}>
|
||||
<div className={styles.filterInfo}>
|
||||
<Caption type="label" color="burgundy" textTransform="uppercase">
|
||||
<Caption
|
||||
type="label"
|
||||
color="baseTextMediumContrast"
|
||||
textTransform="uppercase"
|
||||
>
|
||||
{intl.formatMessage({ id: "Filter" })}
|
||||
</Caption>
|
||||
<Caption type="label" color="burgundy">
|
||||
<Caption type="label" color="baseTextMediumContrast">
|
||||
{Object.entries(selectedFilters)
|
||||
.filter(([_, value]) => value)
|
||||
.map(([key]) => intl.formatMessage({ id: key }))
|
||||
@@ -99,11 +102,13 @@ export default function RoomFilter({
|
||||
<form onSubmit={handleSubmit(submitFilter)}>
|
||||
<div className={styles.roomsFilter}>
|
||||
{filterOptions.map((option) => {
|
||||
const { code, description } = option
|
||||
const { code, description, itemCode } = option
|
||||
const isPetRoom = code === RoomPackageCodeEnum.PET_ROOM
|
||||
const isAllergyRoom = code === RoomPackageCodeEnum.ALLERGY_ROOM
|
||||
const isDisabled =
|
||||
(isAllergyRoom && petFriendly) || (isPetRoom && allergyFriendly)
|
||||
(isAllergyRoom && petFriendly) ||
|
||||
(isPetRoom && allergyFriendly) ||
|
||||
!itemCode
|
||||
|
||||
const checkboxChip = (
|
||||
<CheckboxChip
|
||||
|
||||
@@ -16,7 +16,8 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
margin-right: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.infoDesktop {
|
||||
|
||||
@@ -46,6 +46,13 @@ input[type="radio"]:checked + .card .checkIcon {
|
||||
.header {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.priceType {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function FlexibilityOption({
|
||||
</div>
|
||||
<Label size="regular" className={styles.noPricesLabel}>
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{intl.formatMessage({ id: "No Prices available" })}
|
||||
{intl.formatMessage({ id: "No prices available" })}
|
||||
</Caption>
|
||||
</Label>
|
||||
</div>
|
||||
@@ -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 (
|
||||
@@ -94,8 +89,10 @@ export default function FlexibilityOption({
|
||||
</Caption>
|
||||
))}
|
||||
</Popover>
|
||||
<Caption color="uiTextHighContrast">{name}</Caption>
|
||||
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
|
||||
<div className={styles.priceType}>
|
||||
<Caption color="uiTextHighContrast">{name}</Caption>
|
||||
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<PriceTable
|
||||
publicPrice={publicPrice}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -77,11 +77,13 @@ export default function RoomCard({
|
||||
packages?.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) ||
|
||||
undefined
|
||||
|
||||
const selectedRoom = roomCategories.find(
|
||||
(room) => room.name === roomConfiguration.roomType
|
||||
const selectedRoom = roomCategories.find((roomCategory) =>
|
||||
roomCategory.roomTypes.some(
|
||||
(roomType) => roomType.code === roomConfiguration.roomTypeCode
|
||||
)
|
||||
)
|
||||
|
||||
const { roomSize, occupancy, images } = selectedRoom || {}
|
||||
const { name, roomSize, occupancy, images } = selectedRoom || {}
|
||||
|
||||
const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
|
||||
const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
|
||||
@@ -174,9 +176,9 @@ export default function RoomCard({
|
||||
</div>
|
||||
<div className={styles.roomDetails}>
|
||||
<Subtitle className={styles.name} type="two">
|
||||
{roomConfiguration.roomType}
|
||||
{name}
|
||||
</Subtitle>
|
||||
{/* Out of scope for now
|
||||
{/* Out of scope for now
|
||||
<Body>{descriptions?.short}</Body>
|
||||
*/}
|
||||
</div>
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--Spacing-x1);
|
||||
padding: 0 var(--Spacing-x1) 0 var(--Spacing-x-one-and-half);
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.specification .guests {
|
||||
@@ -34,6 +34,10 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.toggleSidePeek button {
|
||||
text-align: start;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: var(--Spacing-x1) var(--Spacing-x2) var(--Spacing-x2);
|
||||
display: flex;
|
||||
|
||||
@@ -14,9 +14,9 @@ export default function RoomSelection({
|
||||
roomsAvailability,
|
||||
roomCategories,
|
||||
user,
|
||||
packages,
|
||||
availablePackages,
|
||||
selectedPackages,
|
||||
setRateSummary,
|
||||
setRateCode,
|
||||
rateSummary,
|
||||
}: RoomSelectionProps) {
|
||||
const router = useRouter()
|
||||
@@ -70,9 +70,9 @@ export default function RoomSelection({
|
||||
rateDefinitions={rateDefinitions}
|
||||
roomConfiguration={roomConfiguration}
|
||||
roomCategories={roomCategories}
|
||||
handleSelectRate={setRateSummary}
|
||||
handleSelectRate={setRateCode}
|
||||
selectedPackages={selectedPackages}
|
||||
packages={packages}
|
||||
packages={availablePackages}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
@@ -81,7 +81,7 @@ export default function RoomSelection({
|
||||
<RateSummary
|
||||
rateSummary={rateSummary}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
packages={packages}
|
||||
packages={availablePackages}
|
||||
roomsAvailability={roomsAvailability}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
101
components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx
Normal file
101
components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { dt } from "@/lib/dt"
|
||||
import {
|
||||
getHotelData,
|
||||
getPackages,
|
||||
getProfileSafely,
|
||||
getRoomsAvailability,
|
||||
} 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 ? childArray.length : undefined,
|
||||
packageCodes: [
|
||||
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||
RoomPackageCodeEnum.PET_ROOM,
|
||||
RoomPackageCodeEnum.ALLERGY_ROOM,
|
||||
],
|
||||
})
|
||||
)
|
||||
|
||||
const roomsAvailabilityPromise = safeTry(
|
||||
getRoomsAvailability({
|
||||
hotelId: hotelId,
|
||||
roomStayStartDate: fromDateString,
|
||||
roomStayEndDate: toDateString,
|
||||
adults: adultCount,
|
||||
children:
|
||||
childArray && 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"
|
||||
@@ -11,41 +9,51 @@ import { filterDuplicateRoomTypesByLowestPrice } from "./utils"
|
||||
import styles from "./rooms.module.css"
|
||||
|
||||
import {
|
||||
DefaultFilterOptions,
|
||||
RoomPackageCodeEnum,
|
||||
type RoomPackageCodes,
|
||||
} 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,
|
||||
roomCategories = [],
|
||||
user,
|
||||
packages,
|
||||
availablePackages,
|
||||
}: 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,
|
||||
description: "Accessible Room",
|
||||
itemCode: availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
|
||||
)?.itemCode,
|
||||
},
|
||||
{
|
||||
code: RoomPackageCodeEnum.ALLERGY_ROOM,
|
||||
description: "Allergy Room",
|
||||
itemCode: availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM
|
||||
)?.itemCode,
|
||||
},
|
||||
{
|
||||
code: RoomPackageCodeEnum.PET_ROOM,
|
||||
description: "Pet Room",
|
||||
itemCode: availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)?.itemCode,
|
||||
},
|
||||
]
|
||||
|
||||
const handleFilter = useCallback(
|
||||
(filter: Record<RoomPackageCodeEnum, boolean | undefined>) => {
|
||||
@@ -54,97 +62,92 @@ 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)
|
||||
)
|
||||
)
|
||||
let notAvailableRooms = visibleRooms.filter((room) =>
|
||||
filteredPackages.every(
|
||||
(filteredPackage) =>
|
||||
!room.features.some((feature) => feature.code === filteredPackage)
|
||||
)
|
||||
)
|
||||
// Clone nested object to keep original object intact and not messup the room data
|
||||
notAvailableRooms = JSON.parse(JSON.stringify(notAvailableRooms))
|
||||
notAvailableRooms.forEach((room) => {
|
||||
room.status = "NotAvailable"
|
||||
})
|
||||
setRooms({
|
||||
...roomsAvailability,
|
||||
roomConfigurations: [...filteredRooms, ...notAvailableRooms],
|
||||
})
|
||||
|
||||
if (filteredRooms.length == 0) {
|
||||
setNoRoomsAvailable()
|
||||
} else if (noRoomsAvailable) {
|
||||
setRoomsAvailable()
|
||||
}
|
||||
|
||||
const petRoomPackage =
|
||||
(filteredPackages.includes(RoomPackageCodeEnum.PET_ROOM) &&
|
||||
packages.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,
|
||||
packages,
|
||||
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
|
||||
numberOfRooms={rooms.roomConfigurations.length}
|
||||
onFilter={handleFilter}
|
||||
filterOptions={packages}
|
||||
filterOptions={defaultPackages}
|
||||
/>
|
||||
<RoomSelection
|
||||
roomsAvailability={rooms}
|
||||
roomCategories={roomCategories}
|
||||
user={user}
|
||||
packages={packages}
|
||||
availablePackages={availablePackages}
|
||||
selectedPackages={selectedPackages}
|
||||
setRateSummary={setRateSummary}
|
||||
setRateCode={setSelectedRate}
|
||||
rateSummary={rateSummary}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user