fix(SW-1241): Adjusted amenities sidepeek on hotel pages and booking flow

Approved-by: Michael Zetterberg
Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-04-23 08:41:04 +00:00
parent c23a32cd10
commit 8152aea649
46 changed files with 654 additions and 731 deletions

View File

@@ -0,0 +1,49 @@
"use client"
import { useIntl } from "react-intl"
import AdditionalAmenities from "@/components/SidePeeks/AmenitiesSidepeekContent/AdditionalAmenities"
import Accordion from "@/components/TempDesignSystem/Accordion"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import AccessibilityAccordionItem from "../AmenitiesSidepeekContent/Accordions/Accessibility"
import BreakfastAccordionItem from "../AmenitiesSidepeekContent/Accordions/Breakfast"
import CheckInCheckOutAccordionItem from "../AmenitiesSidepeekContent/Accordions/CheckInCheckOut"
import ParkingAccordionItem from "../AmenitiesSidepeekContent/Accordions/Parking"
import type { AmenitiesSidePeekProps } from "@/types/components/hotelReservation/amenitiesSidePeek"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
export default function AmenitiesSidePeek({
hotel,
restaurants,
additionalHotelData,
activeSidePeek,
close,
}: AmenitiesSidePeekProps) {
const intl = useIntl()
return (
<SidePeek
title={intl.formatMessage({ defaultMessage: "Amenities" })}
isOpen={activeSidePeek === SidePeekEnum.amenities}
handleClose={close}
>
<Accordion>
<ParkingAccordionItem
parking={hotel.parking}
elevatorPitch={additionalHotelData?.hotelParking.elevatorPitch}
/>
<BreakfastAccordionItem
restaurants={restaurants}
hotelType={hotel.hotelType}
/>
<CheckInCheckOutAccordionItem checkInData={hotel.hotelFacts.checkin} />
<AccessibilityAccordionItem
elevatorPitch={additionalHotelData?.hotelSpecialNeeds.elevatorPitch}
/>
<AdditionalAmenities amenities={hotel.detailedFacilities} />
</Accordion>
</SidePeek>
)
}

View File

@@ -0,0 +1,54 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import ButtonLink from "@/components/ButtonLink"
import { IconName } from "@/components/Icons/iconName"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import styles from "./sidePeekAccordion.module.css"
import type { AccessibilityAccordionItemProps } from "@/types/components/sidePeeks/amenities"
export default function AccessibilityAccordionItem({
elevatorPitch,
accessibilityPageUrl,
}: AccessibilityAccordionItemProps) {
const intl = useIntl()
if (!elevatorPitch && !accessibilityPageUrl) {
return null
}
return (
<AccordionItem
title={intl.formatMessage({
defaultMessage: "Accessibility",
})}
iconName={IconName.Accessibility}
className={styles.accordionItem}
variant="sidepeek"
trackingId="amenities:accessibility"
>
<div className={styles.accessibilityContent}>
<Typography variant="Body/Paragraph/mdRegular">
<p>{elevatorPitch}</p>
</Typography>
{accessibilityPageUrl && (
<ButtonLink
href={`/${accessibilityPageUrl}`}
variant="Secondary"
color="Primary"
size="Medium"
typography="Body/Paragraph/mdBold"
appendToCurrentPath
>
{intl.formatMessage({ defaultMessage: "About accessibility" })}
</ButtonLink>
)}
</div>
</AccordionItem>
)
}

View File

@@ -0,0 +1,61 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { isDefined } from "@/server/utils"
import { IconName } from "@/components/Icons/iconName"
import OpeningHours from "@/components/OpeningHours"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import styles from "./sidePeekAccordion.module.css"
import type { BreakfastAccordionItemProps } from "@/types/components/sidePeeks/amenities"
import { HotelTypeEnum } from "@/types/enums/hotelType"
export default function BreakfastAccordionItem({
restaurants,
hotelType,
}: BreakfastAccordionItemProps) {
const intl = useIntl()
const openingHours = restaurants
?.map((restaurant) => {
const breakfastDetail = restaurant.openingDetails.find(
(details) =>
details.openingHours.name === "Breakfast" ||
details.openingHours.name ===
intl.formatMessage({ defaultMessage: "Breakfast" })
)
return breakfastDetail
})
.filter(isDefined)[0]
if (!openingHours && hotelType !== HotelTypeEnum.ScandicGo) {
return null
}
return (
<AccordionItem
title={intl.formatMessage({ defaultMessage: "Breakfast" })}
iconName={IconName.CoffeeAlt}
variant="sidepeek"
className={styles.accordionItem}
trackingId="amenities:breakfast"
>
{openingHours ? (
<OpeningHours
openingHours={openingHours.openingHours}
alternateOpeningHours={openingHours.alternateOpeningHours}
heading={intl.formatMessage({ defaultMessage: "Opening hours" })}
/>
) : (
<Typography variant="Body/Paragraph/mdRegular">
<p>{intl.formatMessage({ defaultMessage: "All-day breakfast" })}</p>
</Typography>
)}
</AccordionItem>
)
}

View File

@@ -0,0 +1,60 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { IconName } from "@/components/Icons/iconName"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Divider from "@/components/TempDesignSystem/Divider"
import styles from "./sidePeekAccordion.module.css"
import type { CheckInCheckOutAccordionItemProps } from "@/types/components/sidePeeks/amenities"
export default function CheckInCheckOutAccordionItem({
checkInData,
}: CheckInCheckOutAccordionItemProps) {
const intl = useIntl()
const { checkInTime, checkOutTime } = checkInData
return (
<AccordionItem
title={intl.formatMessage({ defaultMessage: "Check-in/Check-out" })}
iconName={IconName.Business}
variant="sidepeek"
className={styles.accordionItem}
trackingId="amenities:check-in"
>
<div className={styles.checkInCheckOutContent}>
<Typography variant="Title/Overline/sm">
<h4 className={styles.subheading}>
{intl.formatMessage({ defaultMessage: "Hours" })}
</h4>
</Typography>
<Divider color="Border/Divider/Default" />
<Typography variant="Body/Paragraph/mdRegular">
<div>
<p>
{intl.formatMessage(
{ defaultMessage: "Check in from: {checkInTime}" },
{
checkInTime,
}
)}
</p>
<p>
{intl.formatMessage(
{ defaultMessage: "Check out at latest: {checkOutTime}" },
{
checkOutTime,
}
)}
</p>
</div>
</Typography>
</div>
</AccordionItem>
)
}

View File

@@ -0,0 +1,59 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import ButtonLink from "@/components/ButtonLink"
import { IconName } from "@/components/Icons/iconName"
import ParkingInformation from "@/components/ParkingInformation"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import styles from "./sidePeekAccordion.module.css"
import type { ParkingAccordionItemProps } from "@/types/components/sidePeeks/amenities"
export default function ParkingAccordionItem({
parking,
elevatorPitch,
parkingPageUrl,
}: ParkingAccordionItemProps) {
const intl = useIntl()
return (
<AccordionItem
title={intl.formatMessage({
defaultMessage: "Parking",
})}
iconName={IconName.Parking}
variant="sidepeek"
className={styles.accordionItem}
trackingId="amenities:parking"
>
<div className={styles.parkingContent}>
{elevatorPitch ? (
<Typography variant="Body/Paragraph/mdRegular">
<p>{elevatorPitch}</p>
</Typography>
) : null}
{parking.map((data) => (
<ParkingInformation key={data.type} parking={data} />
))}
{parkingPageUrl && (
<ButtonLink
href={`/${parkingPageUrl}`}
variant="Secondary"
color="Primary"
size="Medium"
typography="Body/Paragraph/mdBold"
appendToCurrentPath
>
{intl.formatMessage({
defaultMessage: "About parking",
})}
</ButtonLink>
)}
</div>
</AccordionItem>
)
}

View File

@@ -0,0 +1,26 @@
.accordionItem {
color: var(--Text-Default);
}
.parkingContent,
.accessibilityContent {
display: grid;
gap: var(--Space-x3);
}
.checkInCheckOutContent {
display: grid;
gap: var(--Space-x15);
}
.checkInCheckOutContent {
display: grid;
padding: var(--Space-x2) var(--Space-x3);
gap: var(--Space-x1);
border-radius: var(--Corner-radius-Medium);
background: var(--Surface-Secondary-Default);
}
.subheading {
color: var(--Text-Secondary);
}

View File

@@ -0,0 +1,12 @@
.wrapper {
padding: var(--Spacing-x1) var(--Spacing-x0);
border-bottom: 1px solid var(--Base-Border-Subtle);
color: var(--Text-Interactive-Default);
}
.amenity {
display: flex;
gap: var(--Spacing-x1);
padding: var(--Spacing-x-one-and-half) var(--Spacing-x1);
align-items: center;
}

View File

@@ -0,0 +1,42 @@
import { Typography } from "@scandic-hotels/design-system/Typography"
import { FacilityToIcon } from "../../../ContentType/HotelPage/data"
import styles from "./additionalAmenities.module.css"
import type { AdditionalAmenitiesProps } from "@/types/components/sidePeeks/amenities"
import { FacilityEnum } from "@/types/enums/facilities"
export default function AdditionalAmenities({
amenities,
}: AdditionalAmenitiesProps) {
const amenitiesToIgnore = [
FacilityEnum.ParkingAdditionalCost,
FacilityEnum.ParkingElectricCharging,
FacilityEnum.ParkingFreeParking,
FacilityEnum.ParkingGarage,
FacilityEnum.ParkingOutdoor,
FacilityEnum.MeetingArea,
FacilityEnum.ServesBreakfastAlwaysIncluded,
FacilityEnum.LateCheckOutUntil1400Guaranteed,
]
const filteredAmenities = amenities.filter(
(amenity) => !amenitiesToIgnore.includes(amenity.id)
)
return filteredAmenities?.map((amenity) => (
<Typography key={amenity.name} variant="Title/Subtitle/md">
<li className={styles.wrapper}>
<div className={styles.amenity}>
<FacilityToIcon
id={amenity.id}
color="Icon/Interactive/Default"
size={24}
/>
{amenity.name}
</div>
</li>
</Typography>
))
}

View File

@@ -1,24 +0,0 @@
import { useIntl } from "react-intl"
import { IconName } from "@/components/Icons/iconName"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Body from "@/components/TempDesignSystem/Text/Body"
import type { AccessibilityProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
export default function Accessibility({
elevatorPitchText,
}: AccessibilityProps) {
const intl = useIntl()
return (
<AccordionItem
title={intl.formatMessage({
defaultMessage: "Accessibility",
})}
iconName={IconName.Accessibility}
variant="sidepeek"
>
<Body>{elevatorPitchText}</Body>
</AccordionItem>
)
}

View File

@@ -1,47 +0,0 @@
import { useIntl } from "react-intl"
import { IconName } from "@/components/Icons/iconName"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Body from "@/components/TempDesignSystem/Text/Body"
import type { CheckInCheckOutProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
export default function CheckinCheckOut({ checkin }: CheckInCheckOutProps) {
const intl = useIntl()
return (
<AccordionItem
title={intl.formatMessage({
defaultMessage: "Check-in/Check-out",
})}
iconName={IconName.Calendar}
variant="sidepeek"
>
<Body textTransform="bold">
{intl.formatMessage({
defaultMessage: "Hours",
})}
</Body>
<Body>
{intl.formatMessage(
{
defaultMessage: "Check in from: {checkInTime}",
},
{
checkInTime: checkin.checkInTime,
}
)}
</Body>
<Body>
{intl.formatMessage(
{
defaultMessage: "Check out at latest: {checkOutTime}",
},
{
checkOutTime: checkin.checkOutTime,
}
)}
</Body>
</AccordionItem>
)
}

View File

@@ -1,24 +0,0 @@
import { useIntl } from "react-intl"
import { IconName } from "@/components/Icons/iconName"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Body from "@/components/TempDesignSystem/Text/Body"
import type { MeetingsAndConferencesProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
export default function MeetingsAndConferences({
meetingDescription,
}: MeetingsAndConferencesProps) {
const intl = useIntl()
return (
<AccordionItem
title={intl.formatMessage({
defaultMessage: "Meetings & Conferences",
})}
iconName={IconName.Business}
variant="sidepeek"
>
<Body>{meetingDescription}</Body>
</AccordionItem>
)
}

View File

@@ -1,74 +0,0 @@
import { useIntl } from "react-intl"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { IconName } from "@/components/Icons/iconName"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import styles from "./sidePeekAccordion.module.css"
import type { ParkingProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
export default function Parking({ parking }: ParkingProps) {
const intl = useIntl()
return (
<AccordionItem
title={intl.formatMessage({
defaultMessage: "Parking",
})}
iconName={IconName.Parking}
className={styles.parking}
variant="sidepeek"
>
{parking.map((p) => {
const title = `${p.type}${p.name ? ` (${p.name})` : ""}`
return (
<div key={p.name}>
<Subtitle type="two">{title}</Subtitle>
<ul className={styles.list}>
{p.address !== undefined && (
<li>
<MaterialIcon icon="favorite" isFilled color="Icon/Accent" />
{intl.formatMessage(
{
defaultMessage: "Address: {address}",
},
{
address: p.address,
}
)}
</li>
)}
{p.numberOfParkingSpots !== undefined && (
<li>
<MaterialIcon icon="favorite" isFilled color="Icon/Accent" />
{intl.formatMessage(
{
defaultMessage: "Number of parking spots: {number}",
},
{ number: p.numberOfParkingSpots }
)}
</li>
)}
{p.numberOfChargingSpaces !== undefined && (
<li>
<MaterialIcon icon="favorite" isFilled color="Icon/Accent" />
{intl.formatMessage(
{
defaultMessage:
"Number of charging points for electric cars: {number}",
},
{ number: p.numberOfChargingSpaces }
)}
</li>
)}
</ul>
</div>
)
})}
</AccordionItem>
)
}

View File

@@ -1,29 +0,0 @@
import { useIntl } from "react-intl"
import { IconName } from "@/components/Icons/iconName"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Body from "@/components/TempDesignSystem/Text/Body"
import type { RestaurantProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
export default function Restaurant({
restaurantsContentDescriptionMedium,
}: RestaurantProps) {
const intl = useIntl()
return (
<AccordionItem
title={intl.formatMessage(
{
defaultMessage:
"{totalRestaurants, plural, one {Restaurant} other {Restaurants}}",
},
{ totalRestaurants: 1 }
)}
iconName={IconName.Restaurant}
variant="sidepeek"
>
<Body>{restaurantsContentDescriptionMedium}</Body>
</AccordionItem>
)
}

View File

@@ -1,24 +0,0 @@
.list {
font-family: var(--typography-Body-Regular-fontFamily);
list-style-position: inside;
list-style-type: none;
margin-top: var(--Spacing-x-one-and-half);
}
.list li {
display: flex;
align-items: center;
gap: var(--Spacing-x1);
padding-left: var(--Spacing-x1);
justify-items: flex-start;
}
.list li svg {
flex-shrink: 0;
}
.parking details > div {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}

View File

@@ -1,35 +1,5 @@
.spacing {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
.content {
display: grid;
gap: var(--Spacing-x2);
}
.content:last-child {
gap: 0;
}
.content > p {
margin-bottom: var(--Spacing-x-one-and-half);
}
.content > ul > li:first-child {
border-top: 1px solid var(--Base-Border-Subtle);
}
.amenity > p {
border-top: 1px solid var(--Base-Border-Subtle);
padding: calc(var(--Spacing-x-one-and-half) + var(--Spacing-x1))
var(--Spacing-x1);
display: flex;
align-items: center;
gap: var(--Spacing-x1);
}
.noIcon {
margin-left: var(--Spacing-x4);
color: var(--Text-Default);
}

View File

@@ -1,16 +1,18 @@
"use client"
import { useIntl } from "react-intl"
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
import { Typography } from "@scandic-hotels/design-system/Typography"
import Contact from "@/components/HotelReservation/Contact"
import AdditionalAmenities from "@/components/SidePeeks/AmenitiesSidepeekContent/AdditionalAmenities"
import Accordion from "@/components/TempDesignSystem/Accordion"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import Accessibility from "./Accordions/Accessibility"
import CheckinCheckOut from "./Accordions/CheckInCheckOut"
import MeetingsAndConferences from "./Accordions/MeetingsAndConferences"
import Parking from "./Accordions/Parking"
import Restaurant from "./Accordions/Restaurant"
import AccessibilityAccordionItem from "../AmenitiesSidepeekContent/Accordions/Accessibility"
import BreakfastAccordionItem from "../AmenitiesSidepeekContent/Accordions/Breakfast"
import CheckInCheckOutAccordionItem from "../AmenitiesSidepeekContent/Accordions/CheckInCheckOut"
import ParkingAccordionItem from "../AmenitiesSidepeekContent/Accordions/Parking"
import styles from "./hotelSidePeek.module.css"
@@ -19,17 +21,12 @@ import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
export default function HotelSidePeek({
hotel,
restaurants,
additionalHotelData,
activeSidePeek,
close,
}: HotelSidePeekProps) {
const intl = useIntl()
const amenitiesList = hotel.detailedFacilities.filter(
(facility) => facility.public
)
const parking = hotel.parking.filter(
(p) => p?.numberOfParkingSpots || p?.numberOfChargingSpaces || p?.address
)
return (
<SidePeek
@@ -38,61 +35,30 @@ export default function HotelSidePeek({
handleClose={close}
>
<div className={styles.content}>
<Subtitle color="baseTextHighContrast">
{intl.formatMessage({
defaultMessage: "Practical information",
})}
</Subtitle>
<Typography variant="Title/Subtitle/lg">
<h3>
{intl.formatMessage({ defaultMessage: "Practical information" })}
</h3>
</Typography>
<Contact hotel={hotel} />
<Accordion>
{parking?.length > 0 && <Parking parking={parking} />}
{additionalHotelData?.restaurantsOverviewPage
?.restaurantsContentDescriptionMedium && (
<Restaurant
restaurantsContentDescriptionMedium={
additionalHotelData.restaurantsOverviewPage
.restaurantsContentDescriptionMedium
}
/>
)}
{additionalHotelData?.hotelSpecialNeeds.elevatorPitch && (
<Accessibility
elevatorPitchText={
additionalHotelData.hotelSpecialNeeds.elevatorPitch
}
/>
)}
{hotel.hotelFacts?.checkin && (
<CheckinCheckOut checkin={hotel.hotelFacts.checkin} />
)}
{hotel.hotelContent.texts?.meetingDescription?.medium && (
<MeetingsAndConferences
meetingDescription={
hotel.hotelContent.texts.meetingDescription.medium
}
/>
)}
</Accordion>
<div className={styles.amenity}>
{amenitiesList.map((amenity) => {
const Icon = (
<FacilityToIcon id={amenity.id} size={24} color="Icon/Intense" />
)
return (
<Subtitle type="two" key={amenity.id} color="uiTextHighContrast">
{Icon && Icon}
{amenity.name}
</Subtitle>
)
})}
</div>
{/* TODO: handle linking to Hotel Page */}
{/* {showCTA && (
<Button theme="base" intent="secondary" size="large">
Read more about the hotel
</Button>
)} */}
<Accordion>
<ParkingAccordionItem
parking={hotel.parking}
elevatorPitch={additionalHotelData?.hotelParking.elevatorPitch}
/>
<BreakfastAccordionItem
restaurants={restaurants}
hotelType={hotel.hotelType}
/>
<CheckInCheckOutAccordionItem
checkInData={hotel.hotelFacts.checkin}
/>
<AccessibilityAccordionItem
elevatorPitch={additionalHotelData?.hotelSpecialNeeds.elevatorPitch}
/>
<AdditionalAmenities amenities={hotel.detailedFacilities} />
</Accordion>
</div>
</SidePeek>
)