feat(SW-1780): Hotel address now leads to map view with hotel active

Approved-by: Michael Zetterberg
Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-04-28 09:12:17 +00:00
parent 416a95d592
commit 5beffe4968
6 changed files with 189 additions and 125 deletions

View File

@@ -24,6 +24,7 @@ export default function ButtonLink({
color,
size,
typography,
wrapping,
className,
href,
target,
@@ -38,7 +39,7 @@ export default function ButtonLink({
variant,
color,
size,
wrapping,
typography,
className,
})

View File

@@ -34,6 +34,11 @@
background-color: var(--Base-Surface-Primary-light-Normal);
padding: var(--Spacing-x-quarter) var(--Spacing-x1);
border-radius: var(--Corner-radius-Small);
color: var(--Text-Interactive-Default);
}
.hotelName {
color: var(--Text-Default);
}
.intro {
@@ -44,15 +49,26 @@
.captions {
display: flex;
gap: var(--Spacing-x1);
color: var(--Text-Tertiary);
}
.addressButton {
background-color: transparent;
border-width: 0;
padding: 0;
color: var(--Text-Interactive-Secondary);
cursor: pointer;
&:hover {
color: var(--Text-Interactive-Hover-Secondary);
}
}
.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);
color: var(--Text-Secondary);
}
.amenityItem {

View File

@@ -1,20 +1,19 @@
"use client"
import Link from "next/link"
import { useCallback, useEffect, useRef } from "react"
import { Button as AriaButton } from "react-aria-components"
import { useIntl } from "react-intl"
import HotelLogoIcon from "@scandic-hotels/design-system/Icons/HotelLogoIcon"
import TripadvisorIcon from "@scandic-hotels/design-system/Icons/TripadvisorIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
import ButtonLink from "@/components/ButtonLink"
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
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"
@@ -27,9 +26,11 @@ export default function HotelListItem(data: DestinationPagesHotelData) {
const { hotel, url } = data
const galleryImages = mapApiImagesToGalleryImages(hotel.galleryImages || [])
const amenities = hotel.detailedFacilities.slice(0, 5)
const address = `${hotel.address.streetAddress}, ${hotel.address.city}`
const itemRef = useRef<HTMLElement>(null)
const { setHoveredMarker, activeMarker } = useDestinationPageHotelsMapStore()
const { setHoveredMarker, activeMarker, setActiveMarker } =
useDestinationPageHotelsMapStore()
useEffect(() => {
if (activeMarker === hotel.id) {
@@ -74,10 +75,12 @@ export default function HotelListItem(data: DestinationPagesHotelData) {
)}
/>
{hotel.tripadvisor && (
<div className={styles.tripAdvisor}>
<TripadvisorIcon color="Icon/Interactive/Default" />
<Caption color="burgundy">{hotel.tripadvisor}</Caption>
</div>
<Typography variant="Title/Overline/sm">
<div className={styles.tripAdvisor}>
<TripadvisorIcon color="CurrentColor" />
<span>{hotel.tripadvisor}</span>
</div>
</Typography>
)}
</div>
<div className={styles.content}>
@@ -85,50 +88,63 @@ export default function HotelListItem(data: DestinationPagesHotelData) {
<div className={styles.logo}>
<HotelLogoIcon hotelId={hotel.id} hotelType={hotel.hotelType} />
</div>
<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(
{
defaultMessage: "{number} km to city center",
},
{
number: getSingleDecimal(
hotel.location.distanceToCentre / 1000
),
}
)}
</Caption>
</div>
<Typography variant="Title/Subtitle/lg">
<h3 className={styles.hotelName}>{hotel.name}</h3>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<div className={styles.captions}>
<Typography variant="Link/sm">
<AriaButton
className={styles.addressButton}
onPress={() => setActiveMarker(hotel.id)}
>
{address}
</AriaButton>
</Typography>
<Divider variant="vertical" color="beige" />
<p>
{intl.formatMessage(
{
defaultMessage: "{number} km to city center",
},
{
number: getSingleDecimal(
hotel.location.distanceToCentre / 1000
),
}
)}
</p>
</div>
</Typography>
</div>
<ul className={styles.amenityList}>
{amenities.map((amenity) => {
const Icon = (
<FacilityToIcon id={amenity.id} color="Icon/Default" size={20} />
)
return (
<li className={styles.amenityItem} key={amenity.id}>
{Icon && Icon}
<span className={styles.amenityName}>{amenity.name}</span>
</li>
)
})}
</ul>
<Typography variant="Body/Supporting text (caption)/smRegular">
<ul className={styles.amenityList}>
{amenities.map((amenity) => {
return (
<li className={styles.amenityItem} key={amenity.id}>
<FacilityToIcon
id={amenity.id}
color="CurrentColor"
size={20}
/>
{amenity.name}
</li>
)
})}
</ul>
</Typography>
{url && (
<div className={styles.ctaWrapper}>
<Button intent="tertiary" theme="base" size="small" asChild>
<Link href={url}>
{intl.formatMessage({
defaultMessage: "See hotel details",
})}
</Link>
</Button>
<ButtonLink
href={url}
variant="Tertiary"
color="Primary"
size="Small"
>
{intl.formatMessage({
defaultMessage: "See hotel details",
})}
</ButtonLink>
</div>
)}
</div>

View File

@@ -28,6 +28,11 @@
background-color: var(--Base-Surface-Primary-light-Normal);
padding: var(--Spacing-x-quarter) var(--Spacing-x1);
border-radius: var(--Corner-radius-Small);
color: var(--Text-Interactive-Default);
}
.hotelName {
color: var(--Text-Default);
}
.intro {
@@ -38,15 +43,22 @@
.captions {
display: flex;
gap: var(--Spacing-x1);
color: var(--Text-Tertiary);
}
.addressLink {
color: var(--Text-Interactive-Secondary);
&:hover {
color: var(--Text-Interactive-Hover-Secondary);
}
}
.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);
color: var(--Text-Secondary);
}
.amenityItem {

View File

@@ -1,6 +1,6 @@
"use client"
import Link from "next/link"
import NextLink from "next/link"
import { useParams } from "next/navigation"
import { useEffect, useState } from "react"
import { useIntl } from "react-intl"
@@ -12,12 +12,10 @@ import { Typography } from "@scandic-hotels/design-system/Typography"
import { useDestinationPageHotelsMapStore } from "@/stores/destination-page-hotels-map"
import ButtonLink from "@/components/ButtonLink"
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
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"
@@ -34,6 +32,8 @@ export default function HotelListingItem(data: DestinationPagesHotelData) {
const amenities = hotel.detailedFacilities.slice(0, 5)
const [mapUrl, setMapUrl] = useState<string | null>(null)
const address = `${hotel.address.streetAddress}, ${hotel.address.city}`
useEffect(() => {
const url = new URL(window.location.href)
url.searchParams.set("view", "map")
@@ -55,85 +55,106 @@ export default function HotelListingItem(data: DestinationPagesHotelData) {
)}
/>
{hotel.tripadvisor && (
<div className={styles.tripAdvisor}>
<TripadvisorIcon color="Icon/Interactive/Default" />
<Caption color="burgundy">{hotel.tripadvisor}</Caption>
</div>
<Typography variant="Title/Overline/sm">
<div className={styles.tripAdvisor}>
<TripadvisorIcon color="CurrentColor" />
<span>{hotel.tripadvisor}</span>
</div>
</Typography>
)}
</div>
<div className={styles.content}>
<div className={styles.intro}>
<HotelLogoIcon hotelId={hotel.id} 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(
{
defaultMessage: "{number} km to city center",
},
{
number: getSingleDecimal(
hotel.location.distanceToCentre / 1000
),
}
)}
</Caption>
</div>
<Typography variant="Title/Subtitle/lg">
<h3 className={styles.hotelName}>{hotel.name}</h3>
</Typography>
<Typography variant="Body/Supporting text (caption)/smRegular">
<div className={styles.captions}>
{mapUrl ? (
<>
<Typography variant="Link/sm">
<NextLink
onClick={() => setActiveMarker(hotel.id)}
href={mapUrl}
className={styles.addressLink}
aria-label={intl.formatMessage({
defaultMessage: "See on map",
})}
>
{address}
</NextLink>
</Typography>
<Divider variant="vertical" color="beige" />
</>
) : null}
<p>
{intl.formatMessage(
{
defaultMessage: "{number} km to city center",
},
{
number: getSingleDecimal(
hotel.location.distanceToCentre / 1000
),
}
)}
</p>
</div>
</Typography>
</div>
{hotel.hotelDescription ? (
<Typography variant="Body/Paragraph/mdRegular">
<p>{hotel.hotelDescription}</p>
</Typography>
) : null}
<ul className={styles.amenityList}>
{amenities.map((amenity) => {
const Icon = (
<FacilityToIcon id={amenity.id} color="Icon/Default" size={20} />
)
return (
<li className={styles.amenityItem} key={amenity.id}>
{Icon && Icon}
{amenity.name}
</li>
)
})}
</ul>
<Typography variant="Body/Supporting text (caption)/smRegular">
<ul className={styles.amenityList}>
{amenities.map((amenity) => {
return (
<li className={styles.amenityItem} key={amenity.id}>
<FacilityToIcon
id={amenity.id}
color="CurrentColor"
size={20}
/>
{amenity.name}
</li>
)
})}
</ul>
</Typography>
{mapUrl && (
<Button intent="text" variant="icon" theme="base" asChild>
<Link
href={mapUrl}
scroll={true}
onClick={() => setActiveMarker(hotel.id)}
>
{intl.formatMessage({
defaultMessage: "See on map",
})}
<MaterialIcon
icon="chevron_right"
size={20}
color="CurrentColor"
/>
</Link>
</Button>
<ButtonLink
href={mapUrl}
scroll={true}
variant="Text"
color="Primary"
size="Medium"
wrapping={false}
onClick={() => setActiveMarker(hotel.id)}
>
{intl.formatMessage({
defaultMessage: "See on map",
})}
<MaterialIcon icon="chevron_right" size={24} color="CurrentColor" />
</ButtonLink>
)}
{url && (
<>
<Divider variant="horizontal" color="primaryLightSubtle" />
<div className={styles.ctaWrapper}>
<Button intent="tertiary" theme="base" size="small" asChild>
<Link href={url}>
{intl.formatMessage({
defaultMessage: "See hotel details",
})}
</Link>
</Button>
<ButtonLink
href={url}
variant="Tertiary"
color="Primary"
size="Small"
>
{intl.formatMessage({
defaultMessage: "See hotel details",
})}
</ButtonLink>
</div>
</>
)}

View File

@@ -519,9 +519,7 @@ export async function getHotelsByHotelIds({
const data: DestinationPagesHotelData = {
hotel: {
id: hotel.id,
galleryImages: hotel.galleryImages?.length
? [hotel.galleryImages[0]]
: [],
galleryImages: hotel.galleryImages,
name: hotel.name,
tripadvisor: hotel.ratings?.tripAdvisor?.rating,
detailedFacilities: hotel.detailedFacilities || [],