Use the interactive map on the map page

This commit is contained in:
Niclas Edenvin
2024-09-11 13:33:26 +02:00
parent 1e7c24f875
commit 03fa798670
13 changed files with 259 additions and 73 deletions

View File

@@ -0,0 +1,6 @@
.main {
display: grid;
background-color: var(--Scandic-Brand-Warm-White);
min-height: 100dvh;
grid-template-columns: 420px 1fr;
}

View File

@@ -0,0 +1,53 @@
import { env } from "@/env/server"
import {
fetchAvailableHotels,
getFiltersFromHotels,
} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils"
import SelectHotelMap from "@/components/HotelReservation/SelectHotel/SelectHotelMap"
import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { PointOfInterest } from "@/types/hotel"
import { LangParams, PageArgs } from "@/types/params"
export default async function SelectHotelMapPage({
params,
}: PageArgs<LangParams, {}>) {
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
setLang(params.lang)
const hotels = await fetchAvailableHotels({
cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54",
roomStayStartDate: "2024-11-02",
roomStayEndDate: "2024-11-03",
adults: 1,
})
const filters = getFiltersFromHotels(hotels)
// TODO: this is just a quick transformation to get something there. May need rework
const pointOfInterests: PointOfInterest[] = hotels.map((hotel) => ({
coordinates: {
lat: hotel.hotelData.location.latitude,
lng: hotel.hotelData.location.longitude,
},
name: hotel.hotelData.name,
distance: hotel.hotelData.location.distanceToCentre,
category: "Hotel",
}))
return (
<main className={styles.main}>
<SelectHotelMap
apiKey={googleMapsApiKey}
// TODO: use correct coordinates. The city center?
coordinates={{ lat: 59.32, lng: 18.01 }}
pointsOfInterest={pointOfInterests}
mapId={googleMapId}
/>
</main>
)
}

View File

@@ -1,45 +1,21 @@
import { serverClient } from "@/lib/trpc/server"
import { selectHotelMap } from "@/constants/routes/hotelReservation"
import {
fetchAvailableHotels,
getFiltersFromHotels,
} from "@/app/[lang]/(live)/(public)/hotelreservation/select-hotel/utils"
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
import { ChevronRightIcon } from "@/components/Icons"
import StaticMap from "@/components/Maps/StaticMap"
import Link from "@/components/TempDesignSystem/Link"
import { getIntl } from "@/i18n"
import { getLang, setLang } from "@/i18n/serverContext"
import { setLang } from "@/i18n/serverContext"
import styles from "./page.module.css"
import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
import { LangParams, PageArgs } from "@/types/params"
async function getAvailableHotels(
input: AvailabilityInput
): Promise<HotelData[]> {
const getAvailableHotels = await serverClient().hotel.availability.get(input)
if (!getAvailableHotels) throw new Error()
const { availability } = getAvailableHotels
const hotels = availability.map(async (hotel) => {
const hotelData = await serverClient().hotel.hotelData.get({
hotelId: hotel.hotelId.toString(),
language: getLang(),
})
if (!hotelData) throw new Error()
return {
hotelData: hotelData.data.attributes,
price: hotel.bestPricePerNight,
}
})
return await Promise.all(hotels)
}
export default async function SelectHotelPage({
params,
}: PageArgs<LangParams>) {
@@ -48,54 +24,34 @@ export default async function SelectHotelPage({
const tempSearchTerm = "Stockholm"
const intl = await getIntl()
const hotels = await getAvailableHotels({
const hotels = await fetchAvailableHotels({
cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54",
roomStayStartDate: "2024-11-02",
roomStayEndDate: "2024-11-03",
adults: 1,
})
const filters = hotels.flatMap((data) => data.hotelData.detailedFacilities)
const filterIds = [...new Set(filters.map((data) => data.id))]
const filterList: {
name: string
id: number
applyToAllHotels: boolean
public: boolean
icon: string
sortOrder: number
code?: string
iconName?: string
}[] = filterIds
.map((id) => filters.find((find) => find.id === id))
.filter(
(
filter
): filter is {
name: string
id: number
applyToAllHotels: boolean
public: boolean
icon: string
sortOrder: number
code?: string
iconName?: string
} => filter !== undefined
)
const filterList = getFiltersFromHotels(hotels)
return (
<main className={styles.main}>
<section className={styles.section}>
<StaticMap
city={tempSearchTerm}
width={340}
height={180}
zoomLevel={11}
mapType="roadmap"
altText={`Map of ${tempSearchTerm} city center`}
/>
<Link className={styles.link} color="burgundy" href="#">
<Link href={selectHotelMap[params.lang]} keepSearchParams>
<StaticMap
city={tempSearchTerm}
width={340}
height={180}
zoomLevel={11}
mapType="roadmap"
altText={`Map of ${tempSearchTerm} city center`}
/>
</Link>
<Link
className={styles.link}
color="burgundy"
href={selectHotelMap[params.lang]}
keepSearchParams
>
{intl.formatMessage({ id: "Show map" })}
<ChevronRightIcon color="burgundy" />
</Link>

View File

@@ -0,0 +1,43 @@
import { serverClient } from "@/lib/trpc/server"
import { getLang } from "@/i18n/serverContext"
import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput"
import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
import { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
export async function fetchAvailableHotels(
input: AvailabilityInput
): Promise<HotelData[]> {
const availableHotels = await serverClient().hotel.availability.get(input)
if (!availableHotels) throw new Error()
const language = getLang()
const hotels = availableHotels.availability.map(async (hotel) => {
const hotelData = await serverClient().hotel.hotelData.get({
hotelId: hotel.hotelId.toString(),
language,
})
if (!hotelData) throw new Error()
return {
hotelData: hotelData.data.attributes,
price: hotel.bestPricePerNight,
}
})
return await Promise.all(hotels)
}
export function getFiltersFromHotels(hotels: HotelData[]) {
const filters = hotels.flatMap((hotel) => hotel.hotelData.detailedFacilities)
const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))]
const filterList: Filter[] = uniqueFilterIds
.map((filterId) => filters.find((filter) => filter.id === filterId))
.filter((filter): filter is Filter => filter !== undefined)
return filterList
}

View File

@@ -4,7 +4,7 @@ import { useIntl } from "react-intl"
import styles from "./hotelFilter.module.css"
import { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFiltersProps"
import { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFilters"
export default function HotelFilter({ filters }: HotelFiltersProps) {
const intl = useIntl()

View File

@@ -0,0 +1,11 @@
"use client"
import { HotelListingProps } from "@/types/components/hotelReservation/selectHotel/map"
// TODO: This component is copied from
// components/ContentType/HotelPage/Map/DynamicMap/Sidebar.
// Look at that for inspiration on how to do the interaction with the map.
export default function HotelListing({}: HotelListingProps) {
return <section>Hotel listing TBI</section>
}

View File

@@ -0,0 +1,57 @@
"use client"
import { APIProvider } from "@vis.gl/react-google-maps"
import { useState } from "react"
import { useIntl } from "react-intl"
import { selectHotel } from "@/constants/routes/hotelReservation"
import { CloseIcon } from "@/components/Icons"
import InteractiveMap from "@/components/Maps/InteractiveMap"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import useLang from "@/hooks/useLang"
import HotelListing from "./HotelListing"
import styles from "./selectHotelMap.module.css"
import { SelectHotelMapProps } from "@/types/components/hotelReservation/selectHotel/map"
export default function SelectHotelMap({
apiKey,
coordinates,
pointsOfInterest,
mapId,
}: SelectHotelMapProps) {
const lang = useLang()
const intl = useIntl()
const [activePoi, setActivePoi] = useState<string | null>(null)
const closeButton = (
<Button
asChild
intent="inverted"
size="small"
theme="base"
className={styles.closeButton}
>
<Link href={selectHotel[lang]} keepSearchParams color="burgundy">
<CloseIcon color="burgundy" />
{intl.formatMessage({ id: "Close the map" })}
</Link>
</Button>
)
return (
<APIProvider apiKey={apiKey}>
<HotelListing />
<InteractiveMap
closeButton={closeButton}
coordinates={coordinates}
pointsOfInterest={pointsOfInterest}
activePoi={activePoi}
onActivePoiChange={setActivePoi}
mapId={mapId}
/>
</APIProvider>
)
}

View File

@@ -0,0 +1,5 @@
.closeButton {
pointer-events: initial;
box-shadow: var(--button-box-shadow);
gap: var(--Spacing-x-half);
}

View File

@@ -1,7 +1,7 @@
"use client"
import NextLink from "next/link"
import { usePathname, useRouter } from "next/navigation"
import { startTransition, useCallback } from "react"
import { usePathname, useRouter, useSearchParams } from "next/navigation"
import { startTransition, useCallback, useMemo } from "react"
import useRouterTransitionStore from "@/stores/router-transition"
@@ -24,9 +24,14 @@ export default function Link({
variant,
trackingId,
onClick,
/**
* Decides if the link should include the current search params in the URL
*/
keepSearchParams,
...props
}: LinkProps) {
const currentPageSlug = usePathname()
const searchParams = useSearchParams()
let isActive = active || currentPageSlug === href
if (partialMatch && !isActive) {
@@ -44,6 +49,12 @@ export default function Link({
const router = useRouter()
const fullUrl = useMemo(() => {
const search =
keepSearchParams && searchParams.size ? `?${searchParams}` : ""
return `${href}${search}`
}, [href, searchParams, keepSearchParams])
const startRouterTransition = useRouterTransitionStore(
(state) => state.startRouterTransition
)
@@ -75,10 +86,10 @@ export default function Link({
trackPageViewStart()
startTransition(() => {
startRouterTransition()
router.push(href, { scroll })
router.push(fullUrl, { scroll })
})
}}
href={href}
href={fullUrl}
id={trackingId}
{...props}
/>

View File

@@ -10,4 +10,5 @@ export interface LinkProps
partialMatch?: boolean
prefetch?: boolean
trackingId?: string
keepSearchParams?: boolean
}

View File

@@ -8,4 +8,24 @@ export const hotelReservation = {
de: "/de/hotelreservierung",
}
// TODO: Translate paths
export const selectHotel = {
en: `${hotelReservation.en}/select-hotel`,
sv: `${hotelReservation.sv}/select-hotel`,
no: `${hotelReservation.no}/select-hotel`,
fi: `${hotelReservation.fi}/select-hotel`,
da: `${hotelReservation.da}/select-hotel`,
de: `${hotelReservation.de}/select-hotel`,
}
// TODO: Translate paths
export const selectHotelMap = {
en: `${selectHotel.en}/map`,
sv: `${selectHotel.sv}/map`,
no: `${selectHotel.no}/map`,
fi: `${selectHotel.fi}/map`,
da: `${selectHotel.da}/map`,
de: `${selectHotel.de}/map`,
}
export const bookingFlow = [...Object.values(hotelReservation)]

View File

@@ -3,3 +3,11 @@ import { Hotel } from "@/types/hotel"
export type HotelFiltersProps = {
filters: Hotel["detailedFacilities"]
}
export type Filter = {
name: string
id: number
public: boolean
sortOrder: number
code?: string
}

View File

@@ -0,0 +1,15 @@
import { Coordinates } from "@/types/components/maps/coordinates"
import type { PointOfInterest } from "@/types/hotel"
export interface HotelListingProps {
// pointsOfInterest: PointOfInterest[]
// activePoi: PointOfInterest["name"] | null
// onActivePoiChange: (poi: PointOfInterest["name"] | null) => void
}
export interface SelectHotelMapProps {
apiKey: string
coordinates: Coordinates
pointsOfInterest: PointOfInterest[]
mapId: string
}