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 0f636136f..fd9cc33c5 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 @@ -3,6 +3,7 @@ import { notFound } from "next/navigation" import { env } from "@/env/server" import { getLocations } from "@/lib/trpc/memoizedRequests" +import { getHotelPins } from "@/components/HotelReservation/HotelCardDialogListing/utils" import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap" import { generateChildrenString, @@ -11,11 +12,7 @@ import { import { MapModal } from "@/components/MapModal" import { setLang } from "@/i18n/serverContext" -import { - fetchAvailableHotels, - getCentralCoordinates, - getHotelPins, -} from "../../utils" +import { fetchAvailableHotels } from "../../utils" import type { SelectHotelSearchParams } from "@/types/components/hotelReservation/selectHotel/selectHotelSearchParams" import type { LangParams, PageArgs } from "@/types/params" @@ -61,16 +58,12 @@ export default async function SelectHotelMapPage({ const hotelPins = getHotelPins(hotels) - 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 ee541bb7a..a021d9355 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -87,38 +87,3 @@ export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters { { facilityFilters: [], surroundingsFilters: [] } ) } - -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, - 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, - })) -} - -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 /= hotels.length - centralCoordinates.lng /= hotels.length - - return centralCoordinates -} diff --git a/components/Header/MainMenu/MobileMenu/index.tsx b/components/Header/MainMenu/MobileMenu/index.tsx index ae79675dd..272c4b231 100644 --- a/components/Header/MainMenu/MobileMenu/index.tsx +++ b/components/Header/MainMenu/MobileMenu/index.tsx @@ -3,13 +3,13 @@ import { Suspense, useEffect } from "react" import { Dialog, Modal } from "react-aria-components" import { useIntl } from "react-intl" +import { useMediaQuery } from "usehooks-ts" import useDropdownStore from "@/stores/main-menu" import { GiftIcon, SearchIcon, ServiceIcon } from "@/components/Icons" import LanguageSwitcher from "@/components/LanguageSwitcher" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" -import useMediaQuery from "@/hooks/useMediaQuery" import HeaderLink from "../../HeaderLink" diff --git a/components/Header/MainMenu/MyPagesMobileMenu/index.tsx b/components/Header/MainMenu/MyPagesMobileMenu/index.tsx index 466014bd2..e0711fe5c 100644 --- a/components/Header/MainMenu/MyPagesMobileMenu/index.tsx +++ b/components/Header/MainMenu/MyPagesMobileMenu/index.tsx @@ -3,11 +3,11 @@ import { useEffect } from "react" import { Dialog, Modal } from "react-aria-components" import { useIntl } from "react-intl" +import { useMediaQuery } from "usehooks-ts" import useDropdownStore from "@/stores/main-menu" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" -import useMediaQuery from "@/hooks/useMediaQuery" import { getInitials } from "@/utils/user" import Avatar from "../Avatar" diff --git a/components/HotelReservation/HotelCard/hotelCard.module.css b/components/HotelReservation/HotelCard/hotelCard.module.css index 95ea52ad2..a5d90de17 100644 --- a/components/HotelReservation/HotelCard/hotelCard.module.css +++ b/components/HotelReservation/HotelCard/hotelCard.module.css @@ -70,6 +70,10 @@ justify-content: center; } +.address { + display: none; +} + @media screen and (min-width: 1367px) { .card.pageListing { grid-template-areas: @@ -118,4 +122,12 @@ .pageListing .button { width: 160px; } + + .address { + display: block; + } + + .addressMobile { + display: none; + } } diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 623990987..90646041f 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -1,6 +1,10 @@ "use client" +import { useParams } from "next/dist/client/components/navigation" import { useIntl } from "react-intl" +import { Lang } from "@/constants/languages" +import { selectHotelMap } from "@/constants/routes/hotelReservation" + import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { PriceTagIcon, ScandicLogoIcon } from "@/components/Icons" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" @@ -26,6 +30,8 @@ export default function HotelCard({ state = "default", onHotelCardHover, }: HotelCardProps) { + const params = useParams() + const lang = params.lang as Lang const intl = useIntl() const { hotelData } = hotel @@ -74,9 +80,19 @@ export default function HotelCard({ {hotelData.name} - + {`${hotelData.address.streetAddress}, ${hotelData.address.city}`} + + + {`${hotelData.address.streetAddress}, ${hotelData.address.city}`} + + {`${hotelData.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} diff --git a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css index dce743fa1..7a55be6ca 100644 --- a/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css +++ b/components/HotelReservation/HotelCardDialog/hotelCardDialog.module.css @@ -18,6 +18,12 @@ position: relative; } +.name { + height: 48px; + display: flex; + align-items: center; +} + .closeIcon { position: absolute; top: 7px; @@ -52,7 +58,7 @@ .facilities { display: flex; flex-wrap: wrap; - gap: var(--Spacing-x1); + gap: 0 var(--Spacing-x1); } .facilitiesItem { @@ -67,7 +73,6 @@ background: var(--Base-Surface-Secondary-light-Normal); display: flex; flex-direction: column; - gap: var(--Spacing-x-half); } .perNight { diff --git a/components/HotelReservation/HotelCardDialog/index.tsx b/components/HotelReservation/HotelCardDialog/index.tsx index adeb0e176..16d1ce860 100644 --- a/components/HotelReservation/HotelCardDialog/index.tsx +++ b/components/HotelReservation/HotelCardDialog/index.tsx @@ -1,13 +1,18 @@ "use client" +import { useParams } from "next/navigation" import { useIntl } from "react-intl" +import { Lang } from "@/constants/languages" +import { selectRate } from "@/constants/routes/hotelReservation" + import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { CloseLargeIcon } from "@/components/Icons" import TripAdvisorIcon from "@/components/Icons/TripAdvisor" 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" @@ -17,13 +22,15 @@ import styles from "./hotelCardDialog.module.css" import type { HotelCardDialogProps } from "@/types/components/hotelReservation/selectHotel/map" export default function HotelCardDialog({ - pin, + data, isOpen, handleClose, }: HotelCardDialogProps) { + const params = useParams() + const lang = params.lang as Lang const intl = useIntl() - if (!pin) { + if (!data) { return null } @@ -35,7 +42,7 @@ export default function HotelCardDialog({ amenities, images, ratings, - } = pin + } = data const firstImage = images[0]?.imageSizes?.small const altText = images[0]?.metaData?.altText @@ -52,20 +59,24 @@ export default function HotelCardDialog({
{altText}
- - + + {ratings}
- {name} +
+ {name} +
{amenities.map((facility) => { const IconComponent = mapFacilityToIcon(facility.id) return (
- {IconComponent && } + {IconComponent && ( + + )} {facility.name} @@ -90,8 +101,15 @@ export default function HotelCardDialog({ )}
-
diff --git a/components/HotelReservation/HotelCardDialogListing/index.tsx b/components/HotelReservation/HotelCardDialogListing/index.tsx new file mode 100644 index 000000000..fe75c9b33 --- /dev/null +++ b/components/HotelReservation/HotelCardDialogListing/index.tsx @@ -0,0 +1,88 @@ +"use client" + +import { useCallback, useEffect, useRef } from "react" + +import HotelCardDialog from "../HotelCardDialog" +import { getHotelPins } from "./utils" + +import type { HotelCardDialogListingProps } from "@/types/components/hotelReservation/selectHotel/map" + +export default function HotelCardDialogListing({ + hotels, + activeCard, + onActiveCardChange, +}: HotelCardDialogListingProps) { + const hotelsPinData = getHotelPins(hotels) + const activeCardRef = useRef(null) + const observerRef = useRef(null) + + const handleIntersection = useCallback( + (entries: IntersectionObserverEntry[]) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + const cardName = entry.target.getAttribute("data-name") + if (cardName) { + onActiveCardChange(cardName) + } + } + }) + }, + [onActiveCardChange] + ) + + useEffect(() => { + observerRef.current = new IntersectionObserver(handleIntersection, { + root: null, + threshold: 0.5, + }) + + const elements = document.querySelectorAll("[data-name]") + elements.forEach((el) => observerRef.current?.observe(el)) + + return () => { + elements.forEach((el) => observerRef.current?.unobserve(el)) + observerRef.current?.disconnect() + } + }, [handleIntersection]) + + useEffect(() => { + if (activeCardRef.current) { + // Temporarily disconnect the observer + observerRef.current?.disconnect() + + activeCardRef.current.scrollIntoView({ + behavior: "smooth", + block: "nearest", + inline: "center", + }) + + // Reconnect the observer after scrolling + const elements = document.querySelectorAll("[data-name]") + setTimeout(() => { + elements.forEach((el) => observerRef.current?.observe(el)) + }, 500) + } + }, [activeCard]) + + return ( + <> + {hotelsPinData?.length && + hotelsPinData.map((data) => { + const isActive = data.name === activeCard + return ( +
+ onActiveCardChange(null)} + /> +
+ ) + })} + + ) +} diff --git a/components/HotelReservation/HotelCardDialogListing/utils.ts b/components/HotelReservation/HotelCardDialogListing/utils.ts new file mode 100644 index 000000000..6183c5da4 --- /dev/null +++ b/components/HotelReservation/HotelCardDialogListing/utils.ts @@ -0,0 +1,22 @@ +import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" +import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" + +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, + 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, + operaId: hotel.hotelData.operaId, + })) +} diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css index 253df80d6..962684640 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/hotelListing.module.css @@ -2,6 +2,37 @@ display: none; } +.hotelListingMobile { + display: none; + align-items: flex-end; + overflow-x: auto; + position: absolute; + bottom: 0px; + left: 0; + right: 0; + z-index: 10; + height: 280px; + gap: var(--Spacing-x1); +} + +.hotelListingMobile[data-open="true"] { + display: flex; +} + +.hotelListingMobile dialog { + position: relative; + padding: 0; + margin: 0; +} + +.hotelListingMobile > div:first-child { + margin-left: 16px; +} + +.hotelListingMobile > div:last-child { + margin-right: 16px; +} + @media (min-width: 768px) { .hotelListing { display: block; @@ -9,4 +40,9 @@ overflow-y: auto; padding-top: var(--Spacing-x2); } + + .hotelListingMobile, + .hotelListingMobile[data-open="true"] { + display: none; + } } diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx index 65bd476e6..71d324155 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/HotelListing/index.tsx @@ -1,5 +1,6 @@ "use client" +import HotelCardDialogListing from "@/components/HotelReservation/HotelCardDialogListing" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import styles from "./hotelListing.module.css" @@ -10,16 +11,25 @@ import type { HotelListingProps } from "@/types/components/hotelReservation/sele export default function HotelListing({ hotels, activeHotelPin, - onHotelCardHover, + setActiveHotelPin, }: HotelListingProps) { return ( -
- -
+ <> +
+ +
+
+ +
+ ) } diff --git a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx index 3367d25b7..21b804306 100644 --- a/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx +++ b/components/HotelReservation/SelectHotel/SelectHotelMap/index.tsx @@ -3,6 +3,7 @@ import { APIProvider } from "@vis.gl/react-google-maps" import { useRouter, useSearchParams } from "next/navigation" import { useEffect, useState } from "react" import { useIntl } from "react-intl" +import { useMediaQuery } from "usehooks-ts" import { selectHotel } from "@/constants/routes/hotelReservation" @@ -12,6 +13,7 @@ import Button from "@/components/TempDesignSystem/Button" import useLang from "@/hooks/useLang" import HotelListing from "./HotelListing" +import { getCentralCoordinates } from "./utils" import styles from "./selectHotelMap.module.css" @@ -19,19 +21,33 @@ import { SelectHotelMapProps } from "@/types/components/hotelReservation/selectH export default function SelectHotelMap({ apiKey, - coordinates, hotelPins, mapId, - isModal, hotels, }: SelectHotelMapProps) { const searchParams = useSearchParams() const router = useRouter() const lang = useLang() const intl = useIntl() + const isAboveMobile = useMediaQuery("(min-width: 768px)") const [activeHotelPin, setActiveHotelPin] = useState(null) const [showBackToTop, setShowBackToTop] = useState(false) + const centralCoordinates = getCentralCoordinates(hotelPins) + + const coordinates = isAboveMobile + ? centralCoordinates + : { ...centralCoordinates, lat: centralCoordinates.lat - 0.006 } + + const selectHotelParams = new URLSearchParams(searchParams.toString()) + const selectedHotel = selectHotelParams.get("selectedHotel") + + useEffect(() => { + if (selectedHotel) { + setActiveHotelPin(selectedHotel) + } + }, [selectedHotel]) + useEffect(() => { const hotelListingElement = document.querySelector( `.${styles.listingContainer}` @@ -54,10 +70,6 @@ export default function SelectHotelMap({ hotelListingElement?.scrollTo({ top: 0, behavior: "smooth" }) } - function handleModalDismiss() { - router.back() - } - function handlePageRedirect() { router.push(`${selectHotel[lang]}?${searchParams.toString()}`) } @@ -68,7 +80,7 @@ export default function SelectHotelMap({ size="small" theme="base" className={styles.closeButton} - onClick={isModal ? handleModalDismiss : handlePageRedirect} + onClick={handlePageRedirect} > {intl.formatMessage({ id: "Close the map" })} @@ -84,7 +96,7 @@ export default function SelectHotelMap({ size="small" variant="icon" wrapping - onClick={isModal ? handleModalDismiss : handlePageRedirect} + onClick={handlePageRedirect} className={styles.filterContainerCloseButton} > @@ -95,7 +107,7 @@ export default function SelectHotelMap({ {showBackToTop && (