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

@@ -0,0 +1,19 @@
.container {
display: grid;
gap: var(--Spacing-x3);
}
.citiesList {
column-count: 2;
list-style-type: none;
margin-bottom: var(--Spacing-x-half);
}
.citiesList li {
margin-bottom: var(--Spacing-x-one-and-half);
}
@media screen and (min-width: 1367px) {
.citiesList {
column-count: 3;
}
}

View File

@@ -0,0 +1,54 @@
import { ArrowRightIcon } from "@/components/Icons"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Link from "@/components/TempDesignSystem/Link"
import { getIntl } from "@/i18n"
import styles from "./destination.module.css"
import type { DestinationProps } from "@/types/components/destinationOverviewPage/destinationsList/destinationsData"
export default async function Destination({
country,
countryUrl,
numberOfHotels,
cities,
}: DestinationProps) {
const intl = await getIntl()
const accordionSubtitle = intl.formatMessage(
{
id: "{amount, plural, one {# hotel} other {# hotels}}",
},
{ amount: numberOfHotels }
)
return (
<AccordionItem title={country} subtitle={accordionSubtitle}>
<div className={styles.container}>
<ul className={styles.citiesList}>
{cities.map((city) => (
<li key={city.id}>
<Link
href={city.url ? city.url : ""}
color="baseTextMediumContrast"
textDecoration="underline"
>
{`${city.name} (${city.hotelCount})`}
</Link>
</li>
))}
</ul>
{countryUrl && (
<Link href={countryUrl} variant="icon" color="burgundy" weight="bold">
{intl.formatMessage(
{
id: "View all hotels in {country}",
},
{ country: country }
)}
<ArrowRightIcon color="burgundy" />
</Link>
)}
</div>
</AccordionItem>
)
}

View File

@@ -0,0 +1,27 @@
.listContainer {
display: flex;
flex-direction: column;
background-color: var(--Base-Surface-Primary-light-Normal);
border-radius: var(--Corner-radius-Medium);
}
.accordion {
flex: 1;
height: fit-content;
}
@media screen and (min-width: 768px) {
.listContainer {
gap: var(--Spacing-x3);
background-color: transparent;
flex-direction: row;
}
.accordion {
background-color: var(--Base-Surface-Primary-light-Normal);
}
.divider {
display: none;
}
}

View File

@@ -0,0 +1,44 @@
import Accordion from "@/components/TempDesignSystem/Accordion"
import Divider from "@/components/TempDesignSystem/Divider"
import Destination from "./Destination"
import styles from "./destinationsList.module.css"
import type { DestinationsListProps } from "@/types/components/destinationOverviewPage/destinationsList/destinationsData"
export default function DestinationsList({
destinations,
}: DestinationsListProps) {
const middleIndex = Math.ceil(destinations.length / 2)
const accordionLeft = destinations.slice(0, middleIndex)
const accordionRight = destinations.slice(middleIndex)
return (
<div className={styles.listContainer}>
<Accordion className={styles.accordion}>
{accordionLeft.map((data) => (
<Destination
key={data.country}
country={data.country}
countryUrl={data.countryUrl}
numberOfHotels={data.numberOfHotels}
cities={data.cities}
/>
))}
</Accordion>
<Divider color="subtle" className={styles.divider} />
<Accordion className={styles.accordion}>
{accordionRight.map((data) => (
<Destination
key={data.country}
country={data.country}
countryUrl={data.countryUrl}
numberOfHotels={data.numberOfHotels}
cities={data.cities}
/>
))}
</Accordion>
</div>
)
}

View File

@@ -0,0 +1,11 @@
.container {
display: grid;
gap: var(--Spacing-x4);
padding: var(--Spacing-x5) 0 var(--Spacing-x7);
}
@media screen and (min-width: 768px) {
.container {
gap: var(--Spacing-x7);
}
}

View File

@@ -0,0 +1,23 @@
import Title from "@/components/TempDesignSystem/Text/Title"
import { getIntl } from "@/i18n"
import DestinationsList from "./DestinationsList"
import styles from "./hotelsSection.module.css"
import type { HotelsSectionProps } from "@/types/components/destinationOverviewPage/destinationsList/destinationsData"
export default async function HotelsSection({
destinations,
}: HotelsSectionProps) {
const intl = await getIntl()
return (
<section className={styles.container}>
<Title level="h4" as="h2" textAlign="center">
{intl.formatMessage({ id: "Explore all our hotels" })}
</Title>
<DestinationsList destinations={destinations} />
</section>
)
}

View File

@@ -0,0 +1,19 @@
"use client"
import { useIntl } from "react-intl"
import styles from "./inputFrom.module.css"
export default function InputForm() {
const intl = useIntl()
return (
<form className={styles.form}>
<input
type="text"
placeholder={intl.formatMessage({
id: "Find hotels and destinations",
})}
className={styles.formInput}
/>
</form>
)
}

View File

@@ -0,0 +1,14 @@
.form {
position: absolute;
top: 25px;
left: 50%;
transform: translateX(-50%);
z-index: 1;
background-color: var(--Base-Background-Primary-Normal);
}
.formInput {
padding: var(--Spacing-x-one-and-half);
border: none;
width: 250px;
}

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

@@ -0,0 +1,29 @@
.mapContainer {
position: relative;
display: grid;
width: 100%;
max-width: var(--max-width);
height: 700px;
margin: 0 auto;
}
.main {
display: grid;
gap: var(--Spacing-x9);
padding: var(--Spacing-x7) 0 var(--Spacing-x9);
background-color: var(--Base-Surface-Primary-light-Normal);
}
.blocks {
display: grid;
gap: var(--Spacing-x9);
width: 100%;
max-width: var(--max-width-content);
margin: 0 auto;
}
.hotelsAccordions {
width: 100%;
max-width: var(--max-width-content);
margin: 0 auto;
}

View File

@@ -0,0 +1,48 @@
import { Suspense } from "react"
import {
getDestinationOverviewPage,
getDestinationsList,
} from "@/lib/trpc/memoizedRequests"
import Blocks from "@/components/Blocks"
import TrackingSDK from "@/components/TrackingSDK"
import HotelsSection from "./HotelsSection"
import OverviewMapContainer from "./OverviewMapContainer"
import styles from "./destinationOverviewPage.module.css"
export default async function DestinationOverviewPage() {
const [pageData, destinationsData] = await Promise.all([
getDestinationOverviewPage(),
getDestinationsList(),
])
if (!pageData) {
return null
}
const { tracking, destinationOverviewPage } = pageData
return (
<>
<div className={styles.mapContainer}>
<OverviewMapContainer />
</div>
<main className={styles.main}>
<div className={styles.blocks}>
<Blocks blocks={destinationOverviewPage.blocks} />
</div>
</main>
{destinationsData && (
<aside className={styles.hotelsAccordions}>
<HotelsSection destinations={destinationsData} />
</aside>
)}
<Suspense fallback={null}>
<TrackingSDK pageData={tracking} />
</Suspense>
</>
)
}

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
}