From 378225f9952db7de4f287aef672c3105f876c4f9 Mon Sep 17 00:00:00 2001 From: Pontus Dreij Date: Wed, 6 Nov 2024 14:27:55 +0100 Subject: [PATCH] feat(SW-340): Added hotel pins --- .../select-hotel/@modal/(.)map/page.tsx | 8 +-- .../(standard)/select-hotel/utils.ts | 56 +++++++++------ .../SelectHotel/SelectHotelMap/index.tsx | 10 +-- components/MapModal/index.tsx | 5 +- .../hotelListingMapContent.module.css | 51 +++++++++++++ .../HotelListingMapContent/index.tsx | 47 ++++++++++++ .../hotelMapContent.module.css | 42 +++++++++++ .../InteractiveMap/HotelMapContent/index.tsx | 68 ++++++++++++++++++ components/Maps/InteractiveMap/index.tsx | 71 ++++++------------- .../InteractiveMap/interactiveMap.module.css | 43 ----------- components/Maps/Markers/Hotel.tsx | 21 ++++++ .../hotelPage/map/interactiveMap.ts | 11 ++- .../hotelReservation/selectHotel/map.ts | 11 ++- types/enums/hotelListing.ts | 6 ++ 14 files changed, 321 insertions(+), 129 deletions(-) create mode 100644 components/Maps/InteractiveMap/HotelListingMapContent/hotelListingMapContent.module.css create mode 100644 components/Maps/InteractiveMap/HotelListingMapContent/index.tsx create mode 100644 components/Maps/InteractiveMap/HotelMapContent/hotelMapContent.module.css create mode 100644 components/Maps/InteractiveMap/HotelMapContent/index.tsx create mode 100644 components/Maps/Markers/Hotel.tsx create mode 100644 types/enums/hotelListing.ts diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx index 888e9fe18..5a23b06f7 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/@modal/(.)map/page.tsx @@ -14,7 +14,7 @@ import { setLang } from "@/i18n/serverContext" import { fetchAvailableHotels, getCentralCoordinates, - getPointOfInterests, + getHotelPins, } from "../../utils" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" @@ -59,16 +59,16 @@ export default async function SelectHotelMapPage({ children, }) - const pointOfInterests = getPointOfInterests(hotels) + const hotelPins = getHotelPins(hotels) - const centralCoordinates = getCentralCoordinates(pointOfInterests) + const centralCoordinates = getCentralCoordinates(hotelPins) return ( 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 527db7fc8..baddc740d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -3,19 +3,14 @@ import { serverClient } from "@/lib/trpc/server" import { getLang } from "@/i18n/serverContext" -import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums" import type { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput" import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import type { CategorizedFilters, Filter, } from "@/types/components/hotelReservation/selectHotel/hotelFilters" -import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" -import { - type PointOfInterest, - PointOfInterestCategoryNameEnum, - PointOfInterestGroupEnum, -} from "@/types/hotel" +import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" +import { HotelListingEnum } from "@/types/enums/hotelListing" const hotelSurroundingsFilterNames = [ "Hotel surroundings", @@ -34,7 +29,24 @@ export async function fetchAvailableHotels( if (!availableHotels) throw new Error() const language = getLang() - const hotels = availableHotels.availability.map(async (hotel) => { + const hotelMap = new Map() + + availableHotels.availability.forEach((hotel) => { + const existingHotel = hotelMap.get(hotel.hotelId) + if (existingHotel) { + if (hotel.ratePlanSet === HotelListingEnum.RatePlanSet.PUBLIC) { + existingHotel.bestPricePerNight.regularAmount = + hotel.bestPricePerNight?.regularAmount + } else if (hotel.ratePlanSet === HotelListingEnum.RatePlanSet.MEMBER) { + existingHotel.bestPricePerNight.memberAmount = + hotel.bestPricePerNight?.memberAmount + } + } else { + hotelMap.set(hotel.hotelId, { ...hotel }) + } + }) + + const hotels = Array.from(hotelMap.values()).map(async (hotel) => { const hotelData = await getHotelData({ hotelId: hotel.hotelId.toString(), language, @@ -76,32 +88,36 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters { ) } -export function getPointOfInterests(hotels: HotelData[]): PointOfInterest[] { - // TODO: this is just a quick transformation to get something there. May need rework +export function getHotelPins(hotels: HotelData[]): HotelPin[] { return hotels.map((hotel) => ({ coordinates: { lat: hotel.hotelData.location.latitude, lng: hotel.hotelData.location.longitude, }, name: hotel.hotelData.name, - distance: hotel.hotelData.location.distanceToCentre, - categoryName: PointOfInterestCategoryNameEnum.HOTEL, - group: PointOfInterestGroupEnum.LOCATION, + 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", })) } -export function getCentralCoordinates(pointOfInterests: PointOfInterest[]) { - const centralCoordinates = pointOfInterests.reduce( - (acc, poi) => { - acc.lat += poi.coordinates.lat - acc.lng += poi.coordinates.lng +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 /= pointOfInterests.length - centralCoordinates.lng /= pointOfInterests.length + centralCoordinates.lat /= hotels.length + centralCoordinates.lng /= hotels.length return centralCoordinates } diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx index 0dc82d7ab..037967d55 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx @@ -20,7 +20,7 @@ import { SelectHotelMapProps } from "@/types/components/hotelReservation/selectH export default function SelectHotelMap({ apiKey, coordinates, - pointsOfInterest, + hotelPins, mapId, isModal, }: SelectHotelMapProps) { @@ -28,7 +28,7 @@ export default function SelectHotelMap({ const router = useRouter() const lang = useLang() const intl = useIntl() - const [activePoi, setActivePoi] = useState(null) + const [activeHotelPin, setActiveHotelPin] = useState(null) function handleModalDismiss() { router.back() @@ -70,9 +70,9 @@ export default function SelectHotelMap({ diff --git a/components/MapModal/index.tsx b/components/MapModal/index.tsx index 282e08151..2c06a08cc 100644 --- a/components/MapModal/index.tsx +++ b/components/MapModal/index.tsx @@ -12,14 +12,13 @@ export function MapModal({ children }: { children: React.ReactNode }) { const router = useRouter() const [mapHeight, setMapHeight] = useState("0px") const [mapTop, setMapTop] = useState("0px") - const [isOpen, setOpen] = useState(true) - + const [isOpen, setIsOpen] = useState(true) const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0) const rootDiv = useRef(null) const handleOnOpenChange = (open: boolean) => { - setOpen(open) + setIsOpen(open) if (!open) { router.back() } diff --git a/components/Maps/InteractiveMap/HotelListingMapContent/hotelListingMapContent.module.css b/components/Maps/InteractiveMap/HotelListingMapContent/hotelListingMapContent.module.css new file mode 100644 index 000000000..2729d5e79 --- /dev/null +++ b/components/Maps/InteractiveMap/HotelListingMapContent/hotelListingMapContent.module.css @@ -0,0 +1,51 @@ +.advancedMarker { + height: 32px; + min-width: 109px !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ +} + +.advancedMarker.active { + height: var(--Spacing-x5); + width: var( + --Spacing-x5 + ) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ +} + +.pin { + position: absolute; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + padding: var(--Spacing-x-half) var(--Spacing-x1) var(--Spacing-x-half) + var(--Spacing-x-half); + border-radius: var(--Corner-radius-Rounded); + background-color: var(--Base-Surface-Primary-light-Normal); + box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1); + gap: var(--Spacing-x1); +} + +.pin.active { + padding-right: var(--Spacing-x-one-and-half); +} + +.pinLabel { + display: none; +} + +.pin.active .pinLabel { + display: flex; + align-items: center; + gap: var(--Spacing-x2); + text-wrap: nowrap; +} + +.pinIcon { + width: 24px; + height: 24px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: var(--Primary-Dark-Surface-Normal); +} diff --git a/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx b/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx new file mode 100644 index 000000000..82ba1bc4e --- /dev/null +++ b/components/Maps/InteractiveMap/HotelListingMapContent/index.tsx @@ -0,0 +1,47 @@ +import { + AdvancedMarker, + AdvancedMarkerAnchorPoint, +} from "@vis.gl/react-google-maps" + +import Body from "@/components/TempDesignSystem/Text/Body" + +import HotelMarker from "../../Markers/Hotel" + +import styles from "./hotelListingMapContent.module.css" + +import { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" + +export default function HotelListingMapContent({ + activeHotelPin, + hotelPins, +}: { + activeHotelPin?: HotelPin["name"] | null + hotelPins: HotelPin[] +}) { + return ( +
+ {hotelPins.map((pin) => ( + + + + + + + + {pin.price} {pin.currency} + + + + + ))} +
+ ) +} diff --git a/components/Maps/InteractiveMap/HotelMapContent/hotelMapContent.module.css b/components/Maps/InteractiveMap/HotelMapContent/hotelMapContent.module.css new file mode 100644 index 000000000..d17183d45 --- /dev/null +++ b/components/Maps/InteractiveMap/HotelMapContent/hotelMapContent.module.css @@ -0,0 +1,42 @@ +.advancedMarker { + height: var(--Spacing-x4); + width: var( + --Spacing-x4 + ) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ +} + +.advancedMarker.active { + height: var(--Spacing-x5); + width: var( + --Spacing-x5 + ) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ +} + +.poi { + position: absolute; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + padding: var(--Spacing-x-half); + border-radius: var(--Corner-radius-Rounded); + background-color: var(--Base-Surface-Primary-light-Normal); + box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1); + gap: var(--Spacing-x1); +} + +.poi.active { + padding-right: var(--Spacing-x-one-and-half); +} + +.poiLabel { + display: none; +} + +.poi.active .poiLabel { + display: flex; + align-items: center; + gap: var(--Spacing-x2); + text-wrap: nowrap; +} diff --git a/components/Maps/InteractiveMap/HotelMapContent/index.tsx b/components/Maps/InteractiveMap/HotelMapContent/index.tsx new file mode 100644 index 000000000..3581913b0 --- /dev/null +++ b/components/Maps/InteractiveMap/HotelMapContent/index.tsx @@ -0,0 +1,68 @@ +import { + AdvancedMarker, + AdvancedMarkerAnchorPoint, +} from "@vis.gl/react-google-maps" + +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" + +import PoiMarker from "../../Markers/Poi" +import ScandicMarker from "../../Markers/Scandic" + +import styles from "./hotelMapContent.module.css" + +import { PointOfInterest } from "@/types/hotel" + +export default function HotelMapContent({ + coordinates, + pointsOfInterest, + onActivePoiChange, + activePoi, +}: { + coordinates: { lat: number; lng: number } + pointsOfInterest: PointOfInterest[] + onActivePoiChange?: (poiName: string | null) => void + activePoi?: string | null +}) { + function toggleActivePoi(poiName: string) { + onActivePoiChange?.(activePoi === poiName ? null : poiName) + } + return ( + <> + + + + + {pointsOfInterest.map((poi) => ( + onActivePoiChange?.(poi.name)} + onMouseLeave={() => onActivePoiChange?.(null)} + onClick={() => toggleActivePoi(poi.name)} + > + + + + + {poi.name} + + {poi.distance} km + + + + + + ))} + + ) +} diff --git a/components/Maps/InteractiveMap/index.tsx b/components/Maps/InteractiveMap/index.tsx index ebc89de0c..7fef29ae1 100644 --- a/components/Maps/InteractiveMap/index.tsx +++ b/components/Maps/InteractiveMap/index.tsx @@ -1,19 +1,12 @@ "use client" -import { - AdvancedMarker, - AdvancedMarkerAnchorPoint, - Map, - type MapProps, - useMap, -} from "@vis.gl/react-google-maps" +import { Map, type MapProps, useMap } from "@vis.gl/react-google-maps" import { useIntl } from "react-intl" import { MinusIcon, PlusIcon } from "@/components/Icons" -import PoiMarker from "@/components/Maps/Markers/Poi" -import ScandicMarker from "@/components/Maps/Markers/Scandic" import Button from "@/components/TempDesignSystem/Button" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" + +import HotelListingMapContent from "./HotelListingMapContent" +import HotelMapContent from "./HotelMapContent" import styles from "./interactiveMap.module.css" @@ -23,9 +16,12 @@ export default function InteractiveMap({ coordinates, pointsOfInterest, activePoi, + hotelPins, + activeHotelPin, mapId, - onActivePoiChange, closeButton, + onActivePoiChange, + onActiveHotelPinChange, }: InteractiveMapProps) { const intl = useIntl() const map = useMap() @@ -51,46 +47,23 @@ export default function InteractiveMap({ } } - function toggleActivePoi(poiName: string) { - onActivePoiChange(activePoi === poiName ? null : poiName) - } - return (
- - - - - {pointsOfInterest.map((poi) => ( - onActivePoiChange(poi.name)} - onClick={() => toggleActivePoi(poi.name)} - > - - - - - {poi.name} - - {poi.distance} km - - - - - - ))} + {hotelPins && ( + + )} + {pointsOfInterest && ( + + )}
{closeButton} diff --git a/components/Maps/InteractiveMap/interactiveMap.module.css b/components/Maps/InteractiveMap/interactiveMap.module.css index 471409188..ace0021a3 100644 --- a/components/Maps/InteractiveMap/interactiveMap.module.css +++ b/components/Maps/InteractiveMap/interactiveMap.module.css @@ -52,49 +52,6 @@ box-shadow: var(--button-box-shadow); } -.advancedMarker { - height: var(--Spacing-x4); - width: var( - --Spacing-x4 - ) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ -} - -.advancedMarker.active { - height: var(--Spacing-x5); - width: var( - --Spacing-x5 - ) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ -} - -.poi { - position: absolute; - top: 0; - left: 0; - display: flex; - justify-content: center; - align-items: center; - padding: var(--Spacing-x-half); - border-radius: var(--Corner-radius-Rounded); - background-color: var(--Base-Surface-Primary-light-Normal); - box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1); - gap: var(--Spacing-x1); -} - -.poi.active { - padding-right: var(--Spacing-x-one-and-half); -} - -.poiLabel { - display: none; -} - -.poi.active .poiLabel { - display: flex; - align-items: center; - gap: var(--Spacing-x2); - text-wrap: nowrap; -} - @media screen and (min-width: 768px) { .ctaButtons { top: var(--Spacing-x4); diff --git a/components/Maps/Markers/Hotel.tsx b/components/Maps/Markers/Hotel.tsx new file mode 100644 index 000000000..e18c07483 --- /dev/null +++ b/components/Maps/Markers/Hotel.tsx @@ -0,0 +1,21 @@ +export default function HotelMarker({ + className, + ...props +}: React.SVGAttributes) { + return ( + + + + ) +} diff --git a/types/components/hotelPage/map/interactiveMap.ts b/types/components/hotelPage/map/interactiveMap.ts index d04484f79..dcdb1e5f5 100644 --- a/types/components/hotelPage/map/interactiveMap.ts +++ b/types/components/hotelPage/map/interactiveMap.ts @@ -1,13 +1,18 @@ import { ReactElement } from "react" +import { HotelPin } from "../../hotelReservation/selectHotel/map" + import type { Coordinates } from "@/types/components/maps/coordinates" import type { PointOfInterest } from "@/types/hotel" export interface InteractiveMapProps { coordinates: Coordinates - pointsOfInterest: PointOfInterest[] - activePoi: PointOfInterest["name"] | null + pointsOfInterest?: PointOfInterest[] + activePoi?: PointOfInterest["name"] | null + hotelPins?: HotelPin[] + activeHotelPin?: HotelPin["name"] | null mapId: string - onActivePoiChange: (poi: PointOfInterest["name"] | null) => void closeButton: ReactElement + onActivePoiChange?: (poi: PointOfInterest["name"] | null) => void + onActiveHotelPinChange?: (hotelPin: PointOfInterest["name"] | null) => void } diff --git a/types/components/hotelReservation/selectHotel/map.ts b/types/components/hotelReservation/selectHotel/map.ts index 0dfbe98a7..302da1224 100644 --- a/types/components/hotelReservation/selectHotel/map.ts +++ b/types/components/hotelReservation/selectHotel/map.ts @@ -1,5 +1,4 @@ import { Coordinates } from "@/types/components/maps/coordinates" -import type { PointOfInterest } from "@/types/hotel" export interface HotelListingProps { // pointsOfInterest: PointOfInterest[] @@ -10,7 +9,15 @@ export interface HotelListingProps { export interface SelectHotelMapProps { apiKey: string coordinates: Coordinates - pointsOfInterest: PointOfInterest[] + hotelPins: HotelPin[] mapId: string isModal: boolean } + +export type HotelPin = { + name: string + coordinates: Coordinates + price: string + currency: string + image: string +} diff --git a/types/enums/hotelListing.ts b/types/enums/hotelListing.ts new file mode 100644 index 000000000..5c16a2c19 --- /dev/null +++ b/types/enums/hotelListing.ts @@ -0,0 +1,6 @@ +export namespace HotelListingEnum { + export const enum RatePlanSet { + PUBLIC = "PUBLIC", + MEMBER = "MEMBER", + } +}