Merge branch 'master' into feat/sw-929-release-preps

This commit is contained in:
Linus Flood
2024-11-27 19:13:21 +01:00
310 changed files with 7543 additions and 4417 deletions

View File

@@ -0,0 +1,5 @@
.hotelAlert {
max-width: var(--max-width-navigation);
margin: 0 auto;
padding-top: var(--Spacing-x-one-and-half);
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

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

View File

@@ -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 {

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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>
)
}

View File

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

View File

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

View File

@@ -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}
/>
)}

View File

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

View 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 ?? []}
/>
)
}

View File

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

View File

@@ -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>
)
}

View File

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