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, color,
size, size,
typography, typography,
wrapping,
className, className,
href, href,
target, target,
@@ -38,7 +39,7 @@ export default function ButtonLink({
variant, variant,
color, color,
size, size,
wrapping,
typography, typography,
className, className,
}) })

View File

@@ -34,6 +34,11 @@
background-color: var(--Base-Surface-Primary-light-Normal); background-color: var(--Base-Surface-Primary-light-Normal);
padding: var(--Spacing-x-quarter) var(--Spacing-x1); padding: var(--Spacing-x-quarter) var(--Spacing-x1);
border-radius: var(--Corner-radius-Small); border-radius: var(--Corner-radius-Small);
color: var(--Text-Interactive-Default);
}
.hotelName {
color: var(--Text-Default);
} }
.intro { .intro {
@@ -44,15 +49,26 @@
.captions { .captions {
display: flex; display: flex;
gap: var(--Spacing-x1); 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 { .amenityList {
display: flex; display: flex;
gap: var(--Spacing-x-one-and-half); gap: var(--Spacing-x-one-and-half);
flex-wrap: wrap; flex-wrap: wrap;
color: var(--UI-Text-Medium-contrast); color: var(--Text-Secondary);
font-family: var(--typography-Body-Regular-fontFamily);
font-size: var(--typography-Caption-Underline-fontSize);
} }
.amenityItem { .amenityItem {

View File

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

View File

@@ -28,6 +28,11 @@
background-color: var(--Base-Surface-Primary-light-Normal); background-color: var(--Base-Surface-Primary-light-Normal);
padding: var(--Spacing-x-quarter) var(--Spacing-x1); padding: var(--Spacing-x-quarter) var(--Spacing-x1);
border-radius: var(--Corner-radius-Small); border-radius: var(--Corner-radius-Small);
color: var(--Text-Interactive-Default);
}
.hotelName {
color: var(--Text-Default);
} }
.intro { .intro {
@@ -38,15 +43,22 @@
.captions { .captions {
display: flex; display: flex;
gap: var(--Spacing-x1); gap: var(--Spacing-x1);
color: var(--Text-Tertiary);
}
.addressLink {
color: var(--Text-Interactive-Secondary);
&:hover {
color: var(--Text-Interactive-Hover-Secondary);
}
} }
.amenityList { .amenityList {
display: flex; display: flex;
gap: var(--Spacing-x-one-and-half); gap: var(--Spacing-x-one-and-half);
flex-wrap: wrap; flex-wrap: wrap;
color: var(--UI-Text-Medium-contrast); color: var(--Text-Secondary);
font-family: var(--typography-Body-Regular-fontFamily);
font-size: var(--typography-Caption-Underline-fontSize);
} }
.amenityItem { .amenityItem {

View File

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

View File

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