From 27481208909617741ea366d19155a59a49ccdca5 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Thu, 7 Nov 2024 16:07:54 +0100 Subject: [PATCH] feat(SW-340): Added HotelCardDialog component --- .../(standard)/select-hotel/utils.ts | 17 ++-- .../HotelCard/hotelCard.module.css | 16 ++-- .../HotelReservation/HotelCard/index.tsx | 6 +- .../HotelReservation/HotelCard/variants.ts | 6 +- .../hotelCardDialog.module.css | 70 +++++++++++++++ .../HotelCardDialog/index.tsx | 90 +++++++++++++++++++ .../HotelCardListing/index.tsx | 7 +- .../SelectHotelMap/HotelListing/index.tsx | 6 +- .../SelectHotel/SelectHotelMap/index.tsx | 1 + .../hotelListingMapContent.module.css | 28 ++++-- .../HotelListingMapContent/index.tsx | 29 +++++- components/Maps/InteractiveMap/index.tsx | 3 + .../HotelMarker/hotelMarker.module.css | 7 ++ .../{Hotel.tsx => HotelMarker/index.tsx} | 11 ++- .../Maps/Markers/HotelMarker/variants.ts | 15 ++++ .../hotelPage/map/interactiveMap.ts | 4 +- .../selectHotel/hotelCardListingProps.ts | 7 +- .../selectHotel/hotelCardProps.ts | 4 +- .../hotelReservation/selectHotel/map.ts | 23 ++++- 19 files changed, 309 insertions(+), 41 deletions(-) create mode 100644 components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css create mode 100644 components/HotelReservation/HotelCardDialog/index.tsx create mode 100644 components/Maps/Markers/HotelMarker/hotelMarker.module.css rename components/Maps/Markers/{Hotel.tsx => HotelMarker/index.tsx} (89%) create mode 100644 components/Maps/Markers/HotelMarker/variants.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index baddc740d..ee541bb7a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -95,14 +95,15 @@ export function getHotelPins(hotels: HotelData[]): HotelPin[] { lng: hotel.hotelData.location.longitude, }, name: hotel.hotelData.name, - price: hotel.price - ? `${Math.min( - parseFloat(hotel.price.memberAmount ?? "Infinity"), - parseFloat(hotel.price.regularAmount ?? "Infinity") - )}` - : "N/A", - currency: hotel.price?.currency || "Unknown", - image: "default-image-url", + publicPrice: hotel.price?.regularAmount ?? null, + memberPrice: hotel.price?.memberAmount ?? null, + currency: hotel.price?.currency || null, + images: [ + hotel.hotelData.hotelContent.images, + ...(hotel.hotelData.gallery?.heroImages ?? []), + ], + amenities: hotel.hotelData.detailedFacilities.slice(0, 3), + ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null, })) } diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 629b52be6..74569c12f 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -67,7 +67,7 @@ } @media screen and (min-width: 1367px) { - .card.listing { + .card.pageListing { grid-template-areas: "image header" "image hotel" @@ -76,30 +76,30 @@ padding: 0; } - .listing .imageContainer { + .pageListing .imageContainer { position: relative; min-height: 200px; width: 518px; } - .listing .tripAdvisor { + .pageListing .tripAdvisor { position: absolute; display: block; left: 7px; top: 7px; } - .listing .hotelInformation { + .pageListing .hotelInformation { padding-top: var(--Spacing-x2); padding-right: var(--Spacing-x2); } - .listing .hotel { + .pageListing .hotel { gap: var(--Spacing-x2); padding-right: var(--Spacing-x2); } - .listing .prices { + .pageListing .prices { flex-direction: row; align-items: center; justify-content: space-between; @@ -107,11 +107,11 @@ padding-bottom: var(--Spacing-x2); } - .listing .detailsButton { + .pageListing .detailsButton { border-bottom: none; } - .listing .button { + .pageListing .button { width: 160px; } } diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 9ed97d961..9a08d0425 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -17,9 +17,13 @@ import { hotelCardVariants } from "./variants" import styles from "./hotelCard.module.css" +import { HotelCardListingType } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps" -export default function HotelCard({ hotel, type = "listing" }: HotelCardProps) { +export default function HotelCard({ + hotel, + type = HotelCardListingType.PageListing, +}: HotelCardProps) { const intl = useIntl() const { hotelData } = hotel diff --git a/components/HotelReservation/HotelCard/variants.ts b/components/HotelReservation/HotelCard/variants.ts index 3715e6509..e71cc44a1 100644 --- a/components/HotelReservation/HotelCard/variants.ts +++ b/components/HotelReservation/HotelCard/variants.ts @@ -5,11 +5,11 @@ import styles from "./hotelCard.module.css" export const hotelCardVariants = cva(styles.card, { variants: { type: { - listing: styles.listing, - map: styles.map, + pageListing: styles.pageListing, + mapListing: styles.mapListing, }, }, defaultVariants: { - type: "listing", + type: "pageListing", }, }) diff --git a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css new file mode 100644 index 000000000..13e295588 --- /dev/null +++ b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css @@ -0,0 +1,70 @@ +.dialog { + padding-bottom: var(--Spacing-x1); + bottom: 32px; + left: 50%; + transform: translateX(-50%); + border: none; + background: transparent; +} + +.dialogContainer { + border: 1px solid var(--Base-Border-Subtle); + border-radius: var(--Corner-radius-Medium); + width: 402px; + min-height: 227px; + background: var(--Base-Surface-Primary-light-Normal); + box-shadow: 0px 0px 8px 3px rgba(0, 0, 0, 0.1); + flex-direction: row; + display: flex; +} + +.imageContainer { + position: relative; + min-height: 227px; + width: 177px; +} + +.tripAdvisor { + display: none; +} + +.imageContainer img { + object-fit: cover; +} + +.content { + width: 100%; + padding: var(--Spacing-x-one-and-half); + gap: var(--Spacing-x1); + display: flex; + flex-direction: column; +} + +.facilities { + display: flex; + flex-wrap: wrap; + gap: var(--Spacing-x1); +} + +.facilitiesItem { + display: flex; + align-items: center; + gap: var(--Spacing-x-half); +} + +.prices { + border-radius: var(--Corner-radius-Medium); + padding: var(--Spacing-x-half) var(--Spacing-x1); + background: var(--Base-Surface-Secondary-light-Normal); + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); +} + +.perNight { + color: var(--Base-Text-Subtle-light-Normal); +} + +.button { + margin-top: auto; +} diff --git a/components/HotelReservation/HotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/index.tsx new file mode 100644 index 000000000..9f121cf95 --- /dev/null +++ b/components/HotelReservation/HotelCardDialog/index.tsx @@ -0,0 +1,90 @@ +"use client" + +import { useIntl } from "react-intl" + +import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" +import TripAdvisorIcon from "@/components/Icons/TripAdvisor" +import Image from "@/components/Image" +import Button from "@/components/TempDesignSystem/Button" +import Chip from "@/components/TempDesignSystem/Chip" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" + +import styles from "./hotelCardDialog.module.css" + +import { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" + +export default function HotelCardDialog({ + pin, + isOpen, +}: { + isOpen: boolean + pin: HotelPin +}) { + const intl = useIntl() + + const { + name, + publicPrice, + memberPrice, + currency, + amenities, + images, + ratings, + } = pin + + return ( + +
+
+ {images[0].metaData.altText} +
+ + + {ratings} + +
+
+
+ {name} +
+ {amenities.map((facility) => { + const IconComponent = mapFacilityToIcon(facility.id) + return ( +
+ {IconComponent && } + {facility.name} +
+ ) + })} +
+
+ {intl.formatMessage({ id: "From" })} + + {publicPrice} {currency} + + /{intl.formatMessage({ id: "night" })} + + + {memberPrice && ( + + {memberPrice} {currency} + + /{intl.formatMessage({ id: "night" })} + + + )} +
+ +
+
+
+ ) +} diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index b51bd07bb..9a7b4eecf 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -8,11 +8,14 @@ import HotelCard from "../HotelCard" import styles from "./hotelCardListing.module.css" -import { HotelCardListingProps } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" +import { + HotelCardListingProps, + HotelCardListingType, +} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" export default function HotelCardListing({ hotelData, - type = "listing", + type = HotelCardListingType.PageListing, }: HotelCardListingProps) { const searchParams = useSearchParams() diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx index 700ffaa71..2f5d9cb0b 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx @@ -4,12 +4,16 @@ import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import styles from "./hotelListing.module.css" +import { HotelCardListingType } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { HotelListingProps } from "@/types/components/hotelReservation/selectHotel/map" export default function HotelListing({ hotels }: HotelListingProps) { return (
- +
) } diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx index eadd5f461..82887ea02 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx @@ -72,6 +72,7 @@ export default function SelectHotelMap({ closeButton={closeButton} coordinates={coordinates} hotelPins={hotelPins} + hotels={hotels} activeHotelPin={activeHotelPin} onActiveHotelPinChange={setActiveHotelPin} mapId={mapId} diff --git a/components/Maps/InteractiveMap/HotelListingMapContent/hotelListingMapContent.module.css b/components/Maps/InteractiveMap/HotelListingMapContent/hotelListingMapContent.module.css index 2729d5e79..359edad50 100644 --- a/components/Maps/InteractiveMap/HotelListingMapContent/hotelListingMapContent.module.css +++ b/components/Maps/InteractiveMap/HotelListingMapContent/hotelListingMapContent.module.css @@ -4,10 +4,8 @@ } .advancedMarker.active { - height: var(--Spacing-x5); - width: var( - --Spacing-x5 - ) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ + height: 32px; + min-width: 109px !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ } .pin { @@ -23,10 +21,11 @@ background-color: var(--Base-Surface-Primary-light-Normal); box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1); gap: var(--Spacing-x1); + width: max-content; } .pin.active { - padding-right: var(--Spacing-x-one-and-half); + background-color: var(--Primary-Dark-Surface-Normal); } .pinLabel { @@ -49,3 +48,22 @@ justify-content: center; background: var(--Primary-Dark-Surface-Normal); } + +.pin.active .pinIcon { + background: var(--Base-Surface-Primary-light-Normal); +} + +.card { + display: none; + position: absolute; + bottom: 32px; + left: 50%; + transform: translateX(-50%); + width: 402px; + height: 181px; + background-color: var(--Base-Surface-Primary-light-Normal); +} + +.card.active { + display: block; +} diff --git a/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx b/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx index 82ba1bc4e..bec6498ca 100644 --- a/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx +++ b/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx @@ -3,21 +3,31 @@ import { AdvancedMarkerAnchorPoint, } from "@vis.gl/react-google-maps" +import HotelCardDialog from "@/components/HotelReservation/HotelCardDialog" import Body from "@/components/TempDesignSystem/Text/Body" -import HotelMarker from "../../Markers/Hotel" +import HotelMarker from "../../Markers/HotelMarker" import styles from "./hotelListingMapContent.module.css" +import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" export default function HotelListingMapContent({ activeHotelPin, hotelPins, + hotels, + onActiveHotelPinChange, }: { activeHotelPin?: HotelPin["name"] | null hotelPins: HotelPin[] + hotels?: HotelData[] + onActiveHotelPinChange?: (pinName: string | null) => void }) { + function toggleActiveHotelPin(pinName: string) { + onActiveHotelPinChange?.(activeHotelPin === pinName ? null : pinName) + } + return (
{hotelPins.map((pin) => ( @@ -27,16 +37,27 @@ export default function HotelListingMapContent({ position={pin.coordinates} anchorPoint={AdvancedMarkerAnchorPoint.CENTER} zIndex={activeHotelPin === pin.name ? 2 : 0} + onMouseEnter={() => onActiveHotelPinChange?.(pin.name)} + onMouseLeave={() => onActiveHotelPinChange?.(null)} + onClick={() => toggleActiveHotelPin(pin.name)} > + + - + - + - {pin.price} {pin.currency} + {pin.memberPrice} {pin.currency} diff --git a/components/Maps/InteractiveMap/index.tsx b/components/Maps/InteractiveMap/index.tsx index 7fef29ae1..01681c268 100644 --- a/components/Maps/InteractiveMap/index.tsx +++ b/components/Maps/InteractiveMap/index.tsx @@ -18,6 +18,7 @@ export default function InteractiveMap({ activePoi, hotelPins, activeHotelPin, + hotels, mapId, closeButton, onActivePoiChange, @@ -54,6 +55,8 @@ export default function InteractiveMap({ )} {pointsOfInterest && ( diff --git a/components/Maps/Markers/HotelMarker/hotelMarker.module.css b/components/Maps/Markers/HotelMarker/hotelMarker.module.css new file mode 100644 index 000000000..852c26f84 --- /dev/null +++ b/components/Maps/Markers/HotelMarker/hotelMarker.module.css @@ -0,0 +1,7 @@ +.white * { + fill: var(--Base-Surface-Primary-light-Normal); +} + +.burgundy * { + fill: var(--Scandic-Brand-Burgundy); +} diff --git a/components/Maps/Markers/Hotel.tsx b/components/Maps/Markers/HotelMarker/index.tsx similarity index 89% rename from components/Maps/Markers/Hotel.tsx rename to components/Maps/Markers/HotelMarker/index.tsx index e18c07483..896e2716d 100644 --- a/components/Maps/Markers/Hotel.tsx +++ b/components/Maps/Markers/HotelMarker/index.tsx @@ -1,10 +1,17 @@ +import { hotelMarkerVariants } from "./variants" + export default function HotelMarker({ className, + color, ...props -}: React.SVGAttributes) { +}: React.SVGAttributes & { + color?: "burgundy" | "white" +}) { + const classNames = hotelMarkerVariants({ color, className }) + return ( void - onActiveHotelPinChange?: (hotelPin: PointOfInterest["name"] | null) => void + onActiveHotelPinChange?: (hotelPin: HotelPin["name"] | null) => void } diff --git a/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts b/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts index e915932b0..6da39e356 100644 --- a/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts +++ b/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts @@ -2,9 +2,14 @@ import { HotelsAvailabilityPrices } from "@/server/routers/hotels/output" import { Hotel } from "@/types/hotel" +export enum HotelCardListingType { + MapListing = "mapListing", + PageListing = "pageListing", +} + export type HotelCardListingProps = { hotelData: HotelData[] - type?: "map" | "listing" + type?: HotelCardListingType } export type HotelData = { diff --git a/types/components/hotelReservation/selectHotel/hotelCardProps.ts b/types/components/hotelReservation/selectHotel/hotelCardProps.ts index 7d681b68a..655ed0274 100644 --- a/types/components/hotelReservation/selectHotel/hotelCardProps.ts +++ b/types/components/hotelReservation/selectHotel/hotelCardProps.ts @@ -1,6 +1,6 @@ -import { HotelData } from "./hotelCardListingProps" +import { HotelCardListingType, HotelData } from "./hotelCardListingProps" export type HotelCardProps = { hotel: HotelData - type?: "map" | "listing" + type?: HotelCardListingType } diff --git a/types/components/hotelReservation/selectHotel/map.ts b/types/components/hotelReservation/selectHotel/map.ts index 772b2874a..e3630295d 100644 --- a/types/components/hotelReservation/selectHotel/map.ts +++ b/types/components/hotelReservation/selectHotel/map.ts @@ -1,4 +1,12 @@ +import { z } from "zod" + +import { + imageMetaDataSchema, + imageSizesSchema, +} from "@/server/routers/hotels/schemas/image" + import { HotelData } from "./hotelCardListingProps" +import { Filter } from "./hotelFilters" import type { Coordinates } from "@/types/components/maps/coordinates" @@ -18,10 +26,19 @@ export interface SelectHotelMapProps { hotels: HotelData[] } +type ImageSizes = z.infer +type ImageMetaData = z.infer + export type HotelPin = { name: string coordinates: Coordinates - price: string - currency: string - image: string + publicPrice: string | null + memberPrice: string | null + currency: string | null + images: { + imageSizes: ImageSizes + metaData: ImageMetaData + }[] + amenities: Filter[] + ratings: number | null }