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:
@@ -7,9 +7,9 @@ import { env } from "@/env/server"
|
|||||||
import { getHotelPage } from "@/lib/trpc/memoizedRequests"
|
import { getHotelPage } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import { auth } from "@/auth"
|
import { auth } from "@/auth"
|
||||||
import DestinationOverviewPage from "@/components/ContentType/DestinationOverviewPage"
|
|
||||||
import DestinationCityPage from "@/components/ContentType/DestinationPage/DestinationCityPage"
|
import DestinationCityPage from "@/components/ContentType/DestinationPage/DestinationCityPage"
|
||||||
import DestinationCountryPage from "@/components/ContentType/DestinationPage/DestinationCountryPage"
|
import DestinationCountryPage from "@/components/ContentType/DestinationPage/DestinationCountryPage"
|
||||||
|
import DestinationOverviewPage from "@/components/ContentType/DestinationPage/DestinationOverviewPage"
|
||||||
import HotelPage from "@/components/ContentType/HotelPage"
|
import HotelPage from "@/components/ContentType/HotelPage"
|
||||||
import HotelSubpage from "@/components/ContentType/HotelSubpage"
|
import HotelSubpage from "@/components/ContentType/HotelSubpage"
|
||||||
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
|
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: var(--max-width);
|
max-width: var(--max-width);
|
||||||
|
height: 700px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
|
||||||
import {
|
import {
|
||||||
getDestinationOverviewPage,
|
getDestinationOverviewPage,
|
||||||
getDestinationsList,
|
getDestinationsList,
|
||||||
@@ -26,16 +25,11 @@ export default async function DestinationOverviewPage() {
|
|||||||
|
|
||||||
const { tracking, destinationOverviewPage } = pageData
|
const { tracking, destinationOverviewPage } = pageData
|
||||||
|
|
||||||
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
|
|
||||||
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{googleMapsApiKey ? (
|
|
||||||
<div className={styles.mapContainer}>
|
<div className={styles.mapContainer}>
|
||||||
<OverviewMapContainer apiKey={googleMapsApiKey} mapId={googleMapId} />
|
<OverviewMapContainer />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
<div className={styles.blocks}>
|
<div className={styles.blocks}>
|
||||||
<Blocks blocks={destinationOverviewPage.blocks} />
|
<Blocks blocks={destinationOverviewPage.blocks} />
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
.zoomButtons {
|
.zoomButtons {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x1);
|
||||||
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.closeButton {
|
.closeButton {
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export default function DynamicMap({
|
|||||||
{children}
|
{children}
|
||||||
</Map>
|
</Map>
|
||||||
<div className={styles.ctaButtons}>
|
<div className={styles.ctaButtons}>
|
||||||
|
{onClose && (
|
||||||
<Button
|
<Button
|
||||||
theme="base"
|
theme="base"
|
||||||
intent="inverted"
|
intent="inverted"
|
||||||
@@ -91,6 +92,7 @@ export default function DynamicMap({
|
|||||||
<CloseLargeIcon color="burgundy" />
|
<CloseLargeIcon color="burgundy" />
|
||||||
<span>{intl.formatMessage({ id: "Close the map" })}</span>
|
<span>{intl.formatMessage({ id: "Close the map" })}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
<div className={styles.zoomButtons}>
|
<div className={styles.zoomButtons}>
|
||||||
<Button
|
<Button
|
||||||
theme="base"
|
theme="base"
|
||||||
|
|||||||
@@ -16,11 +16,10 @@ import { debounce } from "@/utils/debounce"
|
|||||||
import DynamicMap from "./DynamicMap"
|
import DynamicMap from "./DynamicMap"
|
||||||
import MapContent from "./MapContent"
|
import MapContent from "./MapContent"
|
||||||
import MapProvider from "./MapProvider"
|
import MapProvider from "./MapProvider"
|
||||||
import { mapMarkerDataToGeoJson } from "./utils"
|
import { getHotelMapMarkers, mapMarkerDataToGeoJson } from "./utils"
|
||||||
|
|
||||||
import styles from "./map.module.css"
|
import styles from "./map.module.css"
|
||||||
|
|
||||||
import type { DestinationMarker } from "@/types/components/maps/destinationMarkers"
|
|
||||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||||
|
|
||||||
interface MapProps {
|
interface MapProps {
|
||||||
@@ -44,20 +43,7 @@ export default function Map({
|
|||||||
const [mapHeight, setMapHeight] = useState("0px")
|
const [mapHeight, setMapHeight] = useState("0px")
|
||||||
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
const [scrollHeightWhenOpened, setScrollHeightWhenOpened] = useState(0)
|
||||||
|
|
||||||
const markers = hotels
|
const markers = getHotelMapMarkers(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 geoJson = mapMarkerDataToGeoJson(markers)
|
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)
|
// Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
MarkerFeature,
|
MarkerFeature,
|
||||||
MarkerGeojson,
|
MarkerGeojson,
|
||||||
} from "@/types/components/maps/destinationMarkers"
|
} from "@/types/components/maps/destinationMarkers"
|
||||||
|
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||||
|
|
||||||
export function mapMarkerDataToGeoJson(markers: DestinationMarker[]) {
|
export function mapMarkerDataToGeoJson(markers: DestinationMarker[]) {
|
||||||
const features = markers.map<MarkerFeature>(
|
const features = markers.map<MarkerFeature>(
|
||||||
@@ -26,3 +27,21 @@ export function mapMarkerDataToGeoJson(markers: DestinationMarker[]) {
|
|||||||
|
|
||||||
return geoJson
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(
|
export const getDestinationCityPage = cache(
|
||||||
async function getMemoizedDestinationCityPage() {
|
async function getMemoizedDestinationCityPage() {
|
||||||
return serverClient().contentstack.destinationCityPage.get()
|
return serverClient().contentstack.destinationCityPage.get()
|
||||||
|
|||||||
@@ -1172,6 +1172,52 @@ export const hotelQueryRouter = router({
|
|||||||
return hotels.filter((hotel): hotel is HotelDataWithUrl => !!hotel)
|
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
|
nearbyHotelIds: serviceProcedure
|
||||||
.input(nearbyHotelIdsInput)
|
.input(nearbyHotelIdsInput)
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ export async function getHotelIdsByCityId(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getHotelIdsByCountry(
|
export async function getHotelIdsByCountry(
|
||||||
country: Country,
|
country: string,
|
||||||
options: RequestOptionsWithOutBody,
|
options: RequestOptionsWithOutBody,
|
||||||
params: URLSearchParams
|
params: URLSearchParams
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export type OverviewMapProps = {
|
|
||||||
mapId: string
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export type OverviewMapContainerProps = {
|
|
||||||
apiKey: string
|
|
||||||
mapId: string
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user