diff --git a/components/HotelReservation/HotelCard/NoPriceAvailableCard/index.tsx b/components/HotelReservation/HotelCard/NoPriceAvailableCard/index.tsx index 884b0ec12..628fc4cf0 100644 --- a/components/HotelReservation/HotelCard/NoPriceAvailableCard/index.tsx +++ b/components/HotelReservation/HotelCard/NoPriceAvailableCard/index.tsx @@ -1,7 +1,7 @@ import { useIntl } from "react-intl" import { ErrorCircleIcon } from "@/components/Icons" -import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" import styles from "./noPriceAvailable.module.css" @@ -13,11 +13,11 @@ export default function NoPriceAvailableCard() {
- + {intl.formatMessage({ id: "There are no rooms available that match your request.", })} - + ) diff --git a/components/HotelReservation/HotelCardDialog/HotelCardDialogImage/hotelCardDialogImage.module.css b/components/HotelReservation/HotelCardDialog/HotelCardDialogImage/hotelCardDialogImage.module.css new file mode 100644 index 000000000..ccaa841f4 --- /dev/null +++ b/components/HotelReservation/HotelCardDialog/HotelCardDialogImage/hotelCardDialogImage.module.css @@ -0,0 +1,45 @@ +.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; +} + +.imageContainer { + position: relative; + min-width: 177px; + border-radius: var(--Corner-radius-Medium) 0 0 var(--Corner-radius-Medium); + overflow: hidden; +} + +.imageContainer.top { + width: 80px; + min-width: 80px; + height: 90px; + border-radius: var(--Corner-radius-Medium); +} + +.imageContainer img { + object-fit: cover; +} + +.imageContainer .tripAdvisor { + position: absolute; + left: 7px; + top: 7px; + border-radius: 2px; +} + +.imageContainer.top .tripAdvisor { + left: 4px; + top: 4px; +} diff --git a/components/HotelReservation/HotelCardDialog/HotelCardDialogImage/index.tsx b/components/HotelReservation/HotelCardDialog/HotelCardDialogImage/index.tsx new file mode 100644 index 000000000..7e5caa2cb --- /dev/null +++ b/components/HotelReservation/HotelCardDialog/HotelCardDialogImage/index.tsx @@ -0,0 +1,41 @@ +import { TripAdvisorIcon } from "@/components/Icons" +import Image from "@/components/Image" +import Chip from "@/components/TempDesignSystem/Chip" + +import { hotelCardDialogImageVariants } from "./variants" + +import styles from "./hotelCardDialogImage.module.css" + +import type { HotelCardDialogImageProps } from "@/types/components/hotelReservation/selectHotel/map" + +export default function HotelCardDialogImage({ + firstImage, + altText, + ratings, + imageError, + setImageError, + position, +}: HotelCardDialogImageProps) { + const classNames = hotelCardDialogImageVariants({ position }) + + return ( +
+ {!firstImage || imageError ? ( +
+ ) : ( + {altText setImageError(true)} + /> + )} +
+ + + {ratings} + +
+
+ ) +} diff --git a/components/HotelReservation/HotelCardDialog/HotelCardDialogImage/variants.ts b/components/HotelReservation/HotelCardDialog/HotelCardDialogImage/variants.ts new file mode 100644 index 000000000..8ca5bb582 --- /dev/null +++ b/components/HotelReservation/HotelCardDialog/HotelCardDialogImage/variants.ts @@ -0,0 +1,15 @@ +import { cva } from "class-variance-authority" + +import styles from "./hotelCardDialogImage.module.css" + +export const hotelCardDialogImageVariants = cva(styles.imageContainer, { + variants: { + position: { + top: styles.top, + left: styles.left, + }, + }, + defaultVariants: { + position: "top", + }, +}) diff --git a/components/HotelReservation/HotelCardDialog/ListingHotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/ListingHotelCardDialog/index.tsx new file mode 100644 index 000000000..8860bafb9 --- /dev/null +++ b/components/HotelReservation/HotelCardDialog/ListingHotelCardDialog/index.tsx @@ -0,0 +1,116 @@ +import { useIntl } from "react-intl" + +import { selectRate } from "@/constants/routes/hotelReservation" + +import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" +import Button from "@/components/TempDesignSystem/Button" +import Link from "@/components/TempDesignSystem/Link" +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/NoPriceAvailableCard" +import HotelCardDialogImage from "../HotelCardDialogImage" + +import styles from "../hotelCardDialog.module.css" + +import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" +import type { Lang } from "@/constants/languages" + +interface ListingHotelCardProps { + data: HotelPin + lang: Lang + imageError: boolean + setImageError: (error: boolean) => void +} + +export default function ListingHotelCardDialog({ + data, + lang, + imageError, + setImageError, +}: ListingHotelCardProps) { + const intl = useIntl() + const { + name, + publicPrice, + memberPrice, + currency, + amenities, + images, + ratings, + operaId, + } = data + + const firstImage = images[0]?.imageSizes?.small + const altText = images[0]?.metaData?.altText + + return ( +
+
+ +
+
+ {name} +
+
+ {amenities.map((facility) => { + const IconComponent = mapFacilityToIcon(facility.id) + return ( +
+ {IconComponent && ( + + )} + + {facility.name} + +
+ ) + })} +
+
+
+ + {publicPrice || memberPrice ? ( +
+
+ + {intl.formatMessage({ id: "Per night from" })} + +
+ {publicPrice && ( + + {publicPrice} {currency} + + )} + {publicPrice && memberPrice && /} + {memberPrice && ( + + {memberPrice} {currency} + + )} +
+
+ +
+ ) : ( + + )} +
+ ) +} diff --git a/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog/index.tsx new file mode 100644 index 000000000..3ea839626 --- /dev/null +++ b/components/HotelReservation/HotelCardDialog/StandaloneHotelCardDialog/index.tsx @@ -0,0 +1,132 @@ +import { useIntl } from "react-intl" + +import { selectRate } from "@/constants/routes/hotelReservation" + +import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" +import Button from "@/components/TempDesignSystem/Button" +import Link from "@/components/TempDesignSystem/Link" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Footnote from "@/components/TempDesignSystem/Text/Footnote" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import NoPriceAvailableCard from "../../HotelCard/NoPriceAvailableCard" +import HotelCardDialogImage from "../HotelCardDialogImage" + +import styles from "../hotelCardDialog.module.css" + +import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" +import type { Lang } from "@/constants/languages" + +interface StandaloneHotelCardProps { + data: HotelPin + lang: Lang + imageError: boolean + setImageError: (error: boolean) => void +} + +export default function StandaloneHotelCardDialog({ + data, + lang, + imageError, + setImageError, +}: StandaloneHotelCardProps) { + const intl = useIntl() + const { + name, + publicPrice, + memberPrice, + currency, + amenities, + images, + ratings, + operaId, + } = data + + const firstImage = images[0]?.imageSizes?.small + const altText = images[0]?.metaData?.altText + + return ( + <> + +
+
+
+
+ {name} +
+
+
+
+ {amenities.slice(0, 3).map((facility) => { + const IconComponent = mapFacilityToIcon(facility.id) + return ( +
+ {IconComponent && ( + + )} + + {facility.name} + +
+ ) + })} +
+
+ {publicPrice || memberPrice ? ( + <> +
+ + {intl.formatMessage({ id: "From" })} + + {publicPrice && ( + + {publicPrice} {currency} + + /{intl.formatMessage({ id: "night" })} + + + )} + {memberPrice && ( + + {memberPrice} {currency} + + /{intl.formatMessage({ id: "night" })} + + + )} +
+ + + ) : ( + + )} +
+
+ + ) +} diff --git a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css index 30cd03385..ebee53701 100644 --- a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css +++ b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css @@ -10,7 +10,7 @@ .dialogContainer { border: 1px solid var(--Base-Border-Subtle); border-radius: var(--Corner-radius-Medium); - min-width: 334px; + min-width: 402px; background: var(--Base-Surface-Primary-light-Normal); box-shadow: 0px 0px 8px 3px rgba(0, 0, 0, 0.1); flex-direction: row; @@ -18,83 +18,125 @@ position: relative; } +.dialogContainer[data-type="listing"] { + min-width: 358px; +} + +.dialogContainer[data-type="listing"] .header { + display: flex; + flex-direction: row; + gap: var(--Spacing-x-one-and-half); +} + .name { height: 48px; + max-width: 180px; + margin-bottom: var(--Spacing-x-half); display: flex; align-items: center; } .closeIcon { position: absolute; - top: 7px; - right: 7px; + top: 8px; + right: 8px; } - -.imageContainer { - position: relative; - min-width: 177px; - border-radius: var(--Corner-radius-Medium) 0 0 var(--Corner-radius-Medium); - overflow: hidden; -} - -.imageContainer img { - object-fit: cover; -} - -.tripAdvisor { - position: absolute; - display: block; - left: 7px; - top: 7px; -} - .content { width: 100%; min-width: 220px; padding: var(--Spacing-x-one-and-half); - gap: var(--Spacing-x1); display: flex; flex-direction: column; } +.dialogContainer[data-type="listing"] .content { + gap: var(--Spacing-x-one-and-half); +} + .facilities { display: flex; flex-wrap: wrap; gap: 0 var(--Spacing-x1); } +.dialogContainer[data-type="listing"] .facilities { + display: flex; + flex-wrap: nowrap; + overflow: hidden; + white-space: nowrap; + position: relative; + padding-right: 20px; + gap: 0 var(--Spacing-x1); + max-width: 242px; +} +.dialogContainer[data-type="listing"] .facilities::after { + content: ""; + position: absolute; + top: 0; + right: 0; + width: 20px; + height: 100%; + background: linear-gradient( + to left, + rgba(255, 255, 255, 1), + rgba(255, 255, 255, 0) + ); + pointer-events: none; +} + .facilitiesItem { display: flex; align-items: center; gap: var(--Spacing-x-half); } +.dialogContainer[data-type="listing"] .facilitiesItem { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: var(--Spacing-x-half); +} + .priceCard { border-radius: var(--Corner-radius-Medium); padding: var(--Spacing-x-half) var(--Spacing-x1); background: var(--Base-Surface-Secondary-light-Normal); + margin-top: var(--Spacing-x1); } .prices { display: flex; flex-direction: column; gap: var(--Spacing-x1); + justify-content: space-between; } -.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; +.bottomContainer { + display: flex; + flex-direction: row; + border-top: 1px solid var(--Primary-Light-On-Surface-Divider-subtle); + padding-top: var(--Spacing-x2); + padding-bottom: var(--Spacing-x-half); +} + +.pricesContainer { + display: flex; + flex-direction: column; + gap: var(--Spacing-x1); + justify-content: space-between; +} + +.dialogContainer[data-type="listing"] .pricesContainer { + flex: 1; + height: 44px; + gap: 0; + justify-content: flex-start; +} + +.listingPrices { + display: flex; + flex-direction: row; + gap: var(--Spacing-x1); } .perNight { @@ -106,9 +148,6 @@ } @media (min-width: 768px) { - .facilities { - display: none; - } .dialog { bottom: 32px; } diff --git a/components/HotelReservation/HotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/index.tsx index 1c35836b2..b120d478f 100644 --- a/components/HotelReservation/HotelCardDialog/index.tsx +++ b/components/HotelReservation/HotelCardDialog/index.tsx @@ -2,21 +2,11 @@ import { useParams } from "next/navigation" import { useState } from "react" -import { useIntl } from "react-intl" -import { selectRate } from "@/constants/routes/hotelReservation" +import { CloseLargeIcon } from "@/components/Icons" -import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" -import { CloseLargeIcon, TripAdvisorIcon } from "@/components/Icons" -import Image from "@/components/Image" -import Button from "@/components/TempDesignSystem/Button" -import Chip from "@/components/TempDesignSystem/Chip" -import Link from "@/components/TempDesignSystem/Link" -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/NoPriceAvailableCard" +import ListingHotelCardDialog from "./ListingHotelCardDialog" +import StandaloneHotelCardDialog from "./StandaloneHotelCardDialog" import styles from "./hotelCardDialog.module.css" @@ -26,124 +16,41 @@ import type { Lang } from "@/constants/languages" export default function HotelCardDialog({ data, isOpen, + type = "standalone", handleClose, }: HotelCardDialogProps) { const params = useParams() const lang = params.lang as Lang - const intl = useIntl() const [imageError, setImageError] = useState(false) if (!data) { return null } - const { - name, - publicPrice, - memberPrice, - currency, - amenities, - images, - ratings, - } = data - - const firstImage = images[0]?.imageSizes?.small - const altText = images[0]?.metaData?.altText - return ( -
+
-
- {!firstImage || imageError ? ( -
- ) : ( - {altText} setImageError(true)} - /> - )} -
- - - {ratings} - -
-
-
-
- {name} -
-
- {amenities.map((facility) => { - const IconComponent = mapFacilityToIcon(facility.id) - return ( -
- {IconComponent && ( - - )} - - {facility.name} - -
- ) - })} -
-
- {publicPrice || memberPrice ? ( - <> -
- - {intl.formatMessage({ id: "From" })} - - {publicPrice && ( - - {publicPrice} {currency} - - /{intl.formatMessage({ id: "night" })} - - - )} - {memberPrice && ( - - {memberPrice} {currency} - - /{intl.formatMessage({ id: "night" })} - - - )} -
- - - ) : ( - - )} -
-
+ {type === "standalone" ? ( + + ) : ( + + )}
) diff --git a/components/HotelReservation/HotelCardDialogListing/index.tsx b/components/HotelReservation/HotelCardDialogListing/index.tsx index 0a9d0e5fc..a611676f0 100644 --- a/components/HotelReservation/HotelCardDialogListing/index.tsx +++ b/components/HotelReservation/HotelCardDialogListing/index.tsx @@ -95,6 +95,7 @@ export default function HotelCardDialogListing({ data={data} isOpen={!!activeHotelCard} handleClose={handleClose} + type="listing" />
) diff --git a/components/HotelReservation/HotelCardDialogListing/utils.ts b/components/HotelReservation/HotelCardDialogListing/utils.ts index fb658adc0..141444730 100644 --- a/components/HotelReservation/HotelCardDialogListing/utils.ts +++ b/components/HotelReservation/HotelCardDialogListing/utils.ts @@ -25,7 +25,7 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] { ...facility, icon: facility.icon ?? "None", })) - .slice(0, 3), + .slice(0, 5), ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null, operaId: hotel.hotelData.operaId, facilityIds: hotel.hotelData.detailedFacilities.map( diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx index af35a90db..e58119e33 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/SelectHotelMapContainer.tsx @@ -89,8 +89,8 @@ export async function SelectHotelMapContainer({ leadTime: differenceInCalendarDays(arrivalDate, new Date()), searchType: "destination", bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday", - country: validHotels?.[0].hotelData.address.country, - region: validHotels?.[0].hotelData.address.city, + country: validHotels?.[0]?.hotelData.address.country, + region: validHotels?.[0]?.hotelData.address.city, } return ( diff --git a/types/components/hotelReservation/selectHotel/map.ts b/types/components/hotelReservation/selectHotel/map.ts index cb7cfbcc0..ae70cb3e7 100644 --- a/types/components/hotelReservation/selectHotel/map.ts +++ b/types/components/hotelReservation/selectHotel/map.ts @@ -45,11 +45,21 @@ export interface HotelListingMapContentProps { } export interface HotelCardDialogProps { + type?: "listing" | "standalone" isOpen: boolean data: HotelPin handleClose: (event: { stopPropagation: () => void }) => void } +export interface HotelCardDialogImageProps { + firstImage: string | undefined + altText: string | undefined + ratings: number + imageError: boolean + setImageError: (error: boolean) => void + position: "top" | "left" +} + export interface HotelCardDialogListingProps { hotels: HotelData[] | null }