diff --git a/.env.local.example b/.env.local.example
index 6d6af70cc..c747c3e1e 100644
--- a/.env.local.example
+++ b/.env.local.example
@@ -46,3 +46,5 @@ NEXTAUTH_URL="$PUBLIC_URL/api/web/auth"
GOOGLE_STATIC_MAP_KEY=""
GOOGLE_STATIC_MAP_SIGNATURE_SECRET=""
+GOOGLE_STATIC_MAP_ID=""
+GOOGLE_DYNAMIC_MAP_ID=""
diff --git a/.env.test b/.env.test
index 801c4336b..d2b538cc2 100644
--- a/.env.test
+++ b/.env.test
@@ -37,3 +37,7 @@ SEAMLESS_LOGOUT_NO="test"
SEAMLESS_LOGOUT_SV="test"
WEBVIEW_ENCRYPTION_KEY="test"
BOOKING_ENCRYPTION_KEY="test"
+GOOGLE_STATIC_MAP_KEY="test"
+GOOGLE_STATIC_MAP_SIGNATURE_SECRET="test"
+GOOGLE_STATIC_MAP_ID="test"
+GOOGLE_DYNAMIC_MAP_ID="test"
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/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..a049e90c4
--- /dev/null
+++ b/components/ContentType/HotelPage/Map/DynamicMap/Map/index.tsx
@@ -0,0 +1,138 @@
+"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,
+ mapId,
+ onActivePoiChange,
+}: MapContentProps) {
+ const intl = useIntl()
+ const { closeDynamicMap } = useHotelPageStore()
+ const map = useMap()
+
+ const mapOptions: MapProps = {
+ defaultZoom: 14,
+ defaultCenter: coordinates,
+ disableDefaultUI: true,
+ clickableIcons: false,
+ mapId,
+ }
+
+ 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 (
+
+
+
+
+
+
+
+ )
+}
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..785949be5 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,14 @@ export default function DynamicMap({
apiKey,
hotelName,
coordinates,
+ pointsOfInterest,
+ mapId,
}: 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 +62,19 @@ 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
+
+ ))}
+
(() => {
- 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..89f935e62 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,15 @@ 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 intl = await getIntl()
+ const lang = getLang()
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
+ const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
const hotelData = await serverClient().hotel.get({
include: ["RoomCategories"],
})
@@ -36,10 +44,12 @@ export default async function HotelPage() {
hotelImages,
roomCategories,
activitiesCard,
+ pointsOfInterest,
} = hotelData
const facilities = [...MOCK_FACILITIES]
activitiesCard && facilities.push(setActivityCard(activitiesCard))
+ const topThreePois = pointsOfInterest.slice(0, 3)
const coordinates = {
lat: hotelLocation.latitude,
@@ -61,6 +71,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 */}
+
@@ -70,17 +125,18 @@ export default async function HotelPage() {
<>
>
) : null}
-
)
}
diff --git a/components/Icons/Cultural.tsx b/components/Icons/Cultural.tsx
new file mode 100644
index 000000000..ede6800b6
--- /dev/null
+++ b/components/Icons/Cultural.tsx
@@ -0,0 +1,40 @@
+import { iconVariants } from "./variants"
+
+import type { IconProps } from "@/types/components/icon"
+
+export default function CulturalIcon({
+ className,
+ color,
+ ...props
+}: IconProps) {
+ const classNames = iconVariants({ className, color })
+ return (
+
+ )
+}
diff --git a/components/Icons/Museum.tsx b/components/Icons/Museum.tsx
new file mode 100644
index 000000000..d9a774908
--- /dev/null
+++ b/components/Icons/Museum.tsx
@@ -0,0 +1,36 @@
+import { iconVariants } from "./variants"
+
+import type { IconProps } from "@/types/components/icon"
+
+export default function MuseumIcon({ className, color, ...props }: IconProps) {
+ const classNames = iconVariants({ className, color })
+ return (
+
+ )
+}
diff --git a/components/Icons/Shopping.tsx b/components/Icons/Shopping.tsx
new file mode 100644
index 000000000..87b1da6b2
--- /dev/null
+++ b/components/Icons/Shopping.tsx
@@ -0,0 +1,40 @@
+import { iconVariants } from "./variants"
+
+import type { IconProps } from "@/types/components/icon"
+
+export default function ShoppingIcon({
+ className,
+ color,
+ ...props
+}: IconProps) {
+ const classNames = iconVariants({ className, color })
+ return (
+
+ )
+}
diff --git a/components/Icons/StarFilled.tsx b/components/Icons/StarFilled.tsx
new file mode 100644
index 000000000..4e96f47d9
--- /dev/null
+++ b/components/Icons/StarFilled.tsx
@@ -0,0 +1,40 @@
+import { iconVariants } from "./variants"
+
+import type { IconProps } from "@/types/components/icon"
+
+export default function StarFilledIcon({
+ className,
+ color,
+ ...props
+}: IconProps) {
+ const classNames = iconVariants({ className, color })
+ return (
+
+ )
+}
diff --git a/components/Icons/Train.tsx b/components/Icons/Train.tsx
new file mode 100644
index 000000000..79fae85f4
--- /dev/null
+++ b/components/Icons/Train.tsx
@@ -0,0 +1,36 @@
+import { iconVariants } from "./variants"
+
+import type { IconProps } from "@/types/components/icon"
+
+export default function TrainIcon({ className, color, ...props }: IconProps) {
+ const classNames = iconVariants({ className, color })
+ return (
+
+ )
+}
diff --git a/components/Icons/get-icon-by-icon-name.ts b/components/Icons/get-icon-by-icon-name.ts
index 9a0a87697..117971b3c 100644
--- a/components/Icons/get-icon-by-icon-name.ts
+++ b/components/Icons/get-icon-by-icon-name.ts
@@ -22,6 +22,7 @@ import {
CoffeeIcon,
ConciergeIcon,
CrossCircle,
+ CulturalIcon,
DoorOpenIcon,
ElectricBikeIcon,
EmailIcon,
@@ -35,6 +36,7 @@ import {
LockIcon,
MapIcon,
MinusIcon,
+ MuseumIcon,
ParkingIcon,
People2Icon,
PersonIcon,
@@ -46,6 +48,9 @@ import {
SaunaIcon,
SearchIcon,
ServiceIcon,
+ ShoppingIcon,
+ StarFilledIcon,
+ TrainIcon,
TshirtWashIcon,
WarningTriangle,
WifiIcon,
@@ -91,6 +96,8 @@ export function getIconByIconName(icon?: IconName): FC | null {
return CoffeeIcon
case IconName.Concierge:
return ConciergeIcon
+ case IconName.Cultural:
+ return CulturalIcon
case IconName.DoorOpen:
return DoorOpenIcon
case IconName.ElectricBike:
@@ -121,6 +128,8 @@ export function getIconByIconName(icon?: IconName): FC | null {
return MapIcon
case IconName.Minus:
return MinusIcon
+ case IconName.Museum:
+ return MuseumIcon
case IconName.Parking:
return ParkingIcon
case IconName.Person:
@@ -143,6 +152,12 @@ export function getIconByIconName(icon?: IconName): FC | null {
return SearchIcon
case IconName.Service:
return ServiceIcon
+ case IconName.Shopping:
+ return ShoppingIcon
+ case IconName.StarFilled:
+ return StarFilledIcon
+ case IconName.Train:
+ return TrainIcon
case IconName.Tripadvisor:
return TripAdvisorIcon
case IconName.TshirtWash:
diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx
index 18538aa5d..5d09f7d4e 100644
--- a/components/Icons/index.tsx
+++ b/components/Icons/index.tsx
@@ -17,6 +17,7 @@ export { default as CoffeeIcon } from "./Coffee"
export { default as ConciergeIcon } from "./Concierge"
export { default as CreditCard } from "./CreditCard"
export { default as CrossCircle } from "./CrossCircle"
+export { default as CulturalIcon } from "./Cultural"
export { default as Delete } from "./Delete"
export { default as DoorOpenIcon } from "./DoorOpen"
export { default as ElectricBikeIcon } from "./ElectricBike"
@@ -31,6 +32,7 @@ export { default as LocationIcon } from "./Location"
export { default as LockIcon } from "./Lock"
export { default as MapIcon } from "./Map"
export { default as MinusIcon } from "./Minus"
+export { default as MuseumIcon } from "./Museum"
export { default as ParkingIcon } from "./Parking"
export { default as People2Icon } from "./People2"
export { default as PersonIcon } from "./Person"
@@ -44,6 +46,9 @@ export { default as SaunaIcon } from "./Sauna"
export { default as ScandicLogoIcon } from "./ScandicLogo"
export { default as SearchIcon } from "./Search"
export { default as ServiceIcon } from "./Service"
+export { default as ShoppingIcon } from "./Shopping"
+export { default as StarFilledIcon } from "./StarFilled"
+export { default as TrainIcon } from "./Train"
export { default as TshirtWashIcon } from "./TshirtWash"
export { default as WarningTriangle } from "./WarningTriangle"
export { default as WifiIcon } from "./Wifi"
diff --git a/components/Maps/Markers/Poi/index.tsx b/components/Maps/Markers/Poi/index.tsx
new file mode 100644
index 000000000..db67af80b
--- /dev/null
+++ b/components/Maps/Markers/Poi/index.tsx
@@ -0,0 +1,27 @@
+import { getIconByIconName } from "@/components/Icons/get-icon-by-icon-name"
+
+import { getCategoryIconName } from "../utils"
+import { poiVariants } from "./variants"
+
+import type { PoiMarkerProps } from "@/types/components/maps/poiMarker"
+
+export default function PoiMarker({
+ category,
+ skipBackground,
+ size = 16,
+ className = "",
+}: PoiMarkerProps) {
+ const iconName = getCategoryIconName(category)
+ const Icon = iconName ? getIconByIconName(iconName) : null
+ const classNames = poiVariants({ category, skipBackground, className })
+
+ return Icon ? (
+
+
+
+ ) : null
+}
diff --git a/components/Maps/Markers/Poi/poi.module.css b/components/Maps/Markers/Poi/poi.module.css
new file mode 100644
index 000000000..e89b8f702
--- /dev/null
+++ b/components/Maps/Markers/Poi/poi.module.css
@@ -0,0 +1,52 @@
+/* 2024-09-18: At the moment, the background-colors for the poi marker is unknown.
+This will be handled later. */
+
+.icon {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ padding: var(--Spacing-x-half);
+ border-radius: var(--Corner-radius-Rounded);
+ background-color: var(--Scandic-Beige-90);
+}
+.airport,
+.amusementPark,
+.busTerminal,
+.fair,
+.hospital,
+.hotel,
+.marketingCity {
+}
+
+.museum {
+ background: var(--Base-Interactive-Surface-Secondary-normal);
+}
+.nearbyCompanies,
+.parkingGarage {
+}
+
+.restaurant {
+ background: var(--Scandic-Peach-50);
+}
+
+.shopping {
+ background: var(--Base-Interactive-Surface-Primary-normal);
+}
+.sports,
+.theatre {
+}
+
+.tourist {
+ background: var(--Scandic-Yellow-60);
+}
+
+.transportations {
+ background: var(--Base-Interactive-Surface-Tertiary-normal);
+}
+.zoo {
+}
+
+.icon.transparent {
+ background: transparent;
+ padding: 0;
+}
diff --git a/components/Maps/Markers/Poi/variants.ts b/components/Maps/Markers/Poi/variants.ts
new file mode 100644
index 000000000..6eed0ae58
--- /dev/null
+++ b/components/Maps/Markers/Poi/variants.ts
@@ -0,0 +1,34 @@
+import { cva } from "class-variance-authority"
+
+import styles from "./poi.module.css"
+
+export const poiVariants = cva(styles.icon, {
+ variants: {
+ category: {
+ Airport: styles.airport,
+ "Amusement park": styles.amusementPark,
+ "Bus terminal": styles.busTerminal,
+ Fair: styles.fair,
+ Hospital: styles.hospital,
+ Hotel: styles.hotel,
+ "Marketing city": styles.marketingCity,
+ Museum: styles.museum,
+ "Nearby companies": styles.nearbyCompanies,
+ "Parking / Garage": styles.parkingGarage,
+ Restaurant: styles.restaurant,
+ Shopping: styles.shopping,
+ Sports: styles.sports,
+ Theatre: styles.theatre,
+ Tourist: styles.tourist,
+ Transportations: styles.transportations,
+ Zoo: styles.zoo,
+ },
+ skipBackground: {
+ true: styles.transparent,
+ false: "",
+ },
+ },
+ defaultVariants: {
+ skipBackground: false,
+ },
+})
diff --git a/components/ContentType/HotelPage/Map/Markers/Scandic.tsx b/components/Maps/Markers/Scandic.tsx
similarity index 100%
rename from components/ContentType/HotelPage/Map/Markers/Scandic.tsx
rename to components/Maps/Markers/Scandic.tsx
diff --git a/components/Maps/Markers/utils.ts b/components/Maps/Markers/utils.ts
new file mode 100644
index 000000000..bf3e5ec38
--- /dev/null
+++ b/components/Maps/Markers/utils.ts
@@ -0,0 +1,21 @@
+import { IconName } from "@/types/components/icon"
+import type { PointOfInterestCategory } from "@/types/hotel"
+
+/* 2024-09-18: At the moment, the icons for the different categories is unknown.
+This will be handled later. */
+export function getCategoryIconName(category?: PointOfInterestCategory | null) {
+ switch (category) {
+ case "Transportations":
+ return IconName.Train
+ case "Shopping":
+ return IconName.Shopping
+ case "Museum":
+ return IconName.Museum
+ case "Tourist":
+ return IconName.Cultural
+ case "Restaurant":
+ return IconName.Restaurant
+ default:
+ return IconName.StarFilled
+ }
+}
diff --git a/components/Maps/StaticMap/index.tsx b/components/Maps/StaticMap/index.tsx
index 170865c0a..52f7dd7b3 100644
--- a/components/Maps/StaticMap/index.tsx
+++ b/components/Maps/StaticMap/index.tsx
@@ -13,6 +13,7 @@ export default function StaticMap({
zoomLevel = 14,
mapType = "roadmap",
altText,
+ mapId,
}: StaticMapProps) {
const key = env.GOOGLE_STATIC_MAP_KEY
const secret = env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET
@@ -27,6 +28,11 @@ export default function StaticMap({
const url = new URL(
`${baseUrl}?center=${center}&zoom=${zoomLevel}&size=${width}x${height}&maptype=${mapType}&key=${key}`
)
+
+ if (mapId) {
+ url.searchParams.append("map_id", mapId)
+ }
+
const src = getUrlWithSignature(url, secret)
return
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 && (
- )}
+ )} */}
)
}
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 (
-
-
+
+
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
}
diff --git a/constants/poiCategories.ts b/constants/poiCategories.ts
new file mode 100644
index 000000000..6153414e7
--- /dev/null
+++ b/constants/poiCategories.ts
@@ -0,0 +1,19 @@
+export enum PoiCategories {
+ "Airport" = "airport",
+ "Amusement park" = "amusementPark",
+ "Bus terminal" = "busTerminal",
+ "Fair" = "fair",
+ "Hospital" = "hospital",
+ "Hotel" = "hotel",
+ "Marketing city" = "marketingCity",
+ "Museum" = "museum",
+ "Nearby companies" = "nearbyCompanies",
+ "Parking / Garage" = "parkingGarage",
+ "Restaurant" = "restaurant",
+ "Shopping" = "shopping",
+ "Sports" = "sports",
+ "Theatre" = "theatre",
+ "Tourist" = "tourist",
+ "Transportations" = "transportations",
+ "Zoo" = "zoo",
+}
diff --git a/env/server.ts b/env/server.ts
index 58f98067b..663de1ad1 100644
--- a/env/server.ts
+++ b/env/server.ts
@@ -61,8 +61,10 @@ export const env = createEnv({
SEAMLESS_LOGOUT_SV: z.string(),
WEBVIEW_ENCRYPTION_KEY: z.string(),
BOOKING_ENCRYPTION_KEY: z.string(),
- GOOGLE_STATIC_MAP_KEY: z.string().optional(),
- GOOGLE_STATIC_MAP_SIGNATURE_SECRET: z.string().optional(),
+ GOOGLE_STATIC_MAP_KEY: z.string(),
+ GOOGLE_STATIC_MAP_SIGNATURE_SECRET: z.string(),
+ GOOGLE_DYNAMIC_MAP_ID: z.string(),
+ GOOGLE_STATIC_MAP_ID: z.string(),
},
emptyStringAsUndefined: true,
runtimeEnv: {
@@ -113,5 +115,7 @@ export const env = createEnv({
GOOGLE_STATIC_MAP_KEY: process.env.GOOGLE_STATIC_MAP_KEY,
GOOGLE_STATIC_MAP_SIGNATURE_SECRET:
process.env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET,
+ GOOGLE_STATIC_MAP_ID: process.env.GOOGLE_STATIC_MAP_ID,
+ GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID,
},
})
diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json
index 7b6206a20..367a43724 100644
--- a/i18n/dictionaries/da.json
+++ b/i18n/dictionaries/da.json
@@ -4,8 +4,10 @@
"Add code": "Tilføj kode",
"Add new card": "Tilføj nyt kort",
"Address": "Adresse",
+ "Airport": "Lufthavn",
"Already a friend?": "Allerede en ven?",
"Amenities": "Faciliteter",
+ "Amusement park": "Forlystelsespark",
"An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.",
"An error occurred when trying to update profile.": "Der opstod en fejl under forsøg på at opdatere profilen.",
"Any changes you've made will be lost.": "Alle ændringer, du har foretaget, går tabt.",
@@ -26,6 +28,7 @@
"Breakfast": "Morgenmad",
"Breakfast excluded": "Morgenmad ikke inkluderet",
"Breakfast included": "Morgenmad inkluderet",
+ "Bus terminal": "Busstation",
"by": "inden",
"Cancel": "Afbestille",
"characters": "tegn",
@@ -66,6 +69,7 @@
"Explore nearby": "Udforsk i nærheden",
"Extras to your booking": "Tillæg til din booking",
"Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.",
+ "Fair": "Messe",
"Find booking": "Find booking",
"Find hotels": "Find hotel",
"Flexibility": "Fleksibilitet",
@@ -78,6 +82,7 @@
"Go back to overview": "Gå tilbage til oversigten",
"Hi": "Hei",
"Highest level": "Højeste niveau",
+ "Hospital": "Hospital",
"Hotel": "Hotel",
"Hotel facilities": "Hotel faciliteter",
"Hotel surroundings": "Hotel omgivelser",
@@ -104,6 +109,7 @@
"Manage preferences": "Administrer præferencer",
"Map": "Kort",
"Map of HOTEL_NAME": "Map of {hotelName}",
+ "Marketing city": "Marketing by",
"Meetings & Conferences": "Møder & Konferencer",
"Member price": "Medlemspris",
"Member price from": "Medlemspris fra",
@@ -114,6 +120,7 @@
"Menu": "Menu",
"Modify": "Ændre",
"Month": "Måned",
+ "Museum": "Museum",
"My communication preferences": "Mine kommunikationspræferencer",
"My membership cards": "Mine medlemskort",
"My pages": "Mine sider",
@@ -121,6 +128,7 @@
"My payment cards": "Mine betalingskort",
"My wishes": "Mine ønsker",
"Nearby": "I nærheden",
+ "Nearby companies": "Nærliggende virksomheder",
"New password": "Nyt kodeord",
"Next": "Næste",
"next level:": "Næste niveau:",
@@ -142,6 +150,7 @@
"Open my pages menu": "Åbn mine sider menuen",
"or": "eller",
"Overview": "Oversigt",
+ "Parking / Garage": "Parkering / Garage",
"Password": "Adgangskode",
"Pay later": "Betal senere",
"Pay now": "Betal nu",
@@ -150,8 +159,8 @@
"Phone is required": "Telefonnummer er påkrævet",
"Phone number": "Telefonnummer",
"Please enter a valid phone number": "Indtast venligst et gyldigt telefonnummer",
- "points": "Point",
"Points": "Point",
+ "points": "Point",
"Points being calculated": "Point udregnes",
"Points earned prior to May 1, 2021": "Point optjent inden 1. maj 2021",
"Points may take up to 10 days to be displayed.": "Det kan tage op til 10 dage at få vist point.",
@@ -162,6 +171,7 @@
"Read more": "Læs mere",
"Read more about the hotel": "Læs mere om hotellet",
"Remove card from member profile": "Fjern kortet fra medlemsprofilen",
+ "Restaurant": "Restaurant",
"Restaurant & Bar": "Restaurant & Bar",
"Retype new password": "Gentag den nye adgangskode",
"Room & Terms": "Værelse & Vilkår",
@@ -179,6 +189,7 @@
"Select date of birth": "Vælg fødselsdato",
"Select language": "Vælg sprog",
"Select your language": "Vælg dit sprog",
+ "Shopping": "Shopping",
"Show all amenities": "Vis alle faciliteter",
"Show less": "Vis mindre",
"Show map": "Vis kort",
@@ -190,18 +201,22 @@
"Something went wrong!": "Noget gik galt!",
"special character": "speciel karakter",
"spendable points expiring by": "{points} Brugbare point udløber den {date}",
+ "Sports": "Sport",
"Standard price": "Standardpris",
"Street": "Gade",
"Successfully updated profile!": "Profilen er opdateret med succes!",
"Summary": "Opsummering",
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortæl os, hvilke oplysninger og opdateringer du gerne vil modtage, og hvordan, ved at klikke på linket nedenfor.",
"Thank you": "Tak",
+ "Theatre": "Teater",
"There are no transactions to display": "Der er ingen transaktioner at vise",
"Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}",
"to": "til",
"Total Points": "Samlet antal point",
+ "Tourist": "Turist",
"Transaction date": "Overførselsdato",
"Transactions": "Transaktioner",
+ "Transportations": "Transport",
"Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)",
"TUI Points": "TUI Points",
"Type of bed": "Sengtype",
@@ -239,6 +254,7 @@
"Your level": "Dit niveau",
"Your points to spend": "Dine brugbare point",
"Zip code": "Postnummer",
+ "Zoo": "Zoo",
"Zoom in": "Zoom ind",
"Zoom out": "Zoom ud"
}
diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json
index 6e1a4b147..70d527064 100644
--- a/i18n/dictionaries/de.json
+++ b/i18n/dictionaries/de.json
@@ -3,8 +3,10 @@
"Add code": "Code hinzufügen",
"Add new card": "Neue Karte hinzufügen",
"Address": "Adresse",
+ "Airport": "Flughafen",
"Already a friend?": "Sind wir schon Freunde?",
"Amenities": "Annehmlichkeiten",
+ "Amusement park": "Vergnügungspark",
"An error occurred when adding a credit card, please try again later.": "Beim Hinzufügen einer Kreditkarte ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
"An error occurred when trying to update profile.": "Beim Versuch, das Profil zu aktualisieren, ist ein Fehler aufgetreten.",
"Any changes you've made will be lost.": "Alle Änderungen, die Sie vorgenommen haben, gehen verloren.",
@@ -24,6 +26,7 @@
"Breakfast": "Frühstück",
"Breakfast excluded": "Frühstück nicht inbegriffen",
"Breakfast included": "Frühstück inbegriffen",
+ "Bus terminal": "Busbahnhof",
"by": "bis",
"Cancel": "Stornieren",
"characters": "figuren",
@@ -64,6 +67,7 @@
"Explore nearby": "Erkunden Sie die Umgebung",
"Extras to your booking": "Extras zu Ihrer Buchung",
"Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.",
+ "Fair": "Messe",
"Find booking": "Buchung finden",
"Find hotels": "Hotels finden",
"Flexibility": "Flexibilität",
@@ -76,6 +80,7 @@
"Go back to overview": "Zurück zur Übersicht",
"Hi": "Hallo",
"Highest level": "Höchstes Level",
+ "Hospital": "Krankenhaus",
"Hotel": "Hotel",
"Hotel facilities": "Hotel-Infos",
"Hotel surroundings": "Umgebung des Hotels",
@@ -102,6 +107,7 @@
"Manage preferences": "Verwalten von Voreinstellungen",
"Map": "Karte",
"Map of HOTEL_NAME": "Map of {hotelName}",
+ "Marketing city": "Marketingstadt",
"Member price": "Mitgliederpreis",
"Member price from": "Mitgliederpreis ab",
"Members": "Mitglieder",
@@ -111,6 +117,7 @@
"Menu": "Menu",
"Modify": "Ändern",
"Month": "Monat",
+ "Museum": "Museum",
"My communication preferences": "Meine Kommunikationseinstellungen",
"My membership cards": "Meine Mitgliedskarten",
"My pages": "Meine Seiten",
@@ -118,6 +125,7 @@
"My payment cards": "Meine Zahlungskarten",
"My wishes": "Meine Wünsche",
"Nearby": "In der Nähe",
+ "Nearby companies": "Nahe gelegene Unternehmen",
"New password": "Neues Kennwort",
"Next": "Nächste",
"next level:": "Nächstes Level:",
@@ -138,6 +146,7 @@
"Open menu": "Menü öffnen",
"Open my pages menu": "Meine Seiten Menü öffnen",
"or": "oder",
+ "Parking / Garage": "Parken / Garage",
"Password": "Passwort",
"Pay later": "Später bezahlen",
"Pay now": "Jetzt bezahlen",
@@ -146,8 +155,8 @@
"Phone is required": "Telefon ist erforderlich",
"Phone number": "Telefonnummer",
"Please enter a valid phone number": "Bitte geben Sie eine gültige Telefonnummer ein",
- "points": "Punkte",
"Points": "Punkte",
+ "points": "Punkte",
"Points being calculated": "Punkte werden berechnet",
"Points earned prior to May 1, 2021": "Zusammengeführte Punkte vor dem 1. Mai 2021",
"Points may take up to 10 days to be displayed.": "Es kann bis zu 10 Tage dauern, bis Punkte angezeigt werden.",
@@ -158,6 +167,7 @@
"Read more": "Mehr lesen",
"Read more about the hotel": "Lesen Sie mehr über das Hotel",
"Remove card from member profile": "Karte aus dem Mitgliedsprofil entfernen",
+ "Restaurant": "Restaurant",
"Retype new password": "Neues Passwort erneut eingeben",
"Room & Terms": "Zimmer & Bedingungen",
"Room facilities": "Zimmerausstattung",
@@ -173,6 +183,7 @@
"Select date of birth": "Geburtsdatum auswählen",
"Select language": "Sprache auswählen",
"Select your language": "Wählen Sie Ihre Sprache",
+ "Shopping": "Einkaufen",
"Show all amenities": "Alle Annehmlichkeiten anzeigen",
"Show less": "Weniger anzeigen",
"Show map": "Karte anzeigen",
@@ -184,18 +195,22 @@
"Something went wrong!": "Etwas ist schief gelaufen!",
"special character": "sonderzeichen",
"spendable points expiring by": "{points} Einlösbare punkte verfallen bis zum {date}",
+ "Sports": "Sport",
"Standard price": "Standardpreis",
"Street": "Straße",
"Successfully updated profile!": "Profil erfolgreich aktualisiert!",
"Summary": "Zusammenfassung",
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Teilen Sie uns mit, welche Informationen und Updates Sie wie erhalten möchten, indem Sie auf den unten stehenden Link klicken.",
"Thank you": "Danke",
+ "Theatre": "Theater",
"There are no transactions to display": "Es sind keine Transaktionen zum Anzeigen vorhanden",
"Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}",
"to": "zu",
"Total Points": "Gesamtpunktzahl",
+ "Tourist": "Tourist",
"Transaction date": "Transaktionsdatum",
"Transactions": "Transaktionen",
+ "Transportations": "Transportmittel",
"Tripadvisor reviews": "{rating} ({count} Bewertungen auf Tripadvisor)",
"TUI Points": "TUI Points",
"Type of bed": "Bettentyp",
@@ -232,6 +247,7 @@
"Your level": "Dein level",
"Your points to spend": "Meine Punkte",
"Zip code": "PLZ",
+ "Zoo": "Zoo",
"Zoom in": "Vergrößern",
"Zoom out": "Verkleinern"
}
diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json
index bd73a9f0a..8a3937452 100644
--- a/i18n/dictionaries/en.json
+++ b/i18n/dictionaries/en.json
@@ -4,8 +4,10 @@
"Add code": "Add code",
"Add new card": "Add new card",
"Address": "Address",
+ "Airport": "Airport",
"Already a friend?": "Already a friend?",
"Amenities": "Amenities",
+ "Amusement park": "Amusement park",
"An error occurred when adding a credit card, please try again later.": "An error occurred when adding a credit card, please try again later.",
"An error occurred when trying to update profile.": "An error occurred when trying to update profile.",
"Any changes you've made will be lost.": "Any changes you've made will be lost.",
@@ -25,6 +27,7 @@
"Breakfast": "Breakfast",
"Breakfast excluded": "Breakfast excluded",
"Breakfast included": "Breakfast included",
+ "Bus terminal": "Bus terminal",
"by": "by",
"Cancel": "Cancel",
"characters": "characters",
@@ -66,6 +69,7 @@
"Explore nearby": "Explore nearby",
"Extras to your booking": "Extras to your booking",
"Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.",
+ "Fair": "Fair",
"FAQ": "FAQ",
"Find booking": "Find booking",
"Find hotels": "Find hotels",
@@ -79,6 +83,7 @@
"Go back to overview": "Go back to overview",
"Hi": "Hi",
"Highest level": "Highest level",
+ "Hospital": "Hospital",
"Hotel": "Hotel",
"Hotel facilities": "Hotel facilities",
"Hotel surroundings": "Hotel surroundings",
@@ -108,6 +113,7 @@
"Manage preferences": "Manage preferences",
"Map": "Map",
"Map of HOTEL_NAME": "Map of {hotelName}",
+ "Marketing city": "Marketing city",
"Meetings & Conferences": "Meetings & Conferences",
"Member price": "Member price",
"Member price from": "Member price from",
@@ -118,6 +124,7 @@
"Menu": "Menu",
"Modify": "Modify",
"Month": "Month",
+ "Museum": "Museum",
"My communication preferences": "My communication preferences",
"My membership cards": "My membership cards",
"My pages": "My pages",
@@ -125,6 +132,7 @@
"My payment cards": "My payment cards",
"My wishes": "My wishes",
"Nearby": "Nearby",
+ "Nearby companies": "Nearby companies",
"New password": "New password",
"Next": "Next",
"next level:": "next level:",
@@ -146,6 +154,7 @@
"Open my pages menu": "Open my pages menu",
"or": "or",
"Overview": "Overview",
+ "Parking / Garage": "Parking / Garage",
"Password": "Password",
"Pay later": "Pay later",
"Pay now": "Pay now",
@@ -154,8 +163,8 @@
"Phone is required": "Phone is required",
"Phone number": "Phone number",
"Please enter a valid phone number": "Please enter a valid phone number",
- "Points": "Points",
"points": "Points",
+ "Points": "Points",
"Points being calculated": "Points being calculated",
"Points earned prior to May 1, 2021": "Points earned prior to May 1, 2021",
"Points may take up to 10 days to be displayed.": "Points may take up to 10 days to be displayed.",
@@ -166,6 +175,7 @@
"Read more": "Read more",
"Read more about the hotel": "Read more about the hotel",
"Remove card from member profile": "Remove card from member profile",
+ "Restaurant": "Restaurant",
"Restaurant & Bar": "Restaurant & Bar",
"Retype new password": "Retype new password",
"Room & Terms": "Room & Terms",
@@ -184,6 +194,7 @@
"Select date of birth": "Select date of birth",
"Select language": "Select language",
"Select your language": "Select your language",
+ "Shopping": "Shopping",
"Show all amenities": "Show all amenities",
"Show less": "Show less",
"Show map": "Show map",
@@ -195,18 +206,22 @@
"Something went wrong!": "Something went wrong!",
"special character": "special character",
"spendable points expiring by": "{points} spendable points expiring by {date}",
+ "Sports": "Sports",
"Standard price": "Standard price",
"Street": "Street",
"Successfully updated profile!": "Successfully updated profile!",
"Summary": "Summary",
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Tell us what information and updates you'd like to receive, and how, by clicking the link below.",
"Thank you": "Thank you",
+ "Theatre": "Theatre",
"There are no transactions to display": "There are no transactions to display",
"Things nearby HOTEL_NAME": "Things nearby {hotelName}",
"to": "to",
"Total Points": "Total Points",
+ "Tourist": "Tourist",
"Transaction date": "Transaction date",
"Transactions": "Transactions",
+ "Transportations": "Transportations",
"Tripadvisor reviews": "{rating} ({count} reviews on Tripadvisor)",
"TUI Points": "TUI Points",
"Type of bed": "Type of bed",
@@ -244,6 +259,7 @@
"Your level": "Your level",
"Your points to spend": "Your points to spend",
"Zip code": "Zip code",
+ "Zoo": "Zoo",
"Zoom in": "Zoom in",
"Zoom out": "Zoom out"
}
diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json
index a97dea3fe..075552ddf 100644
--- a/i18n/dictionaries/fi.json
+++ b/i18n/dictionaries/fi.json
@@ -4,8 +4,10 @@
"Add code": "Lisää koodi",
"Add new card": "Lisää uusi kortti",
"Address": "Osoite",
+ "Airport": "Lentokenttä",
"Already a friend?": "Oletko jo ystävä?",
"Amenities": "Mukavuudet",
+ "Amusement park": "Huvipuisto",
"An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.",
"An error occurred when trying to update profile.": "Profiilia päivitettäessä tapahtui virhe.",
"Any changes you've made will be lost.": "Kaikki tekemäsi muutokset menetetään.",
@@ -25,6 +27,7 @@
"Breakfast": "Aamiainen",
"Breakfast excluded": "Aamiainen ei sisälly",
"Breakfast included": "Aamiainen sisältyy",
+ "Bus terminal": "Bussiasema",
"by": "mennessä",
"Cancel": "Peruuttaa",
"characters": "hahmoja",
@@ -65,6 +68,7 @@
"Explore nearby": "Tutustu lähialueeseen",
"Extras to your booking": "Varauksessa lisäpalveluita",
"Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.",
+ "Fair": "Messukeskus",
"Find booking": "Etsi varaus",
"Find hotels": "Etsi hotelleja",
"Flexibility": "Joustavuus",
@@ -77,6 +81,7 @@
"Go back to overview": "Palaa yleiskatsaukseen",
"Hi": "Hi",
"Highest level": "Korkein taso",
+ "Hospital": "Sairaala",
"Hotel": "Hotelli",
"Hotel facilities": "Hotellin palvelut",
"Hotel surroundings": "Hotellin ympäristö",
@@ -103,6 +108,7 @@
"Manage preferences": "Asetusten hallinta",
"Map": "Kartta",
"Map of HOTEL_NAME": "Map of {hotelName}",
+ "Marketing city": "Markkinointikaupunki",
"Meetings & Conferences": "Kokoukset & Konferenssit",
"Member price": "Jäsenhinta",
"Member price from": "Jäsenhinta alkaen",
@@ -113,6 +119,7 @@
"Menu": "Valikko",
"Modify": "Muokkaa",
"Month": "Kuukausi",
+ "Museum": "Museo",
"My communication preferences": "Viestintämieltymykseni",
"My membership cards": "Jäsenkorttini",
"My pages": "Omat sivut",
@@ -120,6 +127,7 @@
"My payment cards": "Minun maksukortit",
"My wishes": "Toiveeni",
"Nearby": "Lähistöllä",
+ "Nearby companies": "Läheiset yritykset",
"New password": "Uusi salasana",
"Next": "Seuraava",
"next level:": "pistettä tasolle:",
@@ -141,6 +149,7 @@
"Open my pages menu": "Avaa omat sivut -valikko",
"or": "tai",
"Overview": "Yleiskatsaus",
+ "Parking / Garage": "Pysäköinti / Autotalli",
"Password": "Salasana",
"Pay later": "Maksa myöhemmin",
"Pay now": "Maksa nyt",
@@ -149,8 +158,8 @@
"Phone is required": "Puhelin vaaditaan",
"Phone number": "Puhelinnumero",
"Please enter a valid phone number": "Ole hyvä ja näppäile voimassaoleva puhelinnumero",
- "points": "pistettä",
"Points": "Pisteet",
+ "points": "pistettä",
"Points being calculated": "Pisteitä lasketaan",
"Points earned prior to May 1, 2021": "Pisteet, jotka ansaittu ennen 1.5.2021",
"Points may take up to 10 days to be displayed.": "Pisteiden näyttäminen voi kestää jopa 10 päivää.",
@@ -161,6 +170,7 @@
"Read more": "Lue lisää",
"Read more about the hotel": "Lue lisää hotellista",
"Remove card from member profile": "Poista kortti jäsenprofiilista",
+ "Restaurant": "Ravintola",
"Restaurant & Bar": "Ravintola & Baari",
"Retype new password": "Kirjoita uusi salasana uudelleen",
"Room & Terms": "Huone & Ehdot",
@@ -178,6 +188,7 @@
"Select date of birth": "Valitse syntymäaika",
"Select language": "Valitse kieli",
"Select your language": "Valitse kieli",
+ "Shopping": "Ostokset",
"Show all amenities": "Näytä kaikki mukavuudet",
"Show less": "Näytä vähemmän",
"Show map": "Näytä kartta",
@@ -189,18 +200,22 @@
"Something went wrong!": "Jotain meni pieleen!",
"special character": "erikoishahmo",
"spendable points expiring by": "{points} pistettä vanhenee {date} mennessä",
+ "Sports": "Urheilu",
"Standard price": "Normaali hinta",
"Street": "Katu",
"Successfully updated profile!": "Profiilin päivitys onnistui!",
"Summary": "Yhteenveto",
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.",
"Thank you": "Kiitos",
+ "Theatre": "Teatteri",
"There are no transactions to display": "Näytettäviä tapahtumia ei ole",
"Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}",
"to": "to",
"Total Points": "Kokonaispisteet",
+ "Tourist": "Turisti",
"Transaction date": "Tapahtuman päivämäärä",
"Transactions": "Tapahtumat",
+ "Transportations": "Kuljetukset",
"Tripadvisor reviews": "{rating} ({count} arvostelua TripAdvisorissa)",
"TUI Points": "TUI Points",
"Type of bed": "Vuodetyyppi",
@@ -238,6 +253,7 @@
"Your level": "Tasosi",
"Your points to spend": "Käytettävissä olevat pisteesi",
"Zip code": "Postinumero",
+ "Zoo": "Eläintarha",
"Zoom in": "Lähennä",
"Zoom out": "Loitonna"
}
diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json
index 81ef12cc2..ea318036f 100644
--- a/i18n/dictionaries/no.json
+++ b/i18n/dictionaries/no.json
@@ -4,8 +4,10 @@
"Add code": "Legg til kode",
"Add new card": "Legg til nytt kort",
"Address": "Adresse",
+ "Airport": "Flyplass",
"Already a friend?": "Allerede Friend?",
"Amenities": "Fasiliteter",
+ "Amusement park": "Tivoli",
"An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.",
"An error occurred when trying to update profile.": "Det oppstod en feil under forsøk på å oppdatere profilen.",
"Any changes you've made will be lost.": "Eventuelle endringer du har gjort, går tapt.",
@@ -25,6 +27,7 @@
"Breakfast": "Frokost",
"Breakfast excluded": "Frokost ekskludert",
"Breakfast included": "Frokost inkludert",
+ "Bus terminal": "Bussterminal",
"by": "innen",
"Cancel": "Avbryt",
"characters": "tegn",
@@ -65,6 +68,7 @@
"Explore nearby": "Utforsk i nærheten",
"Extras to your booking": "Tilvalg til bestillingen din",
"Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.",
+ "Fair": "Messe",
"Find booking": "Finn booking",
"Find hotels": "Finn hotell",
"Flexibility": "Fleksibilitet",
@@ -77,6 +81,7 @@
"Go back to overview": "Gå tilbake til oversikten",
"Hi": "Hei",
"Highest level": "Høyeste nivå",
+ "Hospital": "Sykehus",
"Hotel": "Hotel",
"Hotel facilities": "Hotelfaciliteter",
"Hotel surroundings": "Hotellomgivelser",
@@ -103,6 +108,7 @@
"Manage preferences": "Administrer preferanser",
"Map": "Kart",
"Map of HOTEL_NAME": "Map of {hotelName}",
+ "Marketing city": "Markedsføringsby",
"Meetings & Conferences": "Møter & Konferanser",
"Member price": "Medlemspris",
"Member price from": "Medlemspris fra",
@@ -113,6 +119,7 @@
"Menu": "Menu",
"Modify": "Endre",
"Month": "Måned",
+ "Museum": "Museum",
"My communication preferences": "Mine kommunikasjonspreferanser",
"My membership cards": "Mine medlemskort",
"My pages": "Mine sider",
@@ -120,6 +127,7 @@
"My payment cards": "Mine betalingskort",
"My wishes": "Mine ønsker",
"Nearby": "I nærheten",
+ "Nearby companies": "Nærliggende selskaper",
"New password": "Nytt passord",
"Next": "Neste",
"next level:": "Neste nivå:",
@@ -141,6 +149,7 @@
"Open my pages menu": "Åpne mine sider menyen",
"or": "eller",
"Overview": "Oversikt",
+ "Parking / Garage": "Parkering / Garasje",
"Password": "Passord",
"Pay later": "Betal senere",
"Pay now": "Betal nå",
@@ -149,8 +158,8 @@
"Phone is required": "Telefon kreves",
"Phone number": "Telefonnummer",
"Please enter a valid phone number": "Vennligst oppgi et gyldig telefonnummer",
- "Points": "Poeng",
"points": "poeng",
+ "Points": "Poeng",
"Points being calculated": "Poeng beregnes",
"Points earned prior to May 1, 2021": "Opptjente poeng før 1. mai 2021",
"Points may take up to 10 days to be displayed.": "Det kan ta opptil 10 dager før poeng vises.",
@@ -161,6 +170,7 @@
"Read more": "Les mer",
"Read more about the hotel": "Les mer om hotellet",
"Remove card from member profile": "Fjern kortet fra medlemsprofilen",
+ "Restaurant": "Restaurant",
"Restaurant & Bar": "Restaurant & Bar",
"Retype new password": "Skriv inn nytt passord på nytt",
"Room & Terms": "Rom & Vilkår",
@@ -178,6 +188,7 @@
"Select date of birth": "Velg fødselsdato",
"Select language": "Velg språk",
"Select your language": "Velg språk",
+ "Shopping": "Shopping",
"Show all amenities": "Vis alle fasiliteter",
"Show less": "Vis mindre",
"Show map": "Vis kart",
@@ -189,18 +200,22 @@
"Something went wrong!": "Noe gikk galt!",
"special character": "spesiell karakter",
"spendable points expiring by": "{points} Brukbare poeng utløper innen {date}",
+ "Sports": "Sport",
"Standard price": "Standardpris",
"Street": "Gate",
"Successfully updated profile!": "Vellykket oppdatert profil!",
"Summary": "Sammendrag",
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Fortell oss hvilken informasjon og hvilke oppdateringer du ønsker å motta, og hvordan, ved å klikke på lenken nedenfor.",
"Thank you": "Takk",
+ "Theatre": "Teater",
"There are no transactions to display": "Det er ingen transaksjoner å vise",
"Things nearby HOTEL_NAME": "Ting i nærheten av {hotelName}",
"to": "til",
"Total Points": "Totale poeng",
+ "Tourist": "Turist",
"Transaction date": "Transaksjonsdato",
"Transactions": "Transaksjoner",
+ "Transportations": "Transport",
"Tripadvisor reviews": "{rating} ({count} anmeldelser på Tripadvisor)",
"TUI Points": "TUI Points",
"Type of bed": "Sengtype",
@@ -238,6 +253,7 @@
"Your level": "Ditt nivå",
"Your points to spend": "Dine brukbare poeng",
"Zip code": "Post kode",
+ "Zoo": "Dyrehage",
"Zoom in": "Zoom inn",
"Zoom out": "Zoom ut"
}
diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json
index 0cf45ea96..4a6d5c3eb 100644
--- a/i18n/dictionaries/sv.json
+++ b/i18n/dictionaries/sv.json
@@ -4,8 +4,10 @@
"Add code": "Lägg till kod",
"Add new card": "Lägg till nytt kort",
"Address": "Adress",
+ "Airport": "Flygplats",
"Already a friend?": "Är du redan en vän?",
"Amenities": "Bekvämligheter",
+ "Amusement park": "Nöjespark",
"An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.",
"An error occurred when trying to update profile.": "Ett fel uppstod när du försökte uppdatera profilen.",
"Any changes you've made will be lost.": "Alla ändringar du har gjort kommer att gå förlorade.",
@@ -25,6 +27,7 @@
"Breakfast": "Frukost",
"Breakfast excluded": "Frukost ingår ej",
"Breakfast included": "Frukost ingår",
+ "Bus terminal": "Bussterminal",
"by": "innan",
"Cancel": "Avbryt",
"characters": "tecken",
@@ -65,6 +68,7 @@
"Explore nearby": "Utforska i närheten",
"Extras to your booking": "Extra tillval till din bokning",
"Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.",
+ "Fair": "Mässa",
"Find booking": "Hitta bokning",
"Find hotels": "Hitta hotell",
"Flexibility": "Flexibilitet",
@@ -77,6 +81,7 @@
"Go back to overview": "Gå tillbaka till översikten",
"Hi": "Hej",
"Highest level": "Högsta nivå",
+ "Hospital": "Sjukhus",
"Hotel": "Hotell",
"Hotel facilities": "Hotellfaciliteter",
"Hotel surroundings": "Hotellomgivning",
@@ -105,6 +110,7 @@
"Manage preferences": "Hantera inställningar",
"Map": "Karta",
"Map of HOTEL_NAME": "Map of {hotelName}",
+ "Marketing city": "Marknadsföringsstad",
"Meetings & Conferences": "Möten & Konferenser",
"Member price": "Medlemspris",
"Member price from": "Medlemspris från",
@@ -115,6 +121,7 @@
"Menu": "Meny",
"Modify": "Ändra",
"Month": "Månad",
+ "Museum": "Museum",
"My communication preferences": "Mina kommunikationspreferenser",
"My membership cards": "Mina medlemskort",
"My pages": "Mina sidor",
@@ -122,6 +129,7 @@
"My payment cards": "Mina betalningskort",
"My wishes": "Mina önskningar",
"Nearby": "I närheten",
+ "Nearby companies": "Närliggande företag",
"New password": "Nytt lösenord",
"Next": "Nästa",
"next level:": "Nästa nivå:",
@@ -143,6 +151,7 @@
"Open my pages menu": "Öppna mina sidor menyn",
"or": "eller",
"Overview": "Översikt",
+ "Parking / Garage": "Parkering / Garage",
"Password": "Lösenord",
"Pay later": "Betala senare",
"Pay now": "Betala nu",
@@ -151,8 +160,8 @@
"Phone is required": "Telefonnummer är obligatorisk",
"Phone number": "Telefonnummer",
"Please enter a valid phone number": "Var vänlig och ange ett giltigt telefonnummer",
- "points": "poäng",
"Points": "Poäng",
+ "points": "poäng",
"Points being calculated": "Poäng beräknas",
"Points earned prior to May 1, 2021": "Intjänade poäng före den 1 maj 2021",
"Points may take up to 10 days to be displayed.": "Det kan ta upp till 10 dagar innan poäng visas.",
@@ -163,6 +172,7 @@
"Read more": "Läs mer",
"Read more about the hotel": "Läs mer om hotellet",
"Remove card from member profile": "Ta bort kortet från medlemsprofilen",
+ "Restaurant": "Restaurang",
"Restaurant & Bar": "Restaurang & Bar",
"Retype new password": "Upprepa nytt lösenord",
"Room & Terms": "Rum & Villkor",
@@ -181,6 +191,7 @@
"Select date of birth": "Välj födelsedatum",
"Select language": "Välj språk",
"Select your language": "Välj ditt språk",
+ "Shopping": "Shopping",
"Show all amenities": "Visa alla bekvämligheter",
"Show less": "Visa mindre",
"Show map": "Visa karta",
@@ -192,18 +203,22 @@
"Something went wrong!": "Något gick fel!",
"special character": "speciell karaktär",
"spendable points expiring by": "{points} poäng förfaller {date}",
+ "Sports": "Sport",
"Standard price": "Standardpris",
"Street": "Gata",
"Successfully updated profile!": "Profilen har uppdaterats framgångsrikt!",
"Summary": "Sammanfattning",
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Berätta för oss vilken information och vilka uppdateringar du vill få och hur genom att klicka på länken nedan.",
"Thank you": "Tack",
+ "Theatre": "Teater",
"There are no transactions to display": "Det finns inga transaktioner att visa",
"Things nearby HOTEL_NAME": "Saker i närheten av {hotelName}",
"to": "till",
"Total Points": "Poäng totalt",
+ "Tourist": "Turist",
"Transaction date": "Transaktionsdatum",
"Transactions": "Transaktioner",
+ "Transportations": "Transport",
"Tripadvisor reviews": "{rating} ({count} recensioner på Tripadvisor)",
"TUI Points": "TUI Points",
"Type of bed": "Sängtyp",
@@ -240,6 +255,7 @@
"Your level": "Din nivå",
"Your points to spend": "Dina spenderbara poäng",
"Zip code": "Postnummer",
+ "Zoo": "Djurpark",
"Zoom in": "Zooma in",
"Zoom out": "Zooma ut"
}
diff --git a/package-lock.json b/package-lock.json
index 092398c1b..0ec69dde2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,7 +27,7 @@
"@trpc/react-query": "^11.0.0-rc.467",
"@trpc/server": "^11.0.0-rc.467",
"@vercel/otel": "^1.9.1",
- "@vis.gl/react-google-maps": "^1.1.3",
+ "@vis.gl/react-google-maps": "^1.2.0",
"class-variance-authority": "^0.7.0",
"clean-deep": "^3.4.0",
"dayjs": "^1.11.10",
@@ -6656,9 +6656,9 @@
}
},
"node_modules/@vis.gl/react-google-maps": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.1.3.tgz",
- "integrity": "sha512-jsjj5OpL5AyQDSfzBHU+r5UGAuZxv/7mw9gM2dP4EhqRXkXBpwDKKlml47/4l0m1WL4fSEXLzv85Mrh1VhPiFA==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@vis.gl/react-google-maps/-/react-google-maps-1.2.0.tgz",
+ "integrity": "sha512-gKVE1Jb+FT+F8RGzFrsgB4GWbRq/vLJm2U5nMHiLJmRyaO6HcSfZJue8mUEDJCShsXE0ASphcoJxTQNrBhbFJg==",
"dependencies": {
"@types/google.maps": "^3.54.10",
"fast-deep-equal": "^3.1.3"
diff --git a/package.json b/package.json
index 51a08a525..d84e0fb31 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"@trpc/react-query": "^11.0.0-rc.467",
"@trpc/server": "^11.0.0-rc.467",
"@vercel/otel": "^1.9.1",
- "@vis.gl/react-google-maps": "^1.1.3",
+ "@vis.gl/react-google-maps": "^1.2.0",
"class-variance-authority": "^0.7.0",
"clean-deep": "^3.4.0",
"dayjs": "^1.11.10",
diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts
index 1eba75ea0..f894e9aab 100644
--- a/server/routers/hotels/output.ts
+++ b/server/routers/hotels/output.ts
@@ -216,16 +216,46 @@ const rewardNightSchema = z.object({
}),
})
-const pointsOfInterestSchema = z.object({
- name: z.string(),
- distance: z.number(),
- category: z.object({
+const poiCategories = z.enum([
+ "Airport",
+ "Amusement park",
+ "Bus terminal",
+ "Fair",
+ "Hospital",
+ "Hotel",
+ "Marketing city",
+ "Museum",
+ "Nearby companies",
+ "Parking / Garage",
+ "Restaurant",
+ "Shopping",
+ "Sports",
+ "Theatre",
+ "Tourist",
+ "Transportations",
+ "Zoo",
+])
+
+export const pointOfInterestSchema = z
+ .object({
name: z.string(),
- group: z.string(),
- }),
- location: locationSchema,
- isHighlighted: z.boolean(),
-})
+ distance: z.number(),
+ category: z.object({
+ name: poiCategories,
+ group: z.string(),
+ }),
+ location: locationSchema,
+ isHighlighted: z.boolean(),
+ })
+ .transform((poi) => ({
+ name: poi.name,
+ distance: poi.distance,
+ category: poi.category.name,
+ coordinates: {
+ lat: poi.location.latitude,
+ lng: poi.location.longitude,
+ },
+ }))
const parkingPricingSchema = z.object({
freeParking: z.boolean(),
@@ -454,7 +484,9 @@ export const getHotelDataSchema = z.object({
detailedFacilities: z.array(detailedFacilitySchema),
healthFacilities: z.array(healthFacilitySchema),
rewardNight: rewardNightSchema,
- pointsOfInterest: z.array(pointsOfInterestSchema),
+ pointsOfInterest: z
+ .array(pointOfInterestSchema)
+ .transform((pois) => pois.sort((a, b) => a.distance - b.distance)),
parking: z.array(parkingSchema),
specialNeedGroups: z.array(specialNeedGroupSchema),
socialMedia: socialMediaSchema,
diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts
index fe92fa109..ada0e96ca 100644
--- a/server/routers/hotels/query.ts
+++ b/server/routers/hotels/query.ts
@@ -251,6 +251,7 @@ export const hotelQueryRouter = router({
hotelRatings: hotelAttributes.ratings,
hotelDetailedFacilities: hotelAttributes.detailedFacilities,
hotelImages: images,
+ pointsOfInterest: hotelAttributes.pointsOfInterest,
roomCategories,
activitiesCard: activities,
}
diff --git a/server/routers/hotels/utils.ts b/server/routers/hotels/utils.ts
new file mode 100644
index 000000000..3dddae69a
--- /dev/null
+++ b/server/routers/hotels/utils.ts
@@ -0,0 +1,18 @@
+import { IconName } from "@/types/components/icon"
+
+export function getIconByPoiCategory(category: string) {
+ switch (category) {
+ case "Transportations":
+ return IconName.Train
+ case "Shopping":
+ return IconName.Shopping
+ case "Museum":
+ return IconName.Museum
+ case "Tourist":
+ return IconName.Cultural
+ case "Restaurant":
+ return IconName.Restaurant
+ default:
+ return null
+ }
+}
diff --git a/types/components/hotelPage/map/dynamicMap.ts b/types/components/hotelPage/map/dynamicMap.ts
index f3aaccf4a..7b8595cc9 100644
--- a/types/components/hotelPage/map/dynamicMap.ts
+++ b/types/components/hotelPage/map/dynamicMap.ts
@@ -1,7 +1,10 @@
+import type { PointOfInterest } from "@/types/hotel"
import type { Coordinates } from "../../maps/coordinates"
export interface DynamicMapProps {
apiKey: string
hotelName: string
coordinates: Coordinates
+ pointsOfInterest: PointOfInterest[]
+ mapId: string
}
diff --git a/types/components/hotelPage/map/dynamicMapContent.ts b/types/components/hotelPage/map/dynamicMapContent.ts
deleted file mode 100644
index ae22a95b2..000000000
--- a/types/components/hotelPage/map/dynamicMapContent.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import type { Coordinates } from "../../maps/coordinates"
-
-export interface DynamicMapContentProps {
- hotelName: string
- coordinates: Coordinates
-}
diff --git a/types/components/hotelPage/map/mapCard.ts b/types/components/hotelPage/map/mapCard.ts
index 0ea8678e0..1ead43544 100644
--- a/types/components/hotelPage/map/mapCard.ts
+++ b/types/components/hotelPage/map/mapCard.ts
@@ -1,3 +1,6 @@
+import type { PointOfInterest } from "@/types/hotel"
+
export interface MapCardProps {
hotelName: string
+ pois: PointOfInterest[]
}
diff --git a/types/components/hotelPage/map/mapContent.ts b/types/components/hotelPage/map/mapContent.ts
new file mode 100644
index 000000000..98e7b4c81
--- /dev/null
+++ b/types/components/hotelPage/map/mapContent.ts
@@ -0,0 +1,10 @@
+import type { Coordinates } from "@/types/components/maps/coordinates"
+import type { PointOfInterest } from "@/types/hotel"
+
+export interface MapContentProps {
+ coordinates: Coordinates
+ pointsOfInterest: PointOfInterest[]
+ activePoi: PointOfInterest["name"] | null
+ mapId: string
+ onActivePoiChange: (poi: PointOfInterest["name"] | null) => void
+}
diff --git a/types/components/hotelPage/map/sidebar.ts b/types/components/hotelPage/map/sidebar.ts
new file mode 100644
index 000000000..6bde9f7dc
--- /dev/null
+++ b/types/components/hotelPage/map/sidebar.ts
@@ -0,0 +1,8 @@
+import type { PointOfInterest } from "@/types/hotel"
+
+export interface SidebarProps {
+ hotelName: string
+ pointsOfInterest: PointOfInterest[]
+ activePoi: PointOfInterest["name"] | null
+ onActivePoiChange: (poi: PointOfInterest["name"] | null) => void
+}
diff --git a/types/components/hotelPage/roomCard.ts b/types/components/hotelPage/roomCard.ts
index 4a555c621..4d07f170c 100644
--- a/types/components/hotelPage/roomCard.ts
+++ b/types/components/hotelPage/roomCard.ts
@@ -1,4 +1,4 @@
-import { RoomData } from "@/types/hotel"
+import type { RoomData } from "@/types/hotel"
export interface RoomCardProps {
id: string
diff --git a/types/components/icon.ts b/types/components/icon.ts
index 781306998..8096ef0f7 100644
--- a/types/components/icon.ts
+++ b/types/components/icon.ts
@@ -25,6 +25,7 @@ export enum IconName {
CloseLarge = "CloseLarge",
Coffee = "Coffee",
Concierge = "Concierge",
+ Cultural = "Cultural",
DoorOpen = "DoorOpen",
ElectricBike = "ElectricBike",
Email = "Email",
@@ -40,6 +41,7 @@ export enum IconName {
Lock = "Lock",
Map = "Map",
Minus = "Minus",
+ Museum = "Museum",
Parking = "Parking",
Person = "Person",
People2 = "People2",
@@ -51,6 +53,9 @@ export enum IconName {
Sauna = "Sauna",
Search = "Search",
Service = "Service",
+ Shopping = "Shopping",
+ StarFilled = "StarFilled",
+ Train = "Train",
Tripadvisor = "Tripadvisor",
TshirtWash = "TshirtWash",
Wifi = "Wifi",
diff --git a/types/components/maps/poiMarker.ts b/types/components/maps/poiMarker.ts
new file mode 100644
index 000000000..89932fb51
--- /dev/null
+++ b/types/components/maps/poiMarker.ts
@@ -0,0 +1,8 @@
+import { poiVariants } from "@/components/Maps/Markers/Poi/variants"
+
+import type { VariantProps } from "class-variance-authority"
+
+export interface PoiMarkerProps extends VariantProps {
+ size?: number
+ className?: string
+}
diff --git a/types/components/maps/staticMap.ts b/types/components/maps/staticMap.ts
index 40a73776c..8519f4101 100644
--- a/types/components/maps/staticMap.ts
+++ b/types/components/maps/staticMap.ts
@@ -1,4 +1,4 @@
-import { Coordinates } from "./coordinates"
+import type { Coordinates } from "./coordinates"
export type StaticMapProps = {
city?: string
@@ -8,4 +8,5 @@ export type StaticMapProps = {
zoomLevel?: number
mapType?: "roadmap" | "satellite" | "terrain" | "hybrid"
altText: string
+ mapId?: string
}
diff --git a/types/hotel.ts b/types/hotel.ts
index f01ea7983..a2656f2b0 100644
--- a/types/hotel.ts
+++ b/types/hotel.ts
@@ -1,8 +1,8 @@
import { z } from "zod"
import {
- getAvailabilitySchema,
getHotelDataSchema,
+ pointOfInterestSchema,
roomSchema,
} from "@/server/routers/hotels/output"
@@ -18,3 +18,6 @@ export type HotelTripAdvisor =
| undefined
export type RoomData = z.infer
+
+export type PointOfInterest = z.output
+export type PointOfInterestCategory = PointOfInterest["category"]