Merged in feat/SW-1458-sidebar-city-map (pull request #1342)
feat(SW-1458): Added hotel list inside sidebar for mapview on city pages * feat(SW-1458): Added hotel list inside sidebar for mapview on city pages Approved-by: Fredrik Thorsson
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
.hotelListWrapper {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.hotelList {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
list-style: none;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
"use client"
|
||||
|
||||
import { useMap, useMapsLibrary } from "@vis.gl/react-google-maps"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import { debounce } from "@/utils/debounce"
|
||||
|
||||
import HotelListItem from "../HotelListItem"
|
||||
import { getVisibleHotels } from "./utils"
|
||||
|
||||
import styles from "./hotelList.module.css"
|
||||
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
|
||||
interface HotelListProps {
|
||||
hotels: HotelDataWithUrl[]
|
||||
}
|
||||
|
||||
export default function HotelList({ hotels }: HotelListProps) {
|
||||
const intl = useIntl()
|
||||
const map = useMap()
|
||||
const coreLib = useMapsLibrary("core")
|
||||
const [visibleHotels, setVisibleHotels] = useState<HotelDataWithUrl[]>([])
|
||||
|
||||
const debouncedUpdateVisibleHotels = useMemo(
|
||||
() =>
|
||||
debounce(() => {
|
||||
setVisibleHotels(getVisibleHotels(hotels, map))
|
||||
}, 500),
|
||||
[map, hotels]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!map || !coreLib) {
|
||||
return
|
||||
}
|
||||
|
||||
function handleBoundsChanged() {
|
||||
debouncedUpdateVisibleHotels()
|
||||
}
|
||||
|
||||
coreLib.event.addListener(map, "bounds_changed", handleBoundsChanged)
|
||||
return () => {
|
||||
coreLib.event.clearListeners(map, "bounds_changed")
|
||||
}
|
||||
}, [map, coreLib, debouncedUpdateVisibleHotels])
|
||||
|
||||
return (
|
||||
<div className={styles.hotelListWrapper}>
|
||||
<div className={styles.header}>
|
||||
<Body>
|
||||
{intl.formatMessage(
|
||||
{ id: "{count} hotels" },
|
||||
{ count: visibleHotels.length }
|
||||
)}
|
||||
</Body>
|
||||
</div>
|
||||
<ul className={styles.hotelList}>
|
||||
{visibleHotels.map(({ hotel, url }) => (
|
||||
<li key={hotel.operaId}>
|
||||
<HotelListItem hotel={hotel} url={url} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
|
||||
export function getVisibleHotels(
|
||||
hotels: HotelDataWithUrl[],
|
||||
map: google.maps.Map | null
|
||||
) {
|
||||
const bounds = map?.getBounds()
|
||||
if (!bounds) {
|
||||
return []
|
||||
}
|
||||
|
||||
const visibleHotels = hotels.filter(({ hotel }) => {
|
||||
const { latitude, longitude } = hotel.location
|
||||
return bounds.contains({ lat: latitude, lng: longitude })
|
||||
})
|
||||
return visibleHotels
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
.hotelListItem {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
align-content: start;
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.intro {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-half);
|
||||
}
|
||||
|
||||
.captions {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.amenityList {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
flex-wrap: wrap;
|
||||
color: var(--UI-Text-Medium-contrast);
|
||||
font-family: var(--typography-Body-Regular-fontFamily);
|
||||
font-size: var(--typography-Caption-Underline-fontSize);
|
||||
}
|
||||
|
||||
.amenityItem {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x-half);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ctaWrapper {
|
||||
justify-self: stretch;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import HotelLogo from "@/components/Icons/Logos"
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
|
||||
import { getSingleDecimal } from "@/utils/numberFormatting"
|
||||
|
||||
import styles from "./hotelListItem.module.css"
|
||||
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
|
||||
interface HotelListItemProps {
|
||||
hotel: Hotel
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export default function HotelListItem({ hotel, url }: HotelListItemProps) {
|
||||
const intl = useIntl()
|
||||
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
|
||||
const amenities = hotel.detailedFacilities.slice(0, 5)
|
||||
|
||||
return (
|
||||
<article className={styles.hotelListItem}>
|
||||
<ImageGallery
|
||||
images={galleryImages}
|
||||
title={intl.formatMessage(
|
||||
{ id: "{title} - Image gallery" },
|
||||
{ title: hotel.name }
|
||||
)}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.intro}>
|
||||
<HotelLogo hotelId={hotel.operaId} hotelType={hotel.hotelType} />
|
||||
<Subtitle type="one" asChild>
|
||||
<h3>{hotel.name}</h3>
|
||||
</Subtitle>
|
||||
<div className={styles.captions}>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{hotel.address.streetAddress}
|
||||
</Caption>
|
||||
<Divider variant="vertical" color="beige" />
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage(
|
||||
{ id: "{number} km to city center" },
|
||||
{
|
||||
number: getSingleDecimal(
|
||||
hotel.location.distanceToCentre / 1000
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<ul className={styles.amenityList}>
|
||||
{amenities.map((amenity) => {
|
||||
const IconComponent = mapFacilityToIcon(amenity.id)
|
||||
return (
|
||||
<li className={styles.amenityItem} key={amenity.id}>
|
||||
{IconComponent && (
|
||||
<IconComponent color="grey80" width={20} height={20} />
|
||||
)}
|
||||
{amenity.name}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
{url && (
|
||||
<div className={styles.ctaWrapper}>
|
||||
<Button intent="tertiary" theme="base" size="small" asChild>
|
||||
<Link href={url}>
|
||||
{intl.formatMessage({ id: "See hotel details" })}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -2,8 +2,10 @@ import { env } from "@/env/server"
|
||||
import { getHotelsByCityIdentifier } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import Map from "../../Map"
|
||||
import HotelList from "./HotelList"
|
||||
|
||||
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
|
||||
|
||||
@@ -15,6 +17,7 @@ export function preloadHotels(cityIdentifier: string) {
|
||||
void getHotelsByCityIdentifier(cityIdentifier)
|
||||
}
|
||||
export default async function CityMap({ city, cityIdentifier }: CityMapProps) {
|
||||
const intl = await getIntl()
|
||||
const hotels = await getHotelsByCityIdentifier(cityIdentifier)
|
||||
|
||||
return (
|
||||
@@ -23,11 +26,10 @@ export default async function CityMap({ city, cityIdentifier }: CityMapProps) {
|
||||
mapId={env.GOOGLE_DYNAMIC_MAP_ID}
|
||||
apiKey={env.GOOGLE_STATIC_MAP_KEY}
|
||||
>
|
||||
<div>
|
||||
<Title level="h2" as="h3">
|
||||
{city.name}
|
||||
</Title>
|
||||
</div>
|
||||
<Title level="h2" as="h3" textTransform="regular">
|
||||
{intl.formatMessage({ id: `Hotels in {city}` }, { city: city.name })}
|
||||
</Title>
|
||||
<HotelList hotels={hotels} />
|
||||
</Map>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -258,6 +258,7 @@
|
||||
"Hotel surroundings": "Hotel surroundings",
|
||||
"Hotels": "Hotels",
|
||||
"Hotels & Destinations": "Hotels & Destinations",
|
||||
"Hotels in {city}": "Hotels in {city}",
|
||||
"Hours": "Hours",
|
||||
"How do you want to sleep?": "How do you want to sleep?",
|
||||
"How it works": "How it works",
|
||||
@@ -701,6 +702,7 @@
|
||||
"{count, plural, one {{count} Hotel} other {{count} Hotels}}": "{count, plural, one {{count} Hotel} other {{count} Hotels}}",
|
||||
"{count, plural, one {{count} Location} other {{count} Locations}}": "{count, plural, one {{count} Location} other {{count} Locations}}",
|
||||
"{count} destinations": "{count} destinations",
|
||||
"{count} hotels": "{count} hotels",
|
||||
"{count} lowercase letter": "{count} lowercase letter",
|
||||
"{count} number": "{count} number",
|
||||
"{count} special character": "{count} special character",
|
||||
|
||||
Reference in New Issue
Block a user