diff --git a/components/Blocks/HotelListing/index.tsx b/components/Blocks/HotelListing/index.tsx
index b005aefb0..1c4694149 100644
--- a/components/Blocks/HotelListing/index.tsx
+++ b/components/Blocks/HotelListing/index.tsx
@@ -1,4 +1,4 @@
-import { getHotels } from "@/lib/trpc/memoizedRequests"
+import { getHotelsByCSFilter } from "@/lib/trpc/memoizedRequests"
import SectionContainer from "@/components/Section/Container"
import Title from "@/components/TempDesignSystem/Text/Title"
@@ -13,7 +13,7 @@ export default async function HotelListing({
hotelsToInclude,
contentType,
}: HotelListingProps) {
- const hotels = await getHotels({
+ const hotels = await getHotelsByCSFilter({
locationFilter,
hotelsToInclude: hotelsToInclude,
})
@@ -27,7 +27,7 @@ export default async function HotelListing({
{heading}
- {hotels.map(({ data, url }) => (
+ {hotels.map(({ url, ...data }) => (
+
+
+ {country}
+
+
+
+ )
+}
diff --git a/components/ContentType/DestinationPage/DestinationCountryPage/CountryMap/map.module.css b/components/ContentType/DestinationPage/DestinationCountryPage/CountryMap/map.module.css
new file mode 100644
index 000000000..d85a576d1
--- /dev/null
+++ b/components/ContentType/DestinationPage/DestinationCountryPage/CountryMap/map.module.css
@@ -0,0 +1,23 @@
+.countryMap {
+ --destination-map-height: 100dvh;
+
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: var(--destination-map-height);
+ width: 100dvw;
+ z-index: var(--hotel-dynamic-map-z-index);
+ display: flex;
+ background-color: var(--Base-Surface-Primary-light-Normal);
+}
+.wrapper {
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.closeButton {
+ pointer-events: initial;
+ box-shadow: var(--button-box-shadow);
+ gap: var(--Spacing-x-half);
+}
diff --git a/components/ContentType/DestinationPage/DestinationCountryPage/index.tsx b/components/ContentType/DestinationPage/DestinationCountryPage/index.tsx
index 92b321150..4dacc76ed 100644
--- a/components/ContentType/DestinationPage/DestinationCountryPage/index.tsx
+++ b/components/ContentType/DestinationPage/DestinationCountryPage/index.tsx
@@ -16,6 +16,7 @@ import SidebarContentWrapper from "../SidebarContentWrapper"
import DestinationPageSidePeek from "../Sidepeek"
import StaticMap from "../StaticMap"
import TopImages from "../TopImages"
+import CountryMap, { preloadHotels } from "./CountryMap"
import styles from "./destinationCountryPage.module.css"
@@ -41,6 +42,8 @@ export default async function DestinationCountryPage() {
destination_settings,
} = destinationCountryPage
+ preloadHotels(destination_settings.country)
+
return (
<>
@@ -72,6 +75,7 @@ export default async function DestinationCountryPage() {
+
diff --git a/components/ContentType/DestinationPage/Map/DynamicMap/dynamicMap.module.css b/components/ContentType/DestinationPage/Map/DynamicMap/dynamicMap.module.css
new file mode 100644
index 000000000..baedbf86d
--- /dev/null
+++ b/components/ContentType/DestinationPage/Map/DynamicMap/dynamicMap.module.css
@@ -0,0 +1,66 @@
+.mapWrapper {
+ --button-box-shadow: 0 0 8px 1px rgba(0, 0, 0, 0.1);
+ width: 100%;
+ height: 100%;
+ position: relative;
+ z-index: 0;
+}
+
+.mapWrapper::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);
+}
+
+@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/DestinationPage/Map/DynamicMap/index.tsx b/components/ContentType/DestinationPage/Map/DynamicMap/index.tsx
new file mode 100644
index 000000000..bf8019538
--- /dev/null
+++ b/components/ContentType/DestinationPage/Map/DynamicMap/index.tsx
@@ -0,0 +1,119 @@
+"use client"
+
+import "client-only"
+
+import { Map, type MapProps, useMap } from "@vis.gl/react-google-maps"
+import { type PropsWithChildren, useEffect } from "react"
+import { useIntl } from "react-intl"
+
+import { CloseLargeIcon, MinusIcon, PlusIcon } from "@/components/Icons"
+import Button from "@/components/TempDesignSystem/Button"
+import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
+
+import styles from "./dynamicMap.module.css"
+
+import type { DestinationMarker } from "@/types/components/maps/destinationMarkers"
+
+interface DynamicMapProps {
+ markers: DestinationMarker[]
+ mapId: string
+ onTilesLoaded?: () => void
+ onClose?: () => void
+}
+
+export default function DynamicMap({
+ markers,
+ mapId,
+ onTilesLoaded,
+ onClose,
+ children,
+}: PropsWithChildren) {
+ const intl = useIntl()
+ const map = useMap()
+
+ useEffect(() => {
+ if (map) {
+ const bounds = new google.maps.LatLngBounds()
+ markers.forEach((marker) => {
+ bounds.extend(marker.coordinates)
+ })
+ map.fitBounds(bounds)
+ }
+ }, [map, markers])
+
+ useHandleKeyUp((event: KeyboardEvent) => {
+ if (event.key === "Escape" && onClose) {
+ onClose()
+ }
+ })
+
+ function zoomIn() {
+ const currentZoom = map && map.getZoom()
+ console.log(currentZoom)
+ if (currentZoom) {
+ map.setZoom(currentZoom + 1)
+ }
+ }
+ function zoomOut() {
+ const currentZoom = map && map.getZoom()
+ console.log(currentZoom)
+ if (currentZoom) {
+ map.setZoom(currentZoom - 1)
+ }
+ }
+
+ const mapOptions: MapProps = {
+ defaultCenter: markers[0].coordinates, // Default center will be overridden by the bounds
+ minZoom: 3,
+ defaultZoom: 8,
+ disableDefaultUI: true,
+ clickableIcons: false,
+ gestureHandling: "greedy",
+ mapId,
+ }
+
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/components/ContentType/DestinationPage/Map/MapContent/index.tsx b/components/ContentType/DestinationPage/Map/MapContent/index.tsx
new file mode 100644
index 000000000..e867565c1
--- /dev/null
+++ b/components/ContentType/DestinationPage/Map/MapContent/index.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import {
+ AdvancedMarker,
+ AdvancedMarkerAnchorPoint,
+} from "@vis.gl/react-google-maps"
+
+import HotelMarkerByType from "@/components/Maps/Markers"
+
+import styles from "./mapContent.module.css"
+
+import type { DestinationMarker } from "@/types/components/maps/destinationMarkers"
+
+interface MapContentProps {
+ markers: DestinationMarker[]
+}
+
+export default function MapContent({ markers }: MapContentProps) {
+ return markers.map((item) => (
+
+
+
+ ))
+}
diff --git a/components/ContentType/DestinationPage/Map/MapContent/mapContent.module.css b/components/ContentType/DestinationPage/Map/MapContent/mapContent.module.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/components/ContentType/DestinationPage/Map/MapProvider.tsx b/components/ContentType/DestinationPage/Map/MapProvider.tsx
new file mode 100644
index 000000000..e3ce4dcd6
--- /dev/null
+++ b/components/ContentType/DestinationPage/Map/MapProvider.tsx
@@ -0,0 +1,15 @@
+"use client"
+import { APIProvider } from "@vis.gl/react-google-maps"
+
+import type { PropsWithChildren } from "react"
+
+interface MapContainerProps {
+ apiKey: string
+}
+
+export default function MapProvider({
+ apiKey,
+ children,
+}: PropsWithChildren) {
+ return {children}
+}
diff --git a/components/ContentType/DestinationPage/Map/index.tsx b/components/ContentType/DestinationPage/Map/index.tsx
new file mode 100644
index 000000000..f8fe57641
--- /dev/null
+++ b/components/ContentType/DestinationPage/Map/index.tsx
@@ -0,0 +1,129 @@
+"use client"
+
+import { useSearchParams } from "next/navigation"
+import {
+ type PropsWithChildren,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from "react"
+import { Dialog, Modal } from "react-aria-components"
+
+import { debounce } from "@/utils/debounce"
+
+import DynamicMap from "./DynamicMap"
+import MapContent from "./MapContent"
+import MapProvider from "./MapProvider"
+
+import styles from "./map.module.css"
+
+import type { DestinationMarker } from "@/types/components/maps/destinationMarkers"
+import type { HotelDataWithUrl } from "@/types/hotel"
+
+interface MapProps {
+ hotels: HotelDataWithUrl[]
+ mapId: string
+ apiKey: string
+}
+
+export default function Map({
+ hotels,
+ mapId,
+ apiKey,
+ children,
+}: PropsWithChildren) {
+ const searchParams = useSearchParams()
+ const isMapView = useMemo(
+ () => searchParams.get("view") === "map",
+ [searchParams]
+ )
+ const rootDiv = useRef(null)
+ const [mapHeight, setMapHeight] = useState("0px")
+ const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
+
+ const markers = hotels
+ .map(({ hotel }) => ({
+ id: hotel.id,
+ type: hotel.hotelType || "regular",
+ name: hotel.name,
+ coordinates: hotel.location
+ ? {
+ lat: hotel.location.latitude,
+ lng: hotel.location.longitude,
+ }
+ : null,
+ }))
+ .filter((item): item is DestinationMarker => !!item.coordinates)
+
+ // Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget)
+ const handleMapHeight = useCallback(() => {
+ const topPosition = rootDiv.current?.getBoundingClientRect().top ?? 0
+ const scrollY = window.scrollY
+ setMapHeight(`calc(100dvh - ${topPosition + scrollY}px)`)
+ }, [])
+
+ // Making sure the map is always opened at the top of the page,
+ // just below the header and booking widget as these should stay visible.
+ // When closing, the page should scroll back to the position it was before opening the map.
+ useEffect(() => {
+ // Skip the first render
+ if (!rootDiv.current) {
+ return
+ }
+
+ if (isMapView && scrollHeightWhenOpened === 0) {
+ const scrollY = window.scrollY
+ setScrollHeightWhenOpened(scrollY)
+ window.scrollTo({ top: 0, behavior: "instant" })
+ } else if (!isMapView && scrollHeightWhenOpened !== 0) {
+ window.scrollTo({ top: scrollHeightWhenOpened, behavior: "instant" })
+ setScrollHeightWhenOpened(0)
+ }
+ }, [isMapView, scrollHeightWhenOpened, rootDiv])
+
+ useEffect(() => {
+ const debouncedResizeHandler = debounce(function () {
+ handleMapHeight()
+ })
+
+ const observer = new ResizeObserver(debouncedResizeHandler)
+
+ observer.observe(document.documentElement)
+
+ return () => {
+ if (observer) {
+ observer.unobserve(document.documentElement)
+ }
+ }
+ }, [rootDiv, isMapView, handleMapHeight])
+
+ function handleClose() {
+ window.history.pushState({}, "", window.location.pathname)
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/components/ContentType/DestinationPage/Map/map.module.css b/components/ContentType/DestinationPage/Map/map.module.css
new file mode 100644
index 000000000..b0bde35a3
--- /dev/null
+++ b/components/ContentType/DestinationPage/Map/map.module.css
@@ -0,0 +1,35 @@
+.dialog {
+ --destination-map-height: 100dvh;
+
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: var(--destination-map-height);
+ width: 100dvw;
+ z-index: var(--hotel-dynamic-map-z-index);
+ display: flex;
+ background-color: var(--Base-Surface-Primary-light-Normal);
+}
+
+.sidebar {
+ width: 100%;
+ max-width: 400px;
+ background-color: var(--Base-Surface-Primary-Normal);
+ overflow-y: auto;
+ padding: var(--Spacing-x4);
+ display: flex;
+ flex-direction: column;
+ gap: var(--Spacing-x4);
+}
+
+.wrapper {
+ position: absolute;
+ top: 0;
+ left: 0;
+}
+
+.closeButton {
+ pointer-events: initial;
+ box-shadow: var(--button-box-shadow);
+ gap: var(--Spacing-x-half);
+}
diff --git a/components/ContentType/DestinationPage/StaticMap/index.tsx b/components/ContentType/DestinationPage/StaticMap/index.tsx
index fafdd3669..243f1c7e4 100644
--- a/components/ContentType/DestinationPage/StaticMap/index.tsx
+++ b/components/ContentType/DestinationPage/StaticMap/index.tsx
@@ -1,3 +1,5 @@
+import Link from "next/link"
+
import { MapIcon } from "@/components/Icons"
import StaticMap from "@/components/Maps/StaticMap"
import Button from "@/components/TempDesignSystem/Button"
@@ -36,10 +38,12 @@ export default async function DestinationStaticMap({
size="small"
theme="base"
className={styles.button}
+ asChild
>
- {/* TODO: Decide on how the map should load */}
-
- {intl.formatMessage({ id: "See on map" })}
+
+
+ {intl.formatMessage({ id: "See on map" })}
+
)
diff --git a/components/Maps/Markers/DowntownCamper.tsx b/components/Maps/Markers/DowntownCamper.tsx
new file mode 100644
index 000000000..76e045c25
--- /dev/null
+++ b/components/Maps/Markers/DowntownCamper.tsx
@@ -0,0 +1,123 @@
+export default function DowntownCamperMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/DowntownCamperSmall.tsx b/components/Maps/Markers/DowntownCamperSmall.tsx
new file mode 100644
index 000000000..cb7989a9f
--- /dev/null
+++ b/components/Maps/Markers/DowntownCamperSmall.tsx
@@ -0,0 +1,78 @@
+export default function DowntownCamperSmallMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/GrandHotel.tsx b/components/Maps/Markers/GrandHotel.tsx
new file mode 100644
index 000000000..e3f7125ac
--- /dev/null
+++ b/components/Maps/Markers/GrandHotel.tsx
@@ -0,0 +1,173 @@
+export default function GrandHotelMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/GrandHotelSmall.tsx b/components/Maps/Markers/GrandHotelSmall.tsx
new file mode 100644
index 000000000..8da9c8c92
--- /dev/null
+++ b/components/Maps/Markers/GrandHotelSmall.tsx
@@ -0,0 +1,128 @@
+export default function GrandHotelSmallMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/Haymarket.tsx b/components/Maps/Markers/Haymarket.tsx
new file mode 100644
index 000000000..aaca7b723
--- /dev/null
+++ b/components/Maps/Markers/Haymarket.tsx
@@ -0,0 +1,121 @@
+export default function HaymarketMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/HaymarketSmall.tsx b/components/Maps/Markers/HaymarketSmall.tsx
new file mode 100644
index 000000000..f15730e59
--- /dev/null
+++ b/components/Maps/Markers/HaymarketSmall.tsx
@@ -0,0 +1,76 @@
+export default function HaymarketSmallMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/HotelNorge.tsx b/components/Maps/Markers/HotelNorge.tsx
new file mode 100644
index 000000000..1820bee12
--- /dev/null
+++ b/components/Maps/Markers/HotelNorge.tsx
@@ -0,0 +1,125 @@
+export default function HotelNorgeMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/HotelNorgeSmall.tsx b/components/Maps/Markers/HotelNorgeSmall.tsx
new file mode 100644
index 000000000..9ac8f719b
--- /dev/null
+++ b/components/Maps/Markers/HotelNorgeSmall.tsx
@@ -0,0 +1,80 @@
+export default function HotelNorgeSmallMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/Marski.tsx b/components/Maps/Markers/Marski.tsx
new file mode 100644
index 000000000..0dd7b51a3
--- /dev/null
+++ b/components/Maps/Markers/Marski.tsx
@@ -0,0 +1,141 @@
+export default function MarskiMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/MarskiSmall.tsx b/components/Maps/Markers/MarskiSmall.tsx
new file mode 100644
index 000000000..c11160b86
--- /dev/null
+++ b/components/Maps/Markers/MarskiSmall.tsx
@@ -0,0 +1,96 @@
+export default function MarskiSmallMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/ScandicGo.tsx b/components/Maps/Markers/ScandicGo.tsx
new file mode 100644
index 000000000..6ed53afa9
--- /dev/null
+++ b/components/Maps/Markers/ScandicGo.tsx
@@ -0,0 +1,123 @@
+export default function ScandicGoMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/ScandicGoSmall.tsx b/components/Maps/Markers/ScandicGoSmall.tsx
new file mode 100644
index 000000000..9a9c59a11
--- /dev/null
+++ b/components/Maps/Markers/ScandicGoSmall.tsx
@@ -0,0 +1,78 @@
+export default function ScandicGoSmallMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/ScandicSmall.tsx b/components/Maps/Markers/ScandicSmall.tsx
new file mode 100644
index 000000000..034013926
--- /dev/null
+++ b/components/Maps/Markers/ScandicSmall.tsx
@@ -0,0 +1,112 @@
+export default function ScandicSmallMarker({
+ className,
+ ...props
+}: React.SVGAttributes) {
+ return (
+
+ )
+}
diff --git a/components/Maps/Markers/index.tsx b/components/Maps/Markers/index.tsx
new file mode 100644
index 000000000..587b06870
--- /dev/null
+++ b/components/Maps/Markers/index.tsx
@@ -0,0 +1,52 @@
+import DowntownCamperMarker from "./DowntownCamper"
+import DowntownCamperSmallMarker from "./DowntownCamperSmall"
+import GrandHotelMarker from "./GrandHotel"
+import GrandHotelSmallMarker from "./GrandHotelSmall"
+import HaymarketMarker from "./Haymarket"
+import HaymarketSmallMarker from "./HaymarketSmall"
+import HotelNorgeMarker from "./HotelNorge"
+import HotelNorgeSmallMarker from "./HotelNorgeSmall"
+import MarskiMarker from "./Marski"
+import MarskiSmallMarker from "./MarskiSmall"
+import ScandicMarker from "./Scandic"
+import ScandicGoMarker from "./ScandicGo"
+import ScandicGoSmallMarker from "./ScandicGoSmall"
+import ScandicSmallMarker from "./ScandicSmall"
+
+import { HotelTypeEnum } from "@/types/enums/hotelType"
+import { SignatureHotelEnum } from "@/types/enums/signatureHotel"
+
+interface HotelMarkerByTypeProps {
+ hotelId: string
+ hotelType: string
+ smallSize?: boolean
+}
+
+export default function HotelMarkerByType({
+ hotelId,
+ hotelType,
+ smallSize = true,
+}: HotelMarkerByTypeProps) {
+ if (hotelType === HotelTypeEnum.ScandicGo) {
+ return smallSize ? :
+ }
+
+ switch (hotelId) {
+ case SignatureHotelEnum.Haymarket:
+ return smallSize ? :
+ case SignatureHotelEnum.HotelNorge:
+ return smallSize ? :
+ case SignatureHotelEnum.DowntownCamper:
+ return smallSize ? (
+
+ ) : (
+
+ )
+ case SignatureHotelEnum.GrandHotelOslo:
+ return smallSize ? :
+ case SignatureHotelEnum.Marski:
+ return smallSize ? :
+ default:
+ return smallSize ? :
+ }
+}
diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts
index f4495da21..336fe6818 100644
--- a/lib/trpc/memoizedRequests/index.ts
+++ b/lib/trpc/memoizedRequests/index.ts
@@ -13,7 +13,7 @@ import type {
} from "@/types/trpc/routers/hotel/hotel"
import type { Lang } from "@/constants/languages"
import type {
- GetHotelsInput,
+ GetHotelsByCSFilterInput,
GetRoomsAvailabilityInput,
GetSelectedRoomAvailabilityInput,
} from "@/server/routers/hotels/input"
@@ -67,10 +67,10 @@ export const getUserTracking = cache(async function getMemoizedUserTracking() {
return serverClient().user.tracking()
})
-export const getHotels = cache(async function getMemoizedHotels(
- input: GetHotelsInput
+export const getHotelsByCSFilter = cache(async function getMemoizedHotels(
+ input: GetHotelsByCSFilterInput
) {
- return serverClient().hotel.hotels.get(input)
+ return serverClient().hotel.hotels.byCSFilter.get(input)
})
export const getHotel = cache(async function getMemoizedHotelData(
@@ -206,6 +206,13 @@ export const getDestinationCityPagesByCountry = cache(
})
}
)
+export const getHotelsByCountry = cache(
+ async function getMemoizedHotelsByCountry(country: Country) {
+ return serverClient().hotel.hotels.byCountry.get({
+ country,
+ })
+ }
+)
export const getDestinationCityPage = cache(
async function getMemoizedDestinationCityPage() {
return serverClient().contentstack.destinationCityPage.get()
diff --git a/server/routers/contentstack/destinationOverviewPage/query.ts b/server/routers/contentstack/destinationOverviewPage/query.ts
index d2a8b6ba8..809d80b96 100644
--- a/server/routers/contentstack/destinationOverviewPage/query.ts
+++ b/server/routers/contentstack/destinationOverviewPage/query.ts
@@ -244,7 +244,6 @@ export const destinationOverviewPageQueryRouter = router({
const hotelIdsParams = new URLSearchParams({
language: apiLang,
city: city.id,
- onlyBasicInfo: "true",
})
const hotels = await getHotelIdsByCityId(
diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts
index 489d6842b..7ed3f649d 100644
--- a/server/routers/hotels/input.ts
+++ b/server/routers/hotels/input.ts
@@ -63,7 +63,7 @@ export const hotelInputSchema = z.object({
language: z.nativeEnum(Lang),
})
-export const getHotelsInput = z.object({
+export const getHotelsByCSFilterInput = z.object({
locationFilter: z
.object({
city: z.string().nullable(),
@@ -73,7 +73,8 @@ export const getHotelsInput = z.object({
.nullable(),
hotelsToInclude: z.array(z.string()),
})
-export interface GetHotelsInput extends z.infer {}
+export interface GetHotelsByCSFilterInput
+ extends z.infer {}
export const nearbyHotelIdsInput = z.object({
hotelId: z.string(),
@@ -116,3 +117,7 @@ export const getAdditionalDataInputSchema = z.object({
hotelId: z.string(),
language: z.string(),
})
+
+export const getHotelsByCountryInput = z.object({
+ country: z.nativeEnum(Country),
+})
diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts
index 43c0d996d..7f2cc877d 100644
--- a/server/routers/hotels/query.ts
+++ b/server/routers/hotels/query.ts
@@ -24,8 +24,9 @@ import {
breakfastPackageInputSchema,
cityCoordinatesInputSchema,
getAdditionalDataInputSchema,
+ getHotelsByCountryInput,
+ getHotelsByCSFilterInput,
getHotelsByHotelIdsAvailabilityInputSchema,
- getHotelsInput,
getMeetingRoomsInputSchema,
hotelInputSchema,
hotelsAvailabilityInputSchema,
@@ -59,8 +60,7 @@ import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHote
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { HotelTypeEnum } from "@/types/enums/hotelType"
import type { RequestOptionsWithOutBody } from "@/types/fetch"
-import type { HotelData } from "@/types/hotel"
-import type { HotelPageUrl } from "@/types/trpc/routers/contentstack/hotelPage"
+import type { HotelDataWithUrl } from "@/types/hotel"
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
@@ -833,207 +833,251 @@ export const hotelQueryRouter = router({
return getHotel(input, ctx.serviceToken)
}),
hotels: router({
- get: contentStackBaseWithServiceProcedure
- .input(getHotelsInput)
- .query(async function ({ ctx, input }) {
- const { locationFilter, hotelsToInclude } = input
+ byCountry: router({
+ get: contentStackBaseWithServiceProcedure
+ .input(getHotelsByCountryInput)
+ .query(async ({ ctx, input }) => {
+ const { lang, serviceToken } = ctx
+ const { country } = input
- const language = ctx.lang
- const apiLang = toApiLang(language)
- const options: RequestOptionsWithOutBody = {
- // needs to clear default option as only
- // cache or next.revalidate is permitted
- cache: undefined,
- headers: {
- Authorization: `Bearer ${ctx.serviceToken}`,
- },
- next: {
- revalidate: env.CACHE_TIME_HOTELS,
- },
- }
-
- let hotelsToFetch: string[] = []
-
- metrics.hotels.counter.add(1, {
- input: JSON.stringify(input),
- language,
- })
- console.info(
- "api.hotel.hotels start",
- JSON.stringify({
- query: {
- ...input,
- language,
+ const options: RequestOptionsWithOutBody = {
+ // needs to clear default option as only
+ // cache or next.revalidate is permitted
+ cache: undefined,
+ headers: {
+ Authorization: `Bearer ${serviceToken}`,
+ },
+ next: {
+ revalidate: env.CACHE_TIME_HOTELS,
},
- })
- )
-
- if (hotelsToInclude.length) {
- hotelsToFetch = hotelsToInclude
- } else if (locationFilter?.city) {
- const locationsParams = new URLSearchParams({
- language: apiLang,
- })
- const locations = await getLocations(
- language,
- options,
- locationsParams,
- null
- )
- if (!locations || "error" in locations) {
- return []
}
-
- const cityId = locations
- .filter(
- (loc): loc is CityLocation =>
- "type" in loc && loc.type === "cities"
- )
- .find((loc) => loc.cityIdentifier === locationFilter.city)?.id
-
- if (!cityId) {
- metrics.hotels.fail.add(1, {
- input: JSON.stringify(input),
- language,
- error_type: "not_found",
- error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
- })
-
- console.error(
- "api.hotel.hotels not found error",
- JSON.stringify({
- query: { ...input, language },
- error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
- })
- )
- return []
- }
- const hotelIdsParams = new URLSearchParams({
- language: apiLang,
- city: cityId,
- onlyBasicInfo: "true",
- })
- const hotelIds = await getHotelIdsByCityId(
- cityId,
- options,
- hotelIdsParams
- )
-
- if (!hotelIds?.length) {
- metrics.hotels.fail.add(1, {
- cityId,
- language,
- error_type: "not_found",
- error: `No hotelIds found for cityId: ${cityId}`,
- })
-
- console.error(
- "api.hotel.hotels not found error",
- JSON.stringify({
- query: { cityId, language },
- error: `No hotelIds found for cityId: ${cityId}`,
- })
- )
- return []
- }
-
- const filteredHotelIds = hotelIds.filter(
- (id) => !locationFilter.excluded.includes(id)
- )
-
- hotelsToFetch = filteredHotelIds
- } else if (locationFilter?.country) {
const hotelIdsParams = new URLSearchParams({
language: ApiLang.En,
- country: locationFilter.country,
- onlyBasicInfo: "true",
+ country,
})
const hotelIds = await getHotelIdsByCountry(
- locationFilter.country,
+ country,
options,
hotelIdsParams
)
- if (!hotelIds?.length) {
- metrics.hotels.fail.add(1, {
+ const hotels = await Promise.all(
+ hotelIds.map(async (hotelId) => {
+ const [hotelData, url] = await Promise.all([
+ getHotel(
+ { hotelId, isCardOnlyPayment: false, language: lang },
+ ctx.serviceToken
+ ),
+ getHotelPageUrl(lang, hotelId),
+ ])
+
+ return hotelData ? { ...hotelData, url } : null
+ })
+ )
+
+ return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
+ }),
+ }),
+ byCSFilter: router({
+ get: contentStackBaseWithServiceProcedure
+ .input(getHotelsByCSFilterInput)
+ .query(async function ({ ctx, input }) {
+ const { locationFilter, hotelsToInclude } = input
+
+ const language = ctx.lang
+ const apiLang = toApiLang(language)
+ const options: RequestOptionsWithOutBody = {
+ // needs to clear default option as only
+ // cache or next.revalidate is permitted
+ cache: undefined,
+ headers: {
+ Authorization: `Bearer ${ctx.serviceToken}`,
+ },
+ next: {
+ revalidate: env.CACHE_TIME_HOTELS,
+ },
+ }
+
+ let hotelsToFetch: string[] = []
+
+ metrics.hotels.counter.add(1, {
+ input: JSON.stringify(input),
+ language,
+ })
+ console.info(
+ "api.hotel.hotels start",
+ JSON.stringify({
+ query: {
+ ...input,
+ language,
+ },
+ })
+ )
+
+ if (hotelsToInclude.length) {
+ hotelsToFetch = hotelsToInclude
+ } else if (locationFilter?.city) {
+ const locationsParams = new URLSearchParams({
+ language: apiLang,
+ })
+ const locations = await getLocations(
+ language,
+ options,
+ locationsParams,
+ null
+ )
+ if (!locations || "error" in locations) {
+ return []
+ }
+
+ const cityId = locations
+ .filter(
+ (loc): loc is CityLocation =>
+ "type" in loc && loc.type === "cities"
+ )
+ .find((loc) => loc.cityIdentifier === locationFilter.city)?.id
+
+ if (!cityId) {
+ metrics.hotels.fail.add(1, {
+ input: JSON.stringify(input),
+ language,
+ error_type: "not_found",
+ error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
+ })
+
+ console.error(
+ "api.hotel.hotels not found error",
+ JSON.stringify({
+ query: { ...input, language },
+ error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
+ })
+ )
+ return []
+ }
+ const hotelIdsParams = new URLSearchParams({
+ language: apiLang,
+ city: cityId,
+ })
+ const hotelIds = await getHotelIdsByCityId(
+ cityId,
+ options,
+ hotelIdsParams
+ )
+
+ if (!hotelIds?.length) {
+ metrics.hotels.fail.add(1, {
+ cityId,
+ language,
+ error_type: "not_found",
+ error: `No hotelIds found for cityId: ${cityId}`,
+ })
+
+ console.error(
+ "api.hotel.hotels not found error",
+ JSON.stringify({
+ query: { cityId, language },
+ error: `No hotelIds found for cityId: ${cityId}`,
+ })
+ )
+ return []
+ }
+
+ const filteredHotelIds = hotelIds.filter(
+ (id) => !locationFilter.excluded.includes(id)
+ )
+
+ hotelsToFetch = filteredHotelIds
+ } else if (locationFilter?.country) {
+ const hotelIdsParams = new URLSearchParams({
+ language: ApiLang.En,
country: locationFilter.country,
+ })
+ const hotelIds = await getHotelIdsByCountry(
+ locationFilter.country,
+ options,
+ hotelIdsParams
+ )
+
+ if (!hotelIds?.length) {
+ metrics.hotels.fail.add(1, {
+ country: locationFilter.country,
+ language,
+ error_type: "not_found",
+ error: `No hotelIds found for country: ${locationFilter.country}`,
+ })
+
+ console.error(
+ "api.hotel.hotels not found error",
+ JSON.stringify({
+ query: { country: locationFilter.country, language },
+ error: `No hotelIds found for cityId: ${locationFilter.country}`,
+ })
+ )
+ return []
+ }
+
+ const filteredHotelIds = hotelIds.filter(
+ (id) => !locationFilter.excluded.includes(id)
+ )
+
+ hotelsToFetch = filteredHotelIds
+ }
+
+ if (!hotelsToFetch.length) {
+ metrics.hotels.fail.add(1, {
+ input: JSON.stringify(input),
language,
error_type: "not_found",
- error: `No hotelIds found for country: ${locationFilter.country}`,
+ error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
})
console.error(
"api.hotel.hotels not found error",
JSON.stringify({
- query: { country: locationFilter.country, language },
- error: `No hotelIds found for cityId: ${locationFilter.country}`,
+ query: JSON.stringify(input),
+ error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
})
)
return []
}
- const filteredHotelIds = hotelIds.filter(
- (id) => !locationFilter.excluded.includes(id)
- )
+ const hotels = await Promise.all(
+ hotelsToFetch.map(async (hotelId) => {
+ const [hotelData, url] = await Promise.all([
+ getHotel(
+ { hotelId, isCardOnlyPayment: false, language },
+ ctx.serviceToken
+ ),
+ getHotelPageUrl(language, hotelId),
+ ])
- hotelsToFetch = filteredHotelIds
- }
-
- if (!hotelsToFetch.length) {
- metrics.hotels.fail.add(1, {
- input: JSON.stringify(input),
- language,
- error_type: "not_found",
- error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
- })
-
- console.error(
- "api.hotel.hotels not found error",
- JSON.stringify({
- query: JSON.stringify(input),
- error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
+ return hotelData
+ ? {
+ ...hotelData,
+ url,
+ }
+ : null
})
)
- return []
- }
- const hotels = await Promise.all(
- hotelsToFetch.map(async (hotelId) => {
- const [hotelData, url] = await Promise.all([
- getHotel(
- { hotelId, isCardOnlyPayment: false, language },
- ctx.serviceToken
- ),
- getHotelPageUrl(language, hotelId),
- ])
-
- return {
- data: hotelData,
- url,
- }
+ metrics.hotels.success.add(1, {
+ input: JSON.stringify(input),
+ language,
})
- )
- metrics.hotels.success.add(1, {
- input: JSON.stringify(input),
- language,
- })
+ console.info(
+ "api.hotels success",
+ JSON.stringify({
+ query: {
+ input: JSON.stringify(input),
+ language,
+ },
+ })
+ )
- console.info(
- "api.hotels success",
- JSON.stringify({
- query: {
- input: JSON.stringify(input),
- language,
- },
- })
- )
-
- return hotels.filter(
- (hotel): hotel is { data: HotelData; url: HotelPageUrl } =>
- !!hotel.data
- )
- }),
+ return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
+ }),
+ }),
}),
nearbyHotelIds: serviceProcedure
.input(nearbyHotelIdsInput)
diff --git a/server/routers/hotels/utils.ts b/server/routers/hotels/utils.ts
index f638f9220..a17bb5529 100644
--- a/server/routers/hotels/utils.ts
+++ b/server/routers/hotels/utils.ts
@@ -432,7 +432,6 @@ export async function getHotelIdsByCityIdentifier(
const hotelIdsParams = new URLSearchParams({
language: apiLang,
city: cityId,
- onlyBasicInfo: "true",
})
const options: RequestOptionsWithOutBody = {
// needs to clear default option as only
diff --git a/types/components/maps/destinationMarkers.ts b/types/components/maps/destinationMarkers.ts
new file mode 100644
index 000000000..279d29d97
--- /dev/null
+++ b/types/components/maps/destinationMarkers.ts
@@ -0,0 +1,6 @@
+export interface DestinationMarker {
+ id: string
+ type: string
+ name: string
+ coordinates: google.maps.LatLngLiteral
+}
diff --git a/types/hotel.ts b/types/hotel.ts
index fab71c413..63027d822 100644
--- a/types/hotel.ts
+++ b/types/hotel.ts
@@ -68,3 +68,5 @@ export type HotelTripAdvisor =
export type AdditionalData = ReturnType
export type ExtraPageSchema = z.output
+
+export type HotelDataWithUrl = HotelData & { url: string }