From 2849c69c522dcccd4412ea902a9fd0f0fa0fa842 Mon Sep 17 00:00:00 2001 From: Arvid Norlin Date: Tue, 3 Sep 2024 15:35:06 +0200 Subject: [PATCH 1/4] refactor: Move Sidepeek param logic to SidePeekProvider --- .../HotelPage/IntroSection/index.tsx | 1 + .../ContentType/HotelPage/SidePeeks.tsx | 99 ----------------- components/ContentType/HotelPage/index.tsx | 55 ++++++++- components/SidePeekProvider/index.tsx | 45 ++++++++ components/TempDesignSystem/Link/index.tsx | 2 +- .../TempDesignSystem/SidePeek/Item/index.tsx | 39 ------- .../SidePeek/Item/sidePeekItem.module.css | 27 ----- .../TempDesignSystem/SidePeek/index.tsx | 75 +++++++++---- .../SidePeek/sidePeek.module.css | 105 ++++++++++++------ .../TempDesignSystem/SidePeek/sidePeek.ts | 6 +- components/TempDesignSystem/SidePeek/types.ts | 4 +- 11 files changed, 227 insertions(+), 231 deletions(-) delete mode 100644 components/ContentType/HotelPage/SidePeeks.tsx create mode 100644 components/SidePeekProvider/index.tsx delete mode 100644 components/TempDesignSystem/SidePeek/Item/index.tsx delete mode 100644 components/TempDesignSystem/SidePeek/Item/sidePeekItem.module.css diff --git a/components/ContentType/HotelPage/IntroSection/index.tsx b/components/ContentType/HotelPage/IntroSection/index.tsx index d20486c60..cb62ca01c 100644 --- a/components/ContentType/HotelPage/IntroSection/index.tsx +++ b/components/ContentType/HotelPage/IntroSection/index.tsx @@ -73,6 +73,7 @@ export default async function IntroSection({ color="burgundy" variant="icon" href={`?s=${about[lang]}`} + scroll={false} > {intl.formatMessage({ id: "Read more about the hotel" })} diff --git a/components/ContentType/HotelPage/SidePeeks.tsx b/components/ContentType/HotelPage/SidePeeks.tsx deleted file mode 100644 index 2fcf9b46f..000000000 --- a/components/ContentType/HotelPage/SidePeeks.tsx +++ /dev/null @@ -1,99 +0,0 @@ -"use client" - -import { usePathname, useRouter, useSearchParams } from "next/navigation" -import { useEffect, useState } from "react" -import { useIntl } from "react-intl" - -import { - about, - activities, - amenities, - meetingsAndConferences, - restaurantAndBar, - wellnessAndExercise, -} from "@/constants/routes/hotelPageParams" - -import SidePeek from "@/components/TempDesignSystem/SidePeek" -import SidePeekItem from "@/components/TempDesignSystem/SidePeek/Item" -import { SidePeekContentKey } from "@/components/TempDesignSystem/SidePeek/types" -import useLang from "@/hooks/useLang" - -function SidePeekContainer() { - const router = useRouter() - const pathname = usePathname() - const searchParams = useSearchParams() - const [activeSidePeek, setActiveSidePeek] = - useState(() => { - const sidePeekParam = searchParams.get("s") as SidePeekContentKey | null - return sidePeekParam || null - }) - - const lang = useLang() - const intl = useIntl() - - useEffect(() => { - const sidePeekParam = searchParams.get("s") as SidePeekContentKey | null - if (sidePeekParam !== activeSidePeek) { - setActiveSidePeek(sidePeekParam) - } - }, [searchParams, activeSidePeek]) - - function handleClose(isOpen: boolean) { - if (!isOpen) { - setActiveSidePeek(null) - - const nextSearchParams = new URLSearchParams(searchParams.toString()) - nextSearchParams.delete("s") - - router.push(`${pathname}?${nextSearchParams}`, { scroll: false }) - } - } - - return ( - - - {/* TODO: Render amenities as per the design. */} - Read more about the amenities here - - - Some additional information about the hotel - - - {/* TODO */} - Restaurant & Bar - - - {/* TODO */} - Wellness & Exercise - - - {/* TODO */} - Activities - - - {/* TODO */} - Meetings & Conferences - - - ) -} - -export default SidePeekContainer diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index f37750db4..210bf9b7c 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -1,6 +1,12 @@ +import hotelPageParams from "@/constants/routes/hotelPageParams" import { env } from "@/env/server" import { serverClient } from "@/lib/trpc/server" +import SidePeekProvider from "@/components/SidePeekProvider" +import SidePeek from "@/components/TempDesignSystem/SidePeek" +import { getIntl } from "@/i18n" +import { getLang } from "@/i18n/serverContext" + import { MOCK_FACILITIES } from "./Facilities/mockData" import { setActivityCard } from "./Facilities/utils" import DynamicMap from "./Map/DynamicMap" @@ -12,13 +18,14 @@ import Facilities from "./Facilities" import IntroSection from "./IntroSection" import PreviewImages from "./PreviewImages" import { Rooms } from "./Rooms" -import SidePeeks from "./SidePeeks" import TabNavigation from "./TabNavigation" import styles from "./hotelPage.module.css" export default async function HotelPage() { const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY + const intl = await getIntl() + const lang = getLang() const hotelData = await serverClient().hotel.get({ include: ["RoomCategories"], }) @@ -61,6 +68,51 @@ export default async function HotelPage() { address={hotelAddress} tripAdvisor={hotelRatings?.tripAdvisor} /> + + {/* eslint-disable import/no-named-as-default-member */} + + {/* TODO: Render amenities as per the design. */} + Read more about the amenities here + + + Some additional information about the hotel + + + {/* TODO */} + Restaurant & Bar + + + {/* TODO */} + Wellness & Exercise + + + {/* TODO */} + Activities + + + {/* TODO */} + Meetings & Conferences + + {/* eslint-enable import/no-named-as-default-member */} + @@ -80,7 +132,6 @@ export default async function HotelPage() { /> ) : null} - ) } diff --git a/components/SidePeekProvider/index.tsx b/components/SidePeekProvider/index.tsx new file mode 100644 index 000000000..dad577b55 --- /dev/null +++ b/components/SidePeekProvider/index.tsx @@ -0,0 +1,45 @@ +"use client" +import { usePathname, useRouter, useSearchParams } from "next/navigation" +import { createContext, useEffect, useState } from "react" + +interface ISidePeekContext { + handleClose: (isOpen: boolean) => void + activeSidePeek: string | null +} + +export const SidePeekContext = createContext(null) + +function SidePeekProvider({ children }: React.PropsWithChildren) { + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() + const [activeSidePeek, setActiveSidePeek] = useState(() => { + const sidePeekParam = searchParams.get("s") + return sidePeekParam || null + }) + + useEffect(() => { + const sidePeekParam = searchParams.get("s") + if (sidePeekParam !== activeSidePeek) { + setActiveSidePeek(sidePeekParam) + } + }, [searchParams, activeSidePeek]) + + function handleClose(isOpen: boolean) { + if (!isOpen) { + const nextSearchParams = new URLSearchParams(searchParams.toString()) + nextSearchParams.delete("s") + + router.push(`${pathname}?${nextSearchParams}`, { scroll: false }) + setActiveSidePeek(null) + } + } + + return ( + + {children} + + ) +} + +export default SidePeekProvider diff --git a/components/TempDesignSystem/Link/index.tsx b/components/TempDesignSystem/Link/index.tsx index 6856f87aa..23cf15e46 100644 --- a/components/TempDesignSystem/Link/index.tsx +++ b/components/TempDesignSystem/Link/index.tsx @@ -75,7 +75,7 @@ export default function Link({ trackPageViewStart() startTransition(() => { startRouterTransition() - router.push(href) + router.push(href, { scroll }) }) }} href={href} diff --git a/components/TempDesignSystem/SidePeek/Item/index.tsx b/components/TempDesignSystem/SidePeek/Item/index.tsx deleted file mode 100644 index fa897aae9..000000000 --- a/components/TempDesignSystem/SidePeek/Item/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -"use client" - -import { PropsWithChildren } from "react" - -import { CloseIcon } from "@/components/Icons" -import { SidePeekContentProps } from "@/components/TempDesignSystem/SidePeek/types" -import Title from "@/components/TempDesignSystem/Text/Title" - -import Button from "../../Button" - -import styles from "./sidePeekItem.module.css" - -function SidePeekItem({ - title, - children, - isActive = false, - onClose, -}: PropsWithChildren) { - return isActive ? ( - - ) : null -} - -export default SidePeekItem \ No newline at end of file diff --git a/components/TempDesignSystem/SidePeek/Item/sidePeekItem.module.css b/components/TempDesignSystem/SidePeek/Item/sidePeekItem.module.css deleted file mode 100644 index eb90ed60b..000000000 --- a/components/TempDesignSystem/SidePeek/Item/sidePeekItem.module.css +++ /dev/null @@ -1,27 +0,0 @@ -.sidePeekItem { - display: grid; - grid-template-rows: min-content auto; - gap: var(--Spacing-x4); - height: 100%; -} - -.content>* { - padding: var(--Spacing-x3) var(--Spacing-x2); -} - -.header { - display: flex; - justify-content: flex-end; - border-bottom: 1px solid var(--Base-Border-Subtle); - align-items: center; -} - -.header:has(> h2) { - justify-content: space-between; -} - -@media screen and (min-width: 1367px) { - .content>* { - padding: var(--Spacing-x4); - } -} \ No newline at end of file diff --git a/components/TempDesignSystem/SidePeek/index.tsx b/components/TempDesignSystem/SidePeek/index.tsx index 271ad30e5..df0b8dc44 100644 --- a/components/TempDesignSystem/SidePeek/index.tsx +++ b/components/TempDesignSystem/SidePeek/index.tsx @@ -1,15 +1,20 @@ "use client" import { useIsSSR } from "@react-aria/ssr" -import React, { Children, cloneElement } from "react" +import { useContext } from "react" import { Dialog, DialogTrigger, Modal, ModalOverlay, } from "react-aria-components" +import { useIntl } from "react-intl" -import { SidePeekContentKey } from "@/components/TempDesignSystem/SidePeek/types" +import { CloseIcon } from "@/components/Icons" +import { SidePeekContext } from "@/components/SidePeekProvider" + +import Button from "../Button" +import Title from "../Text/Title" import styles from "./sidePeek.module.css" @@ -17,33 +22,61 @@ import type { SidePeekProps } from "./sidePeek" function SidePeek({ children, + title, + contentKey, handleClose, - activeSidePeek, + isOpen, }: React.PropsWithChildren) { - const sidePeekChildren = Children.map(children, (child) => { - if (!React.isValidElement(child)) { - return child - } - return cloneElement(child as React.ReactElement, { - isActive: - (child.props.contentKey as SidePeekContentKey) === activeSidePeek, - onClose: handleClose, - }) - }) - const isSSR = useIsSSR() - return isSSR ? ( -
{children}
- ) : ( + const intl = useIntl() + const context = useContext(SidePeekContext) + function onClose() { + const closeHandler = handleClose || context?.handleClose + closeHandler && closeHandler(false) + } + + if (isSSR) { + return ( +
+

{title}

+ {children} +
+ ) + } + return ( - - {sidePeekChildren} + + + + diff --git a/components/TempDesignSystem/SidePeek/sidePeek.module.css b/components/TempDesignSystem/SidePeek/sidePeek.module.css index 0ed9304c5..2d6de4f00 100644 --- a/components/TempDesignSystem/SidePeek/sidePeek.module.css +++ b/components/TempDesignSystem/SidePeek/sidePeek.module.css @@ -1,38 +1,9 @@ -.sidePeek { - position: fixed; - top: var(--current-mobile-site-header-height); - right: auto; - bottom: 0; - width: 100%; - height: calc(100vh - var(--current-mobile-site-header-height)); - background-color: var(--Base-Background-Primary-Normal); - z-index: 100; - box-shadow: 0 0 10px rgba(0, 0, 0, 0.85); -} - -.sidePeek[data-entering] { - animation: slide-up 300ms; -} - -.sidePeek[data-exiting] { - animation: slide-up 300ms reverse; -} - -.dialog { - height: 100%; -} - -.overlay { - position: absolute; - top: var(--current-mobile-site-header-height); - bottom: 0; - left: 0; - right: 0; - z-index: 99; +.modal { + --sidepeek-desktop-width: 600px; } @keyframes slide-in { from { - right: -600px; + right: calc(-1 * var(--sidepeek-desktop-width)); } to { @@ -46,24 +17,84 @@ } to { - top: var(--current-mobile-site-header-height); + top: 0; } } +.overlay { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + z-index: 99; +} + +.modal { + position: fixed; + top: 0; + right: auto; + bottom: 0; + width: 100%; + height: 100vh; + background-color: var(--Base-Background-Primary-Normal); + z-index: 100; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.85); +} + +.modal[data-entering] { + animation: slide-up 300ms; +} + +.modal[data-exiting] { + animation: slide-up 300ms reverse; +} + +.dialog { + height: 100%; +} + +.sidePeek { + display: grid; + grid-template-rows: min-content auto; + height: 100%; +} + +.header { + display: flex; + justify-content: flex-end; + border-bottom: 1px solid var(--Base-Border-Subtle); + align-items: center; + padding: var(--Spacing-x4); +} + +.header:has(> h2) { + justify-content: space-between; +} + +.closeButton { + padding: 0; +} + +.sidePeekContent { + padding: var(--Spacing-x4); +} @media screen and (min-width: 1367px) { - .sidePeek { + .modal { top: 0; right: 0px; - width: 600px; + width: var(--sidepeek-desktop-width); height: 100vh; } - .sidePeek[data-entering] { + + .modal[data-entering] { animation: slide-in 250ms; } - .sidePeek[data-exiting] { + .modal[data-exiting] { animation: slide-in 250ms reverse; } + .overlay { top: 0; } diff --git a/components/TempDesignSystem/SidePeek/sidePeek.ts b/components/TempDesignSystem/SidePeek/sidePeek.ts index 626fc640c..e1781f137 100644 --- a/components/TempDesignSystem/SidePeek/sidePeek.ts +++ b/components/TempDesignSystem/SidePeek/sidePeek.ts @@ -1,4 +1,6 @@ export interface SidePeekProps { - handleClose: (isOpen: boolean) => void - activeSidePeek: string | null + contentKey: string + title: string + isOpen?: boolean + handleClose?: (isOpen: boolean) => void } diff --git a/components/TempDesignSystem/SidePeek/types.ts b/components/TempDesignSystem/SidePeek/types.ts index f506fc6bf..e37554aff 100644 --- a/components/TempDesignSystem/SidePeek/types.ts +++ b/components/TempDesignSystem/SidePeek/types.ts @@ -1,5 +1,3 @@ -export type SidePeekContentKey = string - export type SidePeekProps = { activeContent: string | null onClose: (isOpen: boolean) => void @@ -7,7 +5,7 @@ export type SidePeekProps = { export type SidePeekContentProps = { title?: string - contentKey: SidePeekContentKey + contentKey: string isActive?: boolean onClose?: () => void } From 1729f4b9c716e7691d7d1e96d1b5f7f26d1a5941 Mon Sep 17 00:00:00 2001 From: Christel Westerberg Date: Wed, 18 Sep 2024 09:48:31 +0200 Subject: [PATCH 2/4] fix: hide tier nights --- .../MyPages/Blocks/Overview/Stats/Points/index.tsx | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/components/MyPages/Blocks/Overview/Stats/Points/index.tsx b/components/MyPages/Blocks/Overview/Stats/Points/index.tsx index 0b3e450d7..09784bfe1 100644 --- a/components/MyPages/Blocks/Overview/Stats/Points/index.tsx +++ b/components/MyPages/Blocks/Overview/Stats/Points/index.tsx @@ -6,11 +6,7 @@ import { getMembershipLevelObject } from "@/utils/membershipLevel" import { getMembership } from "@/utils/user" import PointsContainer from "./Container" -import { - NextLevelNightsColumn, - NextLevelPointsColumn, - YourPointsColumn, -} from "./PointsColumn" +import { NextLevelPointsColumn, YourPointsColumn } from "./PointsColumn" import { UserProps } from "@/types/components/myPages/user" @@ -32,7 +28,8 @@ export default async function Points({ user }: UserProps) { subtitle={`${formatMessage({ id: "next level:" })} ${nextLevel.name}`} /> )} - {membership?.nightsToTopTier && ( + {/* TODO: Show NextLevelNightsColumn when nightsToTopTier data is correct from Antavo */} + {/* {membership?.nightsToTopTier && ( - )} + )} */} ) } From e79f413003588c77cd70a34548a796aa91dd9156 Mon Sep 17 00:00:00 2001 From: Erik Tiekstra Date: Tue, 17 Sep 2024 16:13:22 +0200 Subject: [PATCH 3/4] feat(SW-325): added pois to the list and dynamic map --- .../HotelPage/Map/DynamicMap/Content.tsx | 147 ------------------ .../HotelPage/Map/DynamicMap/Map/index.tsx | 137 ++++++++++++++++ .../Map/DynamicMap/Map/map.module.css | 108 +++++++++++++ .../Map/DynamicMap/Sidebar/index.tsx | 102 ++++++++++++ .../Map/DynamicMap/Sidebar/sidebar.module.css | 111 +++++++++++++ .../Map/DynamicMap/dynamicMap.module.css | 136 ---------------- .../HotelPage/Map/DynamicMap/index.tsx | 18 ++- .../HotelPage/Map/MapCard/index.tsx | 13 +- .../HotelPage/Map/MapCard/mapCard.module.css | 14 ++ .../MobileMapToggle/mobileToggle.module.css | 2 +- .../HotelPage/Map/StaticMap/index.tsx | 3 +- components/ContentType/HotelPage/index.tsx | 5 +- components/Icons/Cultural.tsx | 40 +++++ components/Icons/Museum.tsx | 36 +++++ components/Icons/Shopping.tsx | 40 +++++ components/Icons/StarFilled.tsx | 40 +++++ components/Icons/Train.tsx | 36 +++++ components/Icons/get-icon-by-icon-name.ts | 15 ++ components/Icons/index.tsx | 5 + components/Maps/Markers/Poi/index.tsx | 27 ++++ components/Maps/Markers/Poi/poi.module.css | 52 +++++++ components/Maps/Markers/Poi/variants.ts | 34 ++++ .../Map => Maps}/Markers/Scandic.tsx | 0 components/Maps/Markers/utils.ts | 21 +++ constants/poiCategories.ts | 19 +++ i18n/dictionaries/da.json | 18 ++- i18n/dictionaries/de.json | 18 ++- i18n/dictionaries/en.json | 18 ++- i18n/dictionaries/fi.json | 18 ++- i18n/dictionaries/no.json | 18 ++- i18n/dictionaries/sv.json | 18 ++- package-lock.json | 8 +- package.json | 2 +- server/routers/hotels/output.ts | 52 +++++-- server/routers/hotels/query.ts | 1 + server/routers/hotels/utils.ts | 18 +++ types/components/hotelPage/map/dynamicMap.ts | 2 + .../hotelPage/map/dynamicMapContent.ts | 6 - types/components/hotelPage/map/mapCard.ts | 3 + types/components/hotelPage/map/mapContent.ts | 9 ++ types/components/hotelPage/map/sidebar.ts | 8 + types/components/icon.ts | 5 + types/components/maps/poiMarker.ts | 8 + types/hotel.ts | 5 +- 44 files changed, 1078 insertions(+), 318 deletions(-) delete mode 100644 components/ContentType/HotelPage/Map/DynamicMap/Content.tsx create mode 100644 components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx create mode 100644 components/ContentType/HotelPage/Map/DynamicMap/Map/map.module.css create mode 100644 components/ContentType/HotelPage/Map/DynamicMap/Sidebar/index.tsx create mode 100644 components/ContentType/HotelPage/Map/DynamicMap/Sidebar/sidebar.module.css create mode 100644 components/Icons/Cultural.tsx create mode 100644 components/Icons/Museum.tsx create mode 100644 components/Icons/Shopping.tsx create mode 100644 components/Icons/StarFilled.tsx create mode 100644 components/Icons/Train.tsx create mode 100644 components/Maps/Markers/Poi/index.tsx create mode 100644 components/Maps/Markers/Poi/poi.module.css create mode 100644 components/Maps/Markers/Poi/variants.ts rename components/{ContentType/HotelPage/Map => Maps}/Markers/Scandic.tsx (100%) create mode 100644 components/Maps/Markers/utils.ts create mode 100644 constants/poiCategories.ts create mode 100644 server/routers/hotels/utils.ts delete mode 100644 types/components/hotelPage/map/dynamicMapContent.ts create mode 100644 types/components/hotelPage/map/mapContent.ts create mode 100644 types/components/hotelPage/map/sidebar.ts create mode 100644 types/components/maps/poiMarker.ts 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 +
  • + ))} +