Use the interactive map on the map page
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
.main {
|
||||||
|
display: grid;
|
||||||
|
background-color: var(--Scandic-Brand-Warm-White);
|
||||||
|
min-height: 100dvh;
|
||||||
|
grid-template-columns: 420px 1fr;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||||
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
||||||
import { ChevronRightIcon } from "@/components/Icons"
|
import { ChevronRightIcon } from "@/components/Icons"
|
||||||
import StaticMap from "@/components/Maps/StaticMap"
|
import StaticMap from "@/components/Maps/StaticMap"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang, setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import styles from "./page.module.css"
|
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"
|
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({
|
export default async function SelectHotelPage({
|
||||||
params,
|
params,
|
||||||
}: PageArgs<LangParams>) {
|
}: PageArgs<LangParams>) {
|
||||||
@@ -48,54 +24,34 @@ export default async function SelectHotelPage({
|
|||||||
const tempSearchTerm = "Stockholm"
|
const tempSearchTerm = "Stockholm"
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
|
||||||
const hotels = await getAvailableHotels({
|
const hotels = await fetchAvailableHotels({
|
||||||
cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54",
|
cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54",
|
||||||
roomStayStartDate: "2024-11-02",
|
roomStayStartDate: "2024-11-02",
|
||||||
roomStayEndDate: "2024-11-03",
|
roomStayEndDate: "2024-11-03",
|
||||||
adults: 1,
|
adults: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
const filters = hotels.flatMap((data) => data.hotelData.detailedFacilities)
|
const filterList = getFiltersFromHotels(hotels)
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
<section className={styles.section}>
|
<section className={styles.section}>
|
||||||
<StaticMap
|
<Link href={selectHotelMap[params.lang]} keepSearchParams>
|
||||||
city={tempSearchTerm}
|
<StaticMap
|
||||||
width={340}
|
city={tempSearchTerm}
|
||||||
height={180}
|
width={340}
|
||||||
zoomLevel={11}
|
height={180}
|
||||||
mapType="roadmap"
|
zoomLevel={11}
|
||||||
altText={`Map of ${tempSearchTerm} city center`}
|
mapType="roadmap"
|
||||||
/>
|
altText={`Map of ${tempSearchTerm} city center`}
|
||||||
<Link className={styles.link} color="burgundy" href="#">
|
/>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
className={styles.link}
|
||||||
|
color="burgundy"
|
||||||
|
href={selectHotelMap[params.lang]}
|
||||||
|
keepSearchParams
|
||||||
|
>
|
||||||
{intl.formatMessage({ id: "Show map" })}
|
{intl.formatMessage({ id: "Show map" })}
|
||||||
<ChevronRightIcon color="burgundy" />
|
<ChevronRightIcon color="burgundy" />
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import styles from "./hotelFilter.module.css"
|
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) {
|
export default function HotelFilter({ filters }: HotelFiltersProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|||||||
@@ -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>
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
.closeButton {
|
||||||
|
pointer-events: initial;
|
||||||
|
box-shadow: var(--button-box-shadow);
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
import NextLink from "next/link"
|
import NextLink from "next/link"
|
||||||
import { usePathname, useRouter } from "next/navigation"
|
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||||
import { startTransition, useCallback } from "react"
|
import { startTransition, useCallback, useMemo } from "react"
|
||||||
|
|
||||||
import useRouterTransitionStore from "@/stores/router-transition"
|
import useRouterTransitionStore from "@/stores/router-transition"
|
||||||
|
|
||||||
@@ -24,9 +24,14 @@ export default function Link({
|
|||||||
variant,
|
variant,
|
||||||
trackingId,
|
trackingId,
|
||||||
onClick,
|
onClick,
|
||||||
|
/**
|
||||||
|
* Decides if the link should include the current search params in the URL
|
||||||
|
*/
|
||||||
|
keepSearchParams,
|
||||||
...props
|
...props
|
||||||
}: LinkProps) {
|
}: LinkProps) {
|
||||||
const currentPageSlug = usePathname()
|
const currentPageSlug = usePathname()
|
||||||
|
const searchParams = useSearchParams()
|
||||||
let isActive = active || currentPageSlug === href
|
let isActive = active || currentPageSlug === href
|
||||||
|
|
||||||
if (partialMatch && !isActive) {
|
if (partialMatch && !isActive) {
|
||||||
@@ -44,6 +49,12 @@ export default function Link({
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const fullUrl = useMemo(() => {
|
||||||
|
const search =
|
||||||
|
keepSearchParams && searchParams.size ? `?${searchParams}` : ""
|
||||||
|
return `${href}${search}`
|
||||||
|
}, [href, searchParams, keepSearchParams])
|
||||||
|
|
||||||
const startRouterTransition = useRouterTransitionStore(
|
const startRouterTransition = useRouterTransitionStore(
|
||||||
(state) => state.startRouterTransition
|
(state) => state.startRouterTransition
|
||||||
)
|
)
|
||||||
@@ -75,10 +86,10 @@ export default function Link({
|
|||||||
trackPageViewStart()
|
trackPageViewStart()
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
startRouterTransition()
|
startRouterTransition()
|
||||||
router.push(href, { scroll })
|
router.push(fullUrl, { scroll })
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
href={href}
|
href={fullUrl}
|
||||||
id={trackingId}
|
id={trackingId}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,4 +10,5 @@ export interface LinkProps
|
|||||||
partialMatch?: boolean
|
partialMatch?: boolean
|
||||||
prefetch?: boolean
|
prefetch?: boolean
|
||||||
trackingId?: string
|
trackingId?: string
|
||||||
|
keepSearchParams?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,24 @@ export const hotelReservation = {
|
|||||||
de: "/de/hotelreservierung",
|
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)]
|
export const bookingFlow = [...Object.values(hotelReservation)]
|
||||||
|
|||||||
@@ -3,3 +3,11 @@ import { Hotel } from "@/types/hotel"
|
|||||||
export type HotelFiltersProps = {
|
export type HotelFiltersProps = {
|
||||||
filters: Hotel["detailedFacilities"]
|
filters: Hotel["detailedFacilities"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Filter = {
|
||||||
|
name: string
|
||||||
|
id: number
|
||||||
|
public: boolean
|
||||||
|
sortOrder: number
|
||||||
|
code?: string
|
||||||
|
}
|
||||||
15
types/components/hotelReservation/selectHotel/map.ts
Normal file
15
types/components/hotelReservation/selectHotel/map.ts
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user