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:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
.zoomButtons {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user