Merged in feat/SW-1418-destination-page-map-data (pull request #1347)

feat/SW-1418 destination page map data

* feat(SW-1418): implement dynamic map on overview page

* feat(SW-1418): update folder structure

* feat(SW-1418): use getHotelsByHotelIds


Approved-by: Erik Tiekstra
Approved-by: Matilda Landström
This commit is contained in:
Fredrik Thorsson
2025-02-17 08:59:50 +00:00
parent f5c5172555
commit 22111eb7d2
24 changed files with 123 additions and 141 deletions

View File

@@ -1,5 +1,5 @@
import { headers } from "next/headers"
import { notFound,redirect } from "next/navigation"
import { notFound, redirect } from "next/navigation"
import { overview } from "@/constants/routes/myPages"
import { isSignupPage } from "@/constants/routes/signup"
@@ -7,16 +7,16 @@ import { env } from "@/env/server"
import { getHotelPage } from "@/lib/trpc/memoizedRequests"
import { auth } from "@/auth"
import DestinationOverviewPage from "@/components/ContentType/DestinationOverviewPage"
import DestinationCityPage from "@/components/ContentType/DestinationPage/DestinationCityPage"
import DestinationCountryPage from "@/components/ContentType/DestinationPage/DestinationCountryPage"
import DestinationOverviewPage from "@/components/ContentType/DestinationPage/DestinationOverviewPage"
import HotelPage from "@/components/ContentType/HotelPage"
import HotelSubpage from "@/components/ContentType/HotelSubpage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
import StartPage from "@/components/ContentType/StartPage"
import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
import ContentPage from "@/components/ContentType/StaticPages/ContentPage"
import { getLang,setLang } from "@/i18n/serverContext"
import { getLang, setLang } from "@/i18n/serverContext"
import { isValidSession } from "@/utils/session"
import type {

View File

@@ -1,62 +0,0 @@
"use client"
import { Map, type MapProps, useMap } from "@vis.gl/react-google-maps"
import { useIntl } from "react-intl"
import { MinusIcon, PlusIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import styles from "./overviewMap.module.css"
import type { OverviewMapProps } from "@/types/components/destinationOverviewPage/overviewMap/overviewMap"
export default function OverviewMap({ mapId }: OverviewMapProps) {
const map = useMap()
const intl = useIntl()
// Placeholder value, currently these coordinates are Stockholm
const defaultCenter = {
lat: 59.3293,
lng: 18.0686,
}
const mapOptions: MapProps = {
defaultZoom: 4,
minZoom: 3,
defaultCenter: defaultCenter,
disableDefaultUI: true,
clickableIcons: false,
mapId: 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)
}
}
const zoomControls = (
<div className={styles.buttonContainer}>
<Button theme="base" intent="inverted" onClick={zoomOut}>
<MinusIcon width={12} height={12} color="black" />
</Button>
<Button theme="base" intent="inverted" onClick={zoomIn}>
<PlusIcon width={12} height={12} color="black" />
</Button>
</div>
)
return (
<div className={styles.mapContainer}>
<Map {...mapOptions}></Map>
{zoomControls}
</div>
)
}

View File

@@ -1,13 +0,0 @@
.mapContainer {
position: relative;
min-width: var(--max-width-page);
min-height: 700px; /* Placeholder value */
}
.buttonContainer {
position: absolute;
display: flex;
gap: var(--Spacing-x1);
right: 25px;
bottom: 25px;
}

View File

@@ -1,19 +0,0 @@
"use client"
import { APIProvider } from "@vis.gl/react-google-maps"
import InputForm from "./InputForm"
import OverviewMap from "./OverviewMap"
import type { OverviewMapContainerProps } from "@/types/components/destinationOverviewPage/overviewMap/overviewMapContainer"
export default function OverviewMapContainer({
apiKey,
mapId,
}: OverviewMapContainerProps) {
return (
<APIProvider apiKey={apiKey}>
<InputForm />
<OverviewMap mapId={mapId} />
</APIProvider>
)
}

View File

@@ -0,0 +1,31 @@
import { env } from "@/env/server"
import { getAllHotels } from "@/lib/trpc/memoizedRequests"
import DynamicMap from "../../Map/DynamicMap"
import MapContent from "../../Map/MapContent"
import MapProvider from "../../Map/MapProvider"
import { getHotelMapMarkers, mapMarkerDataToGeoJson } from "../../Map/utils"
import InputForm from "./InputForm"
export default async function OverviewMapContainer() {
const hotelData = await getAllHotels()
if (!hotelData) {
return null
}
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
const markers = getHotelMapMarkers(hotelData)
const geoJson = mapMarkerDataToGeoJson(markers)
return (
<MapProvider apiKey={googleMapsApiKey}>
<InputForm />
<DynamicMap mapId={googleMapId} markers={markers}>
<MapContent geojson={geoJson} />
</DynamicMap>
</MapProvider>
)
}

View File

@@ -1,6 +1,5 @@
import { Suspense } from "react"
import { env } from "@/env/server"
import {
getDestinationOverviewPage,
getDestinationsList,
@@ -26,16 +25,11 @@ export default async function DestinationOverviewPage() {
const { tracking, destinationOverviewPage } = pageData
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
return (
<>
{googleMapsApiKey ? (
<div className={styles.mapContainer}>
<OverviewMapContainer apiKey={googleMapsApiKey} mapId={googleMapId} />
</div>
) : null}
<div className={styles.mapContainer}>
<OverviewMapContainer />
</div>
<main className={styles.main}>
<div className={styles.blocks}>
<Blocks blocks={destinationOverviewPage.blocks} />

View File

@@ -36,6 +36,7 @@
.zoomButtons {
display: grid;
gap: var(--Spacing-x1);
margin-top: auto;
}
.closeButton {

View File

@@ -80,17 +80,19 @@ export default function DynamicMap({
{children}
</Map>
<div className={styles.ctaButtons}>
<Button
theme="base"
intent="inverted"
variant="icon"
size="small"
className={styles.closeButton}
onClick={onClose}
>
<CloseLargeIcon color="burgundy" />
<span>{intl.formatMessage({ id: "Close the map" })}</span>
</Button>
{onClose && (
<Button
theme="base"
intent="inverted"
variant="icon"
size="small"
className={styles.closeButton}
onClick={onClose}
>
<CloseLargeIcon color="burgundy" />
<span>{intl.formatMessage({ id: "Close the map" })}</span>
</Button>
)}
<div className={styles.zoomButtons}>
<Button
theme="base"

View File

@@ -16,11 +16,10 @@ import { debounce } from "@/utils/debounce"
import DynamicMap from "./DynamicMap"
import MapContent from "./MapContent"
import MapProvider from "./MapProvider"
import { mapMarkerDataToGeoJson } from "./utils"
import { getHotelMapMarkers, mapMarkerDataToGeoJson } from "./utils"
import styles from "./map.module.css"
import type { DestinationMarker } from "@/types/components/maps/destinationMarkers"
import type { HotelDataWithUrl } from "@/types/hotel"
interface MapProps {
@@ -44,20 +43,7 @@ export default function Map({
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)
const markers = getHotelMapMarkers(hotels)
const geoJson = mapMarkerDataToGeoJson(markers)
// Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget)

View File

@@ -3,6 +3,7 @@ import type {
MarkerFeature,
MarkerGeojson,
} from "@/types/components/maps/destinationMarkers"
import type { HotelDataWithUrl } from "@/types/hotel"
export function mapMarkerDataToGeoJson(markers: DestinationMarker[]) {
const features = markers.map<MarkerFeature>(
@@ -26,3 +27,21 @@ export function mapMarkerDataToGeoJson(markers: DestinationMarker[]) {
return geoJson
}
export function getHotelMapMarkers(hotels: HotelDataWithUrl[]) {
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)
return markers
}

View File

@@ -221,6 +221,9 @@ export const getHotelsByCityIdentifier = cache(
})
}
)
export const getAllHotels = cache(async function getMemoizedAllHotels() {
return serverClient().hotel.hotels.getAllHotels.get()
})
export const getDestinationCityPage = cache(
async function getMemoizedDestinationCityPage() {
return serverClient().contentstack.destinationCityPage.get()

View File

@@ -1172,6 +1172,52 @@ export const hotelQueryRouter = router({
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
}),
}),
getAllHotels: router({
get: serviceProcedure.query(async function ({ ctx }) {
const apiLang = toApiLang(ctx.lang)
const params = new URLSearchParams({
language: apiLang,
})
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,
},
}
const countries = await getCountries(options, params, ctx.lang)
if (!countries) {
return null
}
const countryNames = countries.data.map((country) => country.name)
const hotelData: HotelDataWithUrl[] = (
await Promise.all(
countryNames.map(async (country) => {
const countryParams = new URLSearchParams({
country: country,
})
const hotelIds = await getHotelIdsByCountry(
country,
options,
countryParams
)
const hotels = await getHotelsByHotelIds(
hotelIds,
ctx.lang,
ctx.serviceToken
)
return hotels
})
)
).flat()
return hotelData
}),
}),
}),
nearbyHotelIds: serviceProcedure
.input(nearbyHotelIdsInput)

View File

@@ -348,7 +348,7 @@ export async function getHotelIdsByCityId(
}
export async function getHotelIdsByCountry(
country: Country,
country: string,
options: RequestOptionsWithOutBody,
params: URLSearchParams
) {

View File

@@ -1,3 +0,0 @@
export type OverviewMapProps = {
mapId: string
}

View File

@@ -1,4 +0,0 @@
export type OverviewMapContainerProps = {
apiKey: string
mapId: string
}