diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Content.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Content.tsx deleted file mode 100644 index 451bb07e6..000000000 --- a/components/ContentType/HotelPage/Map/DynamicMap/Content.tsx +++ /dev/null @@ -1,147 +0,0 @@ -"use client" -import { - AdvancedMarker, - Map, - type MapProps, - useMap, -} from "@vis.gl/react-google-maps" -import { useState } from "react" -import { useIntl } from "react-intl" - -import useHotelPageStore from "@/stores/hotel-page" - -import { CloseIcon, MinusIcon, PlusIcon } from "@/components/Icons" -import Button from "@/components/TempDesignSystem/Button" -import Divider from "@/components/TempDesignSystem/Divider" -import Title from "@/components/TempDesignSystem/Text/Title" -import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" -import useLang from "@/hooks/useLang" - -import ScandicMarker from "../Markers/Scandic" - -import styles from "./dynamicMap.module.css" - -import type { DynamicMapContentProps } from "@/types/components/hotelPage/map/dynamicMapContent" - -export default function DynamicMapContent({ - hotelName, - coordinates, -}: DynamicMapContentProps) { - const intl = useIntl() - const lang = useLang() - const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore() - const [isFullScreenSidebar, setIsFullScreenSidebar] = useState(false) - const map = useMap() - - const mapOptions: MapProps = { - defaultZoom: 15, - defaultCenter: coordinates, - disableDefaultUI: true, - clickableIcons: false, - mapId: `${hotelName}-${lang}-map`, - // As reference for future styles when adding POIs - // styles: [ - // { - // featureType: "poi", - // elementType: "all", - // stylers: [{ visibility: "off" }], - // }, - // ], - } - - useHandleKeyUp((event: KeyboardEvent) => { - if (event.key === "Escape" && isDynamicMapOpen) { - closeDynamicMap() - } - }) - - function zoomIn() { - const currentZoom = map && map.getZoom() - if (currentZoom) { - map.setZoom(currentZoom + 1) - } - } - function zoomOut() { - const currentZoom = map && map.getZoom() - if (currentZoom) { - map.setZoom(currentZoom - 1) - } - } - - function toggleFullScreenSidebar() { - setIsFullScreenSidebar((prev) => !prev) - } - - return ( - <> -
- -
- - -
-
- -
- - - - - -
- - ) -} diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx new file mode 100644 index 000000000..196baff83 --- /dev/null +++ b/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx @@ -0,0 +1,137 @@ +"use client" +import { + AdvancedMarker, + AdvancedMarkerAnchorPoint, + Map, + type MapProps, + useMap, +} from "@vis.gl/react-google-maps" +import { useIntl } from "react-intl" + +import useHotelPageStore from "@/stores/hotel-page" + +import { MinusIcon, PlusIcon } from "@/components/Icons" +import CloseLargeIcon from "@/components/Icons/CloseLarge" +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 styles from "./map.module.css" + +import type { MapContentProps } from "@/types/components/hotelPage/map/mapContent" + +export default function MapContent({ + coordinates, + pointsOfInterest, + activePoi, + onActivePoiChange, +}: MapContentProps) { + const intl = useIntl() + const { closeDynamicMap } = useHotelPageStore() + const map = useMap() + + const mapOptions: MapProps = { + defaultZoom: 14, + defaultCenter: coordinates, + disableDefaultUI: true, + clickableIcons: false, + mapId: "6b48ef228325ae84", + } + + function zoomIn() { + const currentZoom = map && map.getZoom() + if (currentZoom) { + map.setZoom(currentZoom + 1) + } + } + function zoomOut() { + const currentZoom = map && map.getZoom() + if (currentZoom) { + map.setZoom(currentZoom - 1) + } + } + + 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/ContentType/HotelPage/Map/DynamicMap/Map/map.module.css b/components/ContentType/HotelPage/Map/DynamicMap/Map/map.module.css new file mode 100644 index 000000000..557730bfe --- /dev/null +++ b/components/ContentType/HotelPage/Map/DynamicMap/Map/map.module.css @@ -0,0 +1,108 @@ +.mapContainer { + --button-box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1); + width: 100%; + position: relative; + z-index: 0; +} + +.mapContainer::after { + content: ""; + position: absolute; + top: 0; + right: 0; + background: linear-gradient( + 43deg, + rgba(172, 172, 172, 0) 57.66%, + rgba(0, 0, 0, 0.25) 92.45% + ); + width: 100%; + height: 100%; + pointer-events: none; +} + +.ctaButtons { + position: absolute; + top: var(--Spacing-x2); + right: var(--Spacing-x2); + z-index: 1; + display: flex; + flex-direction: column; + gap: var(--Spacing-x7); + align-items: flex-end; + pointer-events: none; +} + +.zoomButtons { + display: grid; + gap: var(--Spacing-x1); +} + +.closeButton { + pointer-events: initial; + box-shadow: var(--button-box-shadow); + gap: var(--Spacing-x-half); +} + +.zoomButton { + width: var(--Spacing-x5); + height: var(--Spacing-x5); + padding: 0; + pointer-events: initial; + 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); + right: var(--Spacing-x4); + bottom: var(--Spacing-x4); + justify-content: space-between; + } + + .zoomButtons { + display: flex; + } +} diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx new file mode 100644 index 000000000..217fa51bd --- /dev/null +++ b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx @@ -0,0 +1,102 @@ +"use client" + +import { useState } from "react" +import { useIntl } from "react-intl" + +import PoiMarker from "@/components/Maps/Markers/Poi" +import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" +import Title from "@/components/TempDesignSystem/Text/Title" + +import styles from "./sidebar.module.css" + +import type { SidebarProps } from "@/types/components/hotelPage/map/sidebar" + +export default function Sidebar({ + activePoi, + hotelName, + pointsOfInterest, + onActivePoiChange, +}: SidebarProps) { + const intl = useIntl() + const [isFullScreenSidebar, setIsFullScreenSidebar] = useState(false) + const poiCategories = new Set( + pointsOfInterest.map(({ category }) => category) + ) + const poisInCategories = Array.from(poiCategories).map((category) => ({ + category, + pois: pointsOfInterest.filter((poi) => poi.category === category), + })) + + function toggleFullScreenSidebar() { + setIsFullScreenSidebar((prev) => !prev) + } + + return ( + + ) +} diff --git a/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css new file mode 100644 index 000000000..11f26b822 --- /dev/null +++ b/components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css @@ -0,0 +1,111 @@ +.sidebar { + --sidebar-max-width: 26.25rem; + --sidebar-mobile-toggle-height: 91px; + --sidebar-mobile-fullscreen-height: calc( + 100vh - var(--main-menu-mobile-height) - var(--sidebar-mobile-toggle-height) + ); + + position: absolute; + top: var(--sidebar-mobile-fullscreen-height); + height: 100%; + right: 0; + left: 0; + background-color: var(--Base-Surface-Primary-light-Normal); + z-index: 1; + transition: top 0.3s; +} + +.sidebar:not(.fullscreen) { + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; +} + +.sidebar.fullscreen { + top: 0; +} + +.sidebarToggle { + position: relative; + margin: var(--Spacing-x4) 0 var(--Spacing-x2); + width: 100%; +} + +.sidebarToggle::before { + content: ""; + position: absolute; + display: block; + top: -0.5rem; + width: 100px; + height: 3px; + background-color: var(--UI-Text-High-contrast); +} + +.sidebarContent { + display: grid; + gap: var(--Spacing-x5); + align-content: start; + padding: var(--Spacing-x3) var(--Spacing-x2); + height: var(--sidebar-mobile-fullscreen-height); + overflow-y: auto; +} + +.poiGroup { + display: grid; + gap: var(--Spacing-x2); +} + +.poiHeading { + display: flex; + align-items: center; + gap: var(--Spacing-x1); +} + +.poiList { + list-style: none; +} + +.poiItem { + padding: var(--Spacing-x1) 0; + border-bottom: 1px solid var(--Base-Border-Subtle); +} + +.poiButton { + background-color: var(--Base-Surface-Primary-light-Normal); + border-width: 0; + font-family: var(--typography-Body-Regular-fontFamily); + font-size: var(--typography-Body-Regular-fontSize); + font-weight: var(--typography-Body-Regular-fontWeight); + color: var(--UI-Text-High-contrast); + width: 100%; + display: grid; + grid-template-columns: 1fr max-content; + gap: var(--Spacing-x2); + align-items: center; + padding: var(--Spacing-x-half) var(--Spacing-x1); + border-radius: var(--Corner-radius-Medium); + cursor: pointer; + text-align: left; + transition: background-color 0.3s; +} +.poiButton.active { + background-color: var(--Base-Surface-Primary-light-Hover); +} + +@media screen and (min-width: 768px) { + .sidebar { + position: static; + width: 40vw; + min-width: 10rem; + max-width: var(--sidebar-max-width); + background-color: var(--Base-Surface-Primary-light-Normal); + } + + .sidebarToggle { + display: none; + } + + .sidebarContent { + padding: var(--Spacing-x4) var(--Spacing-x5); + height: 100%; + position: relative; + } +} diff --git a/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css b/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css index 303ad4c1f..2a3e4ff78 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css +++ b/components/ContentType/HotelPage/Map/DynamicMap/dynamicMap.module.css @@ -1,5 +1,4 @@ .dynamicMap { - --sidebar-height: 88px; position: fixed; top: var(--main-menu-mobile-height); right: 0; @@ -10,143 +9,8 @@ background-color: var(--Base-Surface-Primary-light-Normal); } -.sidebar { - position: absolute; - top: calc(100vh - var(--main-menu-mobile-height) - var(--sidebar-height)); - height: 100%; - right: 0; - left: 0; - background-color: var(--Base-Surface-Primary-light-Normal); - z-index: 1; - transition: top 0.3s; -} - -.sidebar:not(.fullscreen) { - border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; -} - -.sidebar.fullscreen { - top: 0; -} - -.sidebarToggle { - display: grid; - gap: var(--Spacing-x1); - padding: var(--Spacing-x2); - height: var(--sidebar-height); -} - -.toggleButton { - position: relative; - display: flex; - justify-content: center; - background-color: transparent; - border-width: 0; - padding: var(--Spacing-x-one-and-half) var(--Spacing-x2); - font-family: var(--typography-Body-Bold-fontFamily); - font-size: var(--typography-Body-Bold-fontSize); - font-weight: var(--typography-Body-Bold-fontWeight); - color: var(--UI-Text-Medium-contrast); - width: 100%; -} - -.toggleButton::before { - content: ""; - position: absolute; - display: block; - top: -0.5rem; - width: 100px; - height: 3px; - background-color: var(--UI-Text-High-contrast); -} - -.sidebarContent { - display: grid; - gap: var(--Spacing-x3); - align-content: start; - padding: var(--Spacing-x3) var(--Spacing-x2); - height: 100%; - overflow-y: auto; -} - -.mapContainer { - flex: 1; - position: relative; -} - -.mapContainer::after { - content: ""; - display: block; - position: absolute; - top: 0; - right: 0; - background: linear-gradient( - 43deg, - rgba(172, 172, 172, 0) 57.66%, - rgba(0, 0, 0, 0.25) 92.45% - ); - width: 100%; - height: 100%; - pointer-events: none; -} - -.ctaButtons { - position: absolute; - top: var(--Spacing-x2); - right: var(--Spacing-x2); - z-index: 1; - display: flex; - flex-direction: column; - gap: var(--Spacing-x7); - align-items: flex-end; - pointer-events: none; -} - -.zoomButtons { - display: grid; - gap: var(--Spacing-x1); -} - -.closeButton { - pointer-events: initial; - box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1); -} - -.zoomButton { - width: var(--Spacing-x5); - height: var(--Spacing-x5); - padding: 0; - pointer-events: initial; - box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1); -} - @media screen and (min-width: 768px) { .dynamicMap { top: var(--main-menu-desktop-height); } - .sidebar { - position: static; - width: 40vw; - min-width: 10rem; - max-width: 26.25rem; - background-color: var(--Base-Surface-Primary-light-Normal); - } - .sidebarToggle { - display: none; - } - - .sidebarContent { - padding: var(--Spacing-x4) var(--Spacing-x5); - } - - .ctaButtons { - top: var(--Spacing-x4); - right: var(--Spacing-x4); - bottom: var(--Spacing-x4); - justify-content: space-between; - } - - .zoomButtons { - display: flex; - } } diff --git a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx index 2eee0b6ae..28cf8cc9b 100644 --- a/components/ContentType/HotelPage/Map/DynamicMap/index.tsx +++ b/components/ContentType/HotelPage/Map/DynamicMap/index.tsx @@ -8,7 +8,8 @@ import useHotelPageStore from "@/stores/hotel-page" import { useHandleKeyUp } from "@/hooks/useHandleKeyUp" -import DynamicMapContent from "./Content" +import MapContent from "./Map" +import Sidebar from "./Sidebar" import styles from "./dynamicMap.module.css" @@ -18,11 +19,13 @@ export default function DynamicMap({ apiKey, hotelName, coordinates, + pointsOfInterest, }: DynamicMapProps) { const intl = useIntl() const { isDynamicMapOpen, closeDynamicMap } = useHotelPageStore() const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0) const hasMounted = useRef(false) + const [activePoi, setActivePoi] = useState(null) useHandleKeyUp((event: KeyboardEvent) => { if (event.key === "Escape" && isDynamicMapOpen) { @@ -58,7 +61,18 @@ export default function DynamicMap({ { hotelName } )} > - + + diff --git a/components/ContentType/HotelPage/Map/MapCard/index.tsx b/components/ContentType/HotelPage/Map/MapCard/index.tsx index c3caca9ed..8764870bc 100644 --- a/components/ContentType/HotelPage/Map/MapCard/index.tsx +++ b/components/ContentType/HotelPage/Map/MapCard/index.tsx @@ -4,7 +4,9 @@ import { useIntl } from "react-intl" import useHotelPageStore from "@/stores/hotel-page" +import PoiMarker from "@/components/Maps/Markers/Poi" import Button from "@/components/TempDesignSystem/Button" +import Body from "@/components/TempDesignSystem/Text/Body" import Caption from "@/components/TempDesignSystem/Text/Caption" import Title from "@/components/TempDesignSystem/Text/Title" @@ -12,7 +14,7 @@ import styles from "./mapCard.module.css" import type { MapCardProps } from "@/types/components/hotelPage/map/mapCard" -export default function MapCard({ hotelName }: MapCardProps) { +export default function MapCard({ hotelName, pois }: MapCardProps) { const intl = useIntl() const { openDynamicMap } = useHotelPageStore() @@ -34,6 +36,15 @@ export default function MapCard({ hotelName }: MapCardProps) { > {hotelName} +
    + {pois.map((poi) => ( +
  • + + {poi.name} + {poi.distance} km +
  • + ))} +