Merged in feat/SW-2511-hotel-page-map (pull request #2582)

feat(SW-2511): hotel page map and marker improvements

* feat(SW-2511): update hotel page map

* fix(SW-2511): fix issue with identical id's for POIs


Approved-by: Anton Gunnarsson
This commit is contained in:
Matilda Landström
2025-07-31 09:06:48 +00:00
parent ace44d00dc
commit e323ca9914
20 changed files with 139 additions and 101 deletions

View File

@@ -6,12 +6,12 @@ import { useState } from "react"
import { Button as ButtonRAC } from "react-aria-components" import { Button as ButtonRAC } from "react-aria-components"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { logger } from "@scandic-hotels/common/logger"
import { Typography } from "@scandic-hotels/design-system/Typography" import { Typography } from "@scandic-hotels/design-system/Typography"
import { PointOfInterestGroupEnum } from "@scandic-hotels/trpc/enums/pointOfInterest"
import PoiMarker from "@/components/Maps/Markers/Poi" import PoiMarker from "@/components/Maps/Markers/Poi"
import { translatePOIGroup } from "./util"
import styles from "./sidebar.module.css" import styles from "./sidebar.module.css"
import type { SidebarProps } from "@/types/components/hotelPage/map/sidebar" import type { SidebarProps } from "@/types/components/hotelPage/map/sidebar"
@@ -93,42 +93,6 @@ export default function Sidebar({
defaultMessage: "View as list", defaultMessage: "View as list",
}) })
function translatePOIGroup(group: PointOfInterestGroupEnum) {
switch (group) {
case PointOfInterestGroupEnum.PUBLIC_TRANSPORT:
return intl.formatMessage({
defaultMessage: "Public transport",
})
case PointOfInterestGroupEnum.ATTRACTIONS:
return intl.formatMessage({
defaultMessage: "Attractions",
})
case PointOfInterestGroupEnum.BUSINESS:
return intl.formatMessage({
defaultMessage: "Business",
})
case PointOfInterestGroupEnum.LOCATION:
return intl.formatMessage({
defaultMessage: "Location",
})
case PointOfInterestGroupEnum.PARKING:
return intl.formatMessage({
defaultMessage: "Parking",
})
case PointOfInterestGroupEnum.SHOPPING_DINING:
return intl.formatMessage({
defaultMessage: "Shopping & Dining",
})
default:
const option: never = group
logger.warn(`Unsupported group given: ${option}`)
return intl.formatMessage({
defaultMessage: "N/A",
})
}
}
return ( return (
<> <>
<aside <aside
@@ -162,23 +126,31 @@ export default function Sidebar({
<Typography variant="Body/Paragraph/mdBold"> <Typography variant="Body/Paragraph/mdBold">
<h3 className={styles.poiHeading}> <h3 className={styles.poiHeading}>
<PoiMarker group={group} /> <PoiMarker group={group} />
{translatePOIGroup(group)} {translatePOIGroup(group, intl)}
</h3> </h3>
</Typography> </Typography>
<ul className={styles.poiList}> <ul className={styles.poiList}>
{pois.map((poi) => ( {pois.map((poi) => (
<li key={poi.name} className={styles.poiItem}> <li
<Typography variant="Body/Paragraph/mdRegular"> key={poi.name + poi.categoryName}
<ButtonRAC className={styles.poiItem}
className={cx(styles.poiButton, { >
[styles.active]: activePoi === poi.name, <ButtonRAC
})} className={cx(styles.poiButton, {
onHoverStart={() => handleMouseEnter(poi.name)} [styles.active]: activePoi === poi.name,
onPress={() => })}
handlePoiClick(poi.name, poi.coordinates) onHoverStart={() => handleMouseEnter(poi.name)}
} onPress={() =>
> handlePoiClick(poi.name, poi.coordinates)
}
>
<Typography variant="Body/Paragraph/mdRegular">
<span>{poi.name}</span> <span>{poi.name}</span>
</Typography>
<Typography
variant="Body/Supporting text (caption)/smRegular"
className={styles.distance}
>
<span> <span>
{intl.formatMessage( {intl.formatMessage(
{ {
@@ -189,8 +161,8 @@ export default function Sidebar({
} }
)} )}
</span> </span>
</ButtonRAC> </Typography>
</Typography> </ButtonRAC>
</li> </li>
))} ))}
</ul> </ul>

View File

@@ -47,13 +47,17 @@
transition: background-color 0.3s; transition: background-color 0.3s;
} }
.poiButton.active { .poiButton.active {
background-color: var(--Base-Surface-Primary-light-Hover); background-color: var(--Surface-Primary-Hover-Light);
} }
.title { .title {
color: var(--Text-Heading); color: var(--Text-Heading);
} }
.distance {
color: var(--Text-Secondary);
}
@media screen and (max-width: 767px) { @media screen and (max-width: 767px) {
.sidebar { .sidebar {
--sidebar-mobile-toggle-height: 88px; --sidebar-mobile-toggle-height: 88px;

View File

@@ -0,0 +1,43 @@
import { logger } from "@scandic-hotels/common/logger"
import { PointOfInterestGroupEnum } from "@scandic-hotels/trpc/enums/pointOfInterest"
import type { IntlShape } from "react-intl"
export function translatePOIGroup(
group: PointOfInterestGroupEnum,
intl: IntlShape
) {
switch (group) {
case PointOfInterestGroupEnum.PUBLIC_TRANSPORT:
return intl.formatMessage({
defaultMessage: "Public transport",
})
case PointOfInterestGroupEnum.ATTRACTIONS:
return intl.formatMessage({
defaultMessage: "Attractions",
})
case PointOfInterestGroupEnum.BUSINESS:
return intl.formatMessage({
defaultMessage: "Business",
})
case PointOfInterestGroupEnum.LOCATION:
return intl.formatMessage({
defaultMessage: "Location",
})
case PointOfInterestGroupEnum.PARKING:
return intl.formatMessage({
defaultMessage: "Parking",
})
case PointOfInterestGroupEnum.SHOPPING_DINING:
return intl.formatMessage({
defaultMessage: "Shopping & Dining",
})
default:
const option: never = group
logger.warn(`Unsupported group given: ${option}`)
return intl.formatMessage({
defaultMessage: "N/A",
})
}
}

View File

@@ -41,12 +41,12 @@ export default function MapCard({ hotelName, pois }: MapCardProps) {
</span> </span>
<ul className={styles.poiList}> <ul className={styles.poiList}>
{pois.map((poi) => ( {pois.map((poi) => (
<li key={poi.name} className={styles.poiItem}> <li key={poi.name + poi.categoryName} className={styles.poiItem}>
<PoiMarker <PoiMarker
group={poi.group} group={poi.group}
categoryName={poi.categoryName} categoryName={poi.categoryName}
skipBackground skipBackground
size={20} size="medium"
/> />
<Typography> <Typography>
<span>{poi.name} </span> <span>{poi.name} </span>

View File

@@ -137,7 +137,7 @@ export function IconByIconName({
case IconName.Calendar: case IconName.Calendar:
return <MaterialIcon icon="calendar_today" {...props} /> return <MaterialIcon icon="calendar_today" {...props} />
case IconName.Camera: case IconName.Camera:
return <MaterialIcon icon="camera" {...props} /> return <MaterialIcon icon="photo_camera" {...props} />
case IconName.Cellphone: case IconName.Cellphone:
case IconName.Phone: case IconName.Phone:
return <MaterialIcon icon="phone" {...props} /> return <MaterialIcon icon="phone" {...props} />

View File

@@ -4,8 +4,7 @@ import {
} from "@vis.gl/react-google-maps" } from "@vis.gl/react-google-maps"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import Body from "@scandic-hotels/design-system/Body" import { Typography } from "@scandic-hotels/design-system/Typography"
import Caption from "@scandic-hotels/design-system/Caption"
import HotelMarkerByType from "../../Markers" import HotelMarkerByType from "../../Markers"
import PoiMarker from "../../Markers/Poi" import PoiMarker from "../../Markers/Poi"
@@ -46,7 +45,7 @@ export default function PoiMapMarkers({
{pointsOfInterest.map((poi) => ( {pointsOfInterest.map((poi) => (
<AdvancedMarker <AdvancedMarker
key={poi.name} key={poi.name + poi.categoryName}
className={styles.advancedMarker} className={styles.advancedMarker}
position={poi.coordinates} position={poi.coordinates}
anchorPoint={AdvancedMarkerAnchorPoint.CENTER} anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
@@ -61,25 +60,28 @@ export default function PoiMapMarkers({
<PoiMarker <PoiMarker
group={poi.group} group={poi.group}
categoryName={poi.categoryName} categoryName={poi.categoryName}
size={activePoi === poi.name ? 20 : 16} size={activePoi === poi.name ? "large" : "small"}
/> />
<Body className={styles.poiLabel} asChild> <span className={styles.poiLabel}>
<span> <Typography variant="Body/Paragraph/mdRegular">
{poi.name} <span>{poi.name}</span>
<Caption asChild> </Typography>
<span> <Typography
{intl.formatMessage( variant="Body/Supporting text (caption)/smRegular"
{ className={styles.distance}
defaultMessage: "{distanceInKm} km", >
}, <span>
{ {intl.formatMessage(
distanceInKm: poi.distance, {
} defaultMessage: "{distanceInKm} km",
)} },
</span> {
</Caption> distanceInKm: poi.distance,
</span> }
</Body> )}
</span>
</Typography>
</span>
</span> </span>
</AdvancedMarker> </AdvancedMarker>
))} ))}

View File

@@ -1,14 +1,14 @@
.advancedMarker { .advancedMarker {
height: var(--Spacing-x4); height: var(--Space-x4);
width: var( width: var(
--Spacing-x4 --Space-x4
) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ ) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */
} }
.advancedMarker.active { .advancedMarker.active {
height: var(--Spacing-x5); height: var(--Space-x5);
width: var( width: var(
--Spacing-x5 --Space-x5
) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */ ) !important; /* Overwriting the default width of the @vis.gl/react-google-maps AdvancedMarker */
} }
@@ -19,15 +19,15 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: var(--Spacing-x-half); padding: var(--Space-x05);
border-radius: var(--Corner-radius-rounded); border-radius: var(--Corner-radius-rounded);
background-color: var(--Base-Surface-Primary-light-Normal); background-color: var(--Surface-UI-Fill-Default);
box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1); box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1);
gap: var(--Spacing-x1); gap: var(--Space-x1);
} }
.poi.active { .poi.active {
padding-right: var(--Spacing-x-one-and-half); padding-right: var(--Space-x15);
} }
.poiLabel { .poiLabel {
@@ -37,6 +37,10 @@
.poi.active .poiLabel { .poi.active .poiLabel {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--Spacing-x2); gap: var(--Space-x2);
text-wrap: nowrap; text-wrap: nowrap;
} }
.distance {
color: var(--Text-Secondary);
}

View File

@@ -9,18 +9,18 @@ export default function PoiMarker({
group, group,
categoryName, categoryName,
skipBackground, skipBackground,
size = 16, size = "small",
className = "", className = "",
}: PoiMarkerProps) { }: PoiMarkerProps) {
const iconName = getIconByPoiGroupAndCategory(group, categoryName) const iconName = getIconByPoiGroupAndCategory(group, categoryName)
const classNames = poiVariants({ group, skipBackground, className }) const classNames = poiVariants({ group, skipBackground, size, className })
return iconName ? ( return iconName ? (
<span className={classNames}> <span className={classNames}>
<IconByIconName <IconByIconName
iconName={iconName} iconName={iconName}
color={skipBackground ? "Icon/Feedback/Neutral" : "Icon/Inverted"} color={skipBackground ? "Icon/Feedback/Neutral" : "Icon/Inverted"}
size={size} size={size === "small" ? 16 : size === "large" ? 24 : 20}
/> />
</span> </span>
) : null ) : null

View File

@@ -1,30 +1,37 @@
.icon { .icon {
width: 24px;
height: 24px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: var(--Corner-radius-rounded); border-radius: var(--Corner-radius-rounded);
background-color: var(--UI-Text-Placeholder); background-color: var(--Surface-UI-Fill-Default);
}
.small {
width: var(--Space-x3);
height: var(--Space-x3);
}
.large {
width: var(--Space-x4);
height: var(--Space-x4);
} }
.attractions { .attractions {
background-color: var(--Base-Interactive-Surface-Secondary-normal); background-color: var(--Surface-Accent-3);
} }
.business { .business {
background-color: var(--Scandic-Yellow-50); background-color: var(--Surface-Accent-4);
} }
.location { .location {
background-color: var(--UI-Text-Placeholder); background-color: var(--Surface-Feedback-Neutral-Accent);
} }
.parking { .parking {
background-color: var(--UI-Text-Active); background-color: var(--Surface-Accent-5);
} }
.publicTransport { .publicTransport {
background-color: var(--Base-Interactive-Surface-Tertiary-normal); background-color: var(--Surface-Accent-2);
} }
.shoppingDining { .shoppingDining {
background-color: var(--Base-Interactive-Surface-Primary-normal); background-color: var(--Surface-Accent-1);
} }
.icon.transparent { .icon.transparent {

View File

@@ -18,8 +18,14 @@ export const poiVariants = cva(styles.icon, {
true: styles.transparent, true: styles.transparent,
false: "", false: "",
}, },
size: {
small: styles.small,
medium: styles.small,
large: styles.large,
},
}, },
defaultVariants: { defaultVariants: {
skipBackground: false, skipBackground: false,
size: "small",
}, },
}) })

View File

@@ -16,7 +16,7 @@ export function getIconByPoiGroupAndCategory(
case PointOfInterestGroupEnum.PARKING: case PointOfInterestGroupEnum.PARKING:
return IconName.Parking return IconName.Parking
case PointOfInterestGroupEnum.SHOPPING_DINING: case PointOfInterestGroupEnum.SHOPPING_DINING:
return IconName.Shopping return category === "Restaurant" ? IconName.Restaurant : IconName.Shopping
case PointOfInterestGroupEnum.LOCATION: case PointOfInterestGroupEnum.LOCATION:
default: default:
return IconName.Location return IconName.Location

View File

@@ -6,6 +6,5 @@ import type { poiVariants } from "@/components/Maps/Markers/Poi/variants"
export interface PoiMarkerProps extends VariantProps<typeof poiVariants> { export interface PoiMarkerProps extends VariantProps<typeof poiVariants> {
group: PointOfInterestGroupEnum group: PointOfInterestGroupEnum
categoryName?: string categoryName?: string
size?: number
className?: string className?: string
} }

View File

@@ -269,7 +269,7 @@
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: block; font-display: block;
src: url(/_static/fonts/material-symbols/rounded-e6bd32d5.woff2) src: url(/_static/fonts/material-symbols/rounded-9290efcd.woff2)
format('woff2'); format('woff2');
} }

View File

@@ -54,6 +54,7 @@ const icons = [
'call', 'call',
'call_quality', 'call_quality',
'camera', 'camera',
'photo_camera',
'cancel', 'cancel',
'chair', 'chair',
'charging_station', 'charging_station',