Merged in feat/sw-3218-move-hotelreservationsidepeek-to-booking-flow (pull request #2600)

feat(SW-2873): Move HotelReservationSidePeek to booking-flow

* Move sidepeek store to booking-flow

* Begin move of HotelReservationSidePeek to booking-flow

* Copy Link

* Update AccessibilityAccordionItem

* Split AccessibilityAccordionItem into two components

* Fix tracking for Accordion

* Duplicate ButtonLink to booking-flow TEMP

* AdditionalAmeneties

* wip

* Move sidepeek accordion items

* Remove temp ButtonLink

* Merge branch 'master' into feat/sw-3218-move-hotelreservationsidepeek-to-booking-flow

* Fix accordion tracking

* Merge branch 'master' into feat/sw-3218-move-hotelreservationsidepeek-to-booking-flow

* Update exports

* Fix self-referencing import

* Merge branch 'master' into feat/sw-3218-move-hotelreservationsidepeek-to-booking-flow

* Add 'use client' to tracking function

* Merge branch 'master' into feat/sw-3218-move-hotelreservationsidepeek-to-booking-flow

* Fix TEMP folder

* Refactor sidepeek tracking

* Merge branch 'master' into feat/sw-3218-move-hotelreservationsidepeek-to-booking-flow


Approved-by: Joakim Jäderberg
This commit is contained in:
Anton Gunnarsson
2025-08-14 12:25:40 +00:00
parent f04e476a6e
commit 322268595d
53 changed files with 826 additions and 425 deletions

View File

@@ -0,0 +1,17 @@
.wrapper {
padding: var(--Space-x1) var(--Space-x0);
border-bottom: 1px solid var(--Base-Border-Subtle);
color: var(--Text-Interactive-Default);
}
.amenity {
display: flex;
gap: var(--Space-x1);
padding: var(--Space-x15) var(--Space-x1);
}
.amenityName {
display: flex;
align-items: center;
min-height: var(--Space-x3);
}

View File

@@ -0,0 +1,46 @@
import { Typography } from "@scandic-hotels/design-system/Typography"
import { FacilityEnum } from "@scandic-hotels/trpc/enums/facilities"
import { FacilityToIcon } from "../TEMP/FacilityToIcon"
import styles from "./additionalAmenities.module.css"
import type { DetailedFacility } from "@scandic-hotels/trpc/types/hotel"
interface AdditionalAmenitiesProps {
amenities: DetailedFacility[]
}
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}
/>
<span className={styles.amenityName}>{amenity.name}</span>
</div>
</li>
</Typography>
))
}

View File

@@ -0,0 +1,67 @@
.wrapper {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: auto;
gap: var(--Spacing-x2);
font-family: var(--typography-Body-Regular-fontFamily);
margin-bottom: var(--Spacing-x3);
}
.address,
.contactInfo {
display: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
grid-column: 1 / 3;
grid-row: 1 / 4;
}
.contactInfo > li {
font-style: normal;
list-style-type: none;
display: flex;
flex-direction: column;
min-width: 0;
}
.soMeIcons {
display: flex;
gap: var(--Spacing-x-one-and-half);
}
.ecoLabel {
width: 38px;
height: auto;
}
.ecoLabel img {
width: 100%;
height: auto;
flex-shrink: 0;
grid-column: 1 / 3;
grid-row: 4 / 4;
}
.ecoContainer {
display: flex;
align-items: center;
column-gap: var(--Spacing-x-one-and-half);
grid-column: 1 / 3;
grid-row: 4 / 4;
font-size: var(--typography-Footnote-Regular-fontSize);
line-height: ();
margin-bottom: var(--Spacing-x1);
}
.ecoLabelText {
display: flex;
color: var(--UI-Text-Medium-contrast);
flex-direction: column;
justify-content: center;
}
.link {
text-decoration: underline;
font-family: var(--typography-Body-Regular-fontFamily);
color: var(--Text-Interactive-Secondary);
}

View File

@@ -0,0 +1,137 @@
"use client"
import { useIntl } from "react-intl"
import Body from "@scandic-hotels/design-system/Body"
import FacebookIcon from "@scandic-hotels/design-system/Icons/FacebookIcon"
import InstagramIcon from "@scandic-hotels/design-system/Icons/InstagramIcon"
import Image from "@scandic-hotels/design-system/Image"
import Link from "@scandic-hotels/design-system/Link"
import useLang from "../../hooks/useLang"
import styles from "./contact.module.css"
import type { Hotel } from "@scandic-hotels/trpc/types/hotel"
interface ContactProps {
hotel: Hotel
}
export default function Contact({ hotel }: ContactProps) {
const lang = useLang()
const intl = useIntl()
const addressStr = `${hotel.address.streetAddress}, `
const cityStr = hotel.address.city
return (
<section className={styles.wrapper}>
<address className={styles.address}>
<ul className={styles.contactInfo}>
<li>
<Body textTransform="bold">
{intl.formatMessage({
defaultMessage: "Address",
})}
</Body>
<Body>
{addressStr}
<br />
{cityStr}
</Body>
</li>
<li>
<Body textTransform="bold">
{intl.formatMessage({
defaultMessage: "Driving directions",
})}
</Body>
<Link
href={`https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent(
`${hotel.name}, ${hotel.address.streetAddress}, ${hotel.address.zipCode} ${hotel.address.city}`
)}`}
>
<span className={styles.link}>
{intl.formatMessage({
defaultMessage: "Google Maps",
})}
</span>
</Link>
</li>
<li>
<Body textTransform="bold">
{intl.formatMessage({
defaultMessage: "Contact us",
})}
</Body>
<Link href={`tel:${hotel.contactInformation.phoneNumber}`}>
<span className={styles.link}>
{hotel.contactInformation.phoneNumber}
</span>
</Link>
</li>
<li>
{(hotel.socialMedia.facebook || hotel.socialMedia.instagram) && (
<>
<Body textTransform="bold">
{intl.formatMessage({
defaultMessage: "Follow us",
})}
</Body>
<div className={styles.soMeIcons}>
{hotel.socialMedia.instagram && (
<Link href={hotel.socialMedia.instagram} target="_blank">
<InstagramIcon color="Icon/Interactive/Default" />
</Link>
)}
{hotel.socialMedia.facebook && (
<Link href={hotel.socialMedia.facebook} target="_blank">
<FacebookIcon color="Icon/Interactive/Default" />
</Link>
)}
</div>
</>
)}
</li>
<li>
<Body textTransform="bold">
{intl.formatMessage({
defaultMessage: "Email",
})}
</Body>
<Link href={`mailto:${hotel.contactInformation.email}`}>
<span className={styles.link}>
{hotel.contactInformation.email}
</span>
</Link>
</li>
</ul>
</address>
{hotel.hotelFacts.ecoLabels?.nordicEcoLabel ? (
<div className={styles.ecoContainer}>
<div className={styles.ecoLabel}>
<Image
height={38}
width={38}
alt={intl.formatMessage({
defaultMessage: "Nordic Swan Ecolabel",
})}
src={`/_static/img/icons/swan-eco/swan_eco_dark_${lang}.png`}
/>
</div>
<div className={styles.ecoLabelText}>
<span>
{intl.formatMessage({
defaultMessage: "Nordic Swan Ecolabel",
})}
</span>
<span>
{hotel.hotelFacts.ecoLabels.svanenEcoLabelCertificateNumber}
</span>
</div>
</div>
) : null}
</section>
)
}

View File

@@ -0,0 +1,5 @@
.spacing {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}

View File

@@ -0,0 +1,84 @@
"use client"
import { useEffect } from "react"
import { trpc } from "@scandic-hotels/trpc/client"
import useLang from "../../hooks/useLang"
import useSidePeekStore from "../../stores/sidepeek"
import HotelSidePeek from "../HotelSidePeek"
import RoomSidePeek from "../RoomSidePeek"
export default function HotelReservationSidePeek() {
const { activeSidePeek, hotelId, roomTypeCode, showCTA } = useSidePeekStore(
(state) => ({
activeSidePeek: state.activeSidePeek,
hotelId: state.hotelId,
roomTypeCode: state.roomTypeCode,
showCTA: state.showCTA,
})
)
const closeFn = useSidePeekStore((state) => state.closeSidePeek)
const lang = useLang()
const { data: hotelData } = trpc.hotel.get.useQuery(
{
hotelId: hotelId ?? "",
language: lang,
isCardOnlyPayment: false,
},
{
enabled: !!hotelId,
}
)
const selectedRoom = hotelData?.roomCategories.find((room) =>
room.roomTypes.some((type) => type.code === roomTypeCode)
)
useEffect(() => {
if (activeSidePeek) {
window.history.pushState(null, "", window.location.href)
}
}, [activeSidePeek])
useEffect(() => {
function handlePopState() {
if (activeSidePeek) {
closeFn()
}
}
window.addEventListener("popstate", handlePopState)
return () => {
window.removeEventListener("popstate", handlePopState)
}
}, [activeSidePeek, closeFn])
if (activeSidePeek) {
return (
<>
{hotelData && (
<HotelSidePeek
additionalHotelData={hotelData.additionalData}
hotel={{ ...hotelData.hotel, url: hotelData.url }}
restaurants={hotelData.restaurants}
activeSidePeek={activeSidePeek}
close={closeFn}
showCTA={showCTA}
/>
)}
{selectedRoom && (
<RoomSidePeek
room={selectedRoom}
activeSidePeek={activeSidePeek}
close={closeFn}
/>
)}
</>
)
}
return null
}

View File

@@ -0,0 +1,5 @@
.content {
display: grid;
gap: var(--Spacing-x2);
color: var(--Text-Default);
}

View File

@@ -0,0 +1,128 @@
"use client"
import { useIntl } from "react-intl"
import Accordion from "@scandic-hotels/design-system/Accordion"
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { SidePeekEnum } from "../../stores/sidepeek"
import { useTrackingContext } from "../../trackingContext"
import AdditionalAmenities from "../AdditionalAmenities"
import Contact from "../Contact"
import BreakfastAccordionItem from "../SidePeekAccordions/BreakfastAccordionItem"
import CheckInCheckOutAccordionItem from "../SidePeekAccordions/CheckInCheckOutAccordionItem"
import ParkingAccordionItem from "../SidePeekAccordions/ParkingAccordionItem"
import styles from "./hotelSidePeek.module.css"
import type {
AdditionalData,
Hotel,
Restaurant,
} from "@scandic-hotels/trpc/types/hotel"
type HotelSidePeekProps = {
hotel: Hotel & { url: string | null }
restaurants: Restaurant[]
additionalHotelData: AdditionalData | undefined
activeSidePeek: SidePeekEnum
close: () => void
showCTA: boolean
}
export default function HotelSidePeek({
hotel,
restaurants,
additionalHotelData,
activeSidePeek,
close,
}: HotelSidePeekProps) {
const intl = useIntl()
return (
<SidePeek
title={hotel.name}
isOpen={activeSidePeek === SidePeekEnum.hotelDetails}
handleClose={close}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<div className={styles.content}>
<Typography variant="Title/Subtitle/lg">
<h3>
{intl.formatMessage({ defaultMessage: "Practical information" })}
</h3>
</Typography>
<Contact hotel={hotel} />
<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>
{hotel.url ? (
<ButtonLink
href={hotel.url}
variant="Secondary"
size="Medium"
typography="Body/Paragraph/mdBold"
>
{intl.formatMessage({
defaultMessage: "Read more about the hotel",
})}
</ButtonLink>
) : null}
</div>
</SidePeek>
)
}
type AccessibilityAccordionItemProps = {
elevatorPitch?: string
}
function AccessibilityAccordionItem({
elevatorPitch,
}: AccessibilityAccordionItemProps) {
const intl = useIntl()
const tracking = useTrackingContext()
if (!elevatorPitch) {
return null
}
return (
<AccordionItem
title={intl.formatMessage({
defaultMessage: "Accessibility",
})}
iconName={IconName.Accessibility}
className={styles.accordionItem}
variant="sidepeek"
onOpen={() => tracking.trackAccordionItemOpen("amenities:accessibility")}
>
<div className={styles.accessibilityContent}>
<Typography variant="Body/Paragraph/mdRegular">
<p>{elevatorPitch}</p>
</Typography>
</div>
</AccordionItem>
)
}

View File

@@ -0,0 +1,174 @@
import { useIntl } from "react-intl"
import { FacilityIcon } from "@scandic-hotels/design-system/Icons/FacilityIcon"
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { BED_TYPE_ICONS, type BedTypes } from "../../../misc/bedTypeIcons"
import styles from "./roomSidePeekContent.module.css"
import type { ApiImage, Room } from "@scandic-hotels/trpc/types/hotel"
interface RoomSidePeekContentProps {
room: Room
}
export function RoomSidePeekContent({ room }: RoomSidePeekContentProps) {
const intl = useIntl()
const roomSize = room.roomSize
const totalOccupancy = room.totalOccupancy
const roomDescription = room.descriptions.medium
const galleryImages = mapApiImagesToGalleryImages(room.images)
return (
<div className={styles.wrapper}>
<div className={styles.mainContent}>
{totalOccupancy && (
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
{intl.formatMessage(
{
defaultMessage:
"Max. {max, plural, one {{range} guest} other {{range} guests}}",
},
{
max: totalOccupancy.max,
range: totalOccupancy.range,
}
)}
</p>
</Typography>
)}
{roomSize && (
<Typography variant="Body/Supporting text (caption)/smRegular">
<p>
{roomSize.min === roomSize.max
? intl.formatMessage(
{
defaultMessage: "{roomSize} m²",
},
{
roomSize: roomSize.min,
}
)
: intl.formatMessage(
{
defaultMessage: "{roomSizeMin}{roomSizeMax} m²",
},
{
roomSizeMin: roomSize.min,
roomSizeMax: roomSize.max,
}
)}
</p>
</Typography>
)}
<div className={styles.imageContainer}>
<ImageGallery images={galleryImages} title={room.name} height={280} />
</div>
</div>
<div className={styles.listContainer}>
<Typography variant="Title/Subtitle/md">
<p>
{intl.formatMessage({
defaultMessage: "Room amenities",
})}
</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<ul className={styles.facilityList}>
{room.roomFacilities
.sort((a, b) => a.sortOrder - b.sortOrder)
.map((facility) => {
return (
<li key={facility.name}>
<FacilityIcon
name={facility.icon}
size={24}
color="Icon/Default"
/>
<span>
{facility.availableInAllRooms
? facility.name
: intl.formatMessage(
{
defaultMessage:
"{facility} (available in some rooms)",
},
{
facility: facility.name,
}
)}
</span>
</li>
)
})}
</ul>
</Typography>
</div>
<div className={styles.listContainer}>
<Typography variant="Title/Subtitle/md">
<p>
{intl.formatMessage({
defaultMessage: "Bed options",
})}
</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{intl.formatMessage({
defaultMessage: "Subject to availability",
})}
</p>
</Typography>
<ul className={styles.bedOptions}>
{room.roomTypes.map((roomType) => {
const description =
roomType.description || roomType.mainBed.description
const MainBedIcon =
BED_TYPE_ICONS[roomType.mainBed.type as BedTypes]
const ExtraBedIcon = roomType.fixedExtraBed
? BED_TYPE_ICONS[roomType.fixedExtraBed.type as BedTypes]
: null
return (
<li key={roomType.code}>
{MainBedIcon ? <MainBedIcon height={24} width={24} /> : null}
{ExtraBedIcon ? <ExtraBedIcon height={24} width={30} /> : null}
<Typography variant="Body/Paragraph/mdRegular">
<span>{description}</span>
</Typography>
</li>
)
})}
</ul>
</div>
<div className={styles.listContainer}>
<Typography variant="Title/Subtitle/md">
<p>
{intl.formatMessage({
defaultMessage: "About the hotel",
})}
</p>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p>{roomDescription}</p>
</Typography>
</div>
</div>
)
}
function mapApiImagesToGalleryImages(apiImages: ApiImage[]) {
return apiImages.map((apiImage) => ({
src: apiImage.imageSizes.medium,
alt:
apiImage.metaData.altText ||
apiImage.metaData.altText_En ||
apiImage.metaData.title ||
apiImage.metaData.title_En,
caption: apiImage.metaData.title || apiImage.metaData.title_En,
smallSrc: apiImage.imageSizes.small,
}))
}

View File

@@ -0,0 +1,58 @@
.wrapper {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
position: relative;
margin-bottom: calc(
var(--Spacing-x4) * 2 + 80px
); /* Creates space between the wrapper and buttonContainer */
}
.mainContent {
color: var(--Text-Secondary);
}
.mainContent,
.listContainer {
display: flex;
flex-direction: column;
gap: var(--Spacing-x-one-and-half);
}
.imageContainer {
position: relative;
border-radius: var(--Corner-radius-md);
overflow: hidden;
}
.imageContainer img {
width: 100%;
aspect-ratio: 16/9;
object-fit: cover;
}
.facilityList {
column-count: 2;
column-gap: var(--Spacing-x2);
color: var(--Text-Secondary);
}
.facilityList li {
display: flex !important; /* Overrides the display none from grids.stackable on Hotel Page */
gap: var(--Spacing-x1);
margin-bottom: var(--Spacing-x-half);
}
.bedOptions {
color: var(--Text-Secondary);
}
.bedOptions li {
display: flex;
gap: var(--Spacing-x1);
margin-bottom: var(--Spacing-x-half);
}
.facilityList li svg {
flex-shrink: 0;
}

View File

@@ -0,0 +1,35 @@
import { useIntl } from "react-intl"
import SidePeek from "@scandic-hotels/design-system/SidePeek"
import { SidePeekEnum } from "../../stores/sidepeek"
import { RoomSidePeekContent } from "./RoomSidePeekContent"
import type { Room } from "@scandic-hotels/trpc/types/hotel"
export type RoomSidePeekProps = {
room: Room
activeSidePeek: SidePeekEnum | null
close: () => void
}
export default function RoomSidePeek({
room,
activeSidePeek,
close,
}: RoomSidePeekProps) {
const intl = useIntl()
return (
<SidePeek
title={room.name}
isOpen={activeSidePeek === SidePeekEnum.roomDetails}
handleClose={close}
closeLabel={intl.formatMessage({
defaultMessage: "Close",
})}
>
<RoomSidePeekContent room={room} />
</SidePeek>
)
}

View File

@@ -0,0 +1,67 @@
"use client"
import { useIntl } from "react-intl"
import { isDefined } from "@scandic-hotels/common/utils/isDefined"
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
import OpeningHours from "@scandic-hotels/design-system/OpeningHours"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { HotelTypeEnum } from "@scandic-hotels/trpc/enums/hotelType"
import { useTrackingContext } from "../../trackingContext"
import styles from "./sidePeekAccordion.module.css"
import type { Restaurant } from "@scandic-hotels/trpc/types/hotel"
interface BreakfastAccordionItemProps {
restaurants?: Restaurant[]
hotelType: string
}
export default function BreakfastAccordionItem({
restaurants,
hotelType,
}: BreakfastAccordionItemProps) {
const intl = useIntl()
const tracking = useTrackingContext()
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}
onOpen={() => tracking.trackAccordionItemOpen("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,66 @@
"use client"
import { useIntl } from "react-intl"
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
import { Divider } from "@scandic-hotels/design-system/Divider"
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useTrackingContext } from "../../trackingContext"
import styles from "./sidePeekAccordion.module.css"
import type { CheckInData } from "@scandic-hotels/trpc/types/hotel"
interface CheckInCheckOutAccordionItemProps {
checkInData: CheckInData
}
export default function CheckInCheckOutAccordionItem({
checkInData,
}: CheckInCheckOutAccordionItemProps) {
const intl = useIntl()
const tracking = useTrackingContext()
const { checkInTime, checkOutTime } = checkInData
return (
<AccordionItem
title={intl.formatMessage({ defaultMessage: "Check-in/Check-out" })}
iconName={IconName.Business}
variant="sidepeek"
className={styles.accordionItem}
onOpen={() => tracking.trackAccordionItemOpen("amenities:check-in")}
>
<div className={styles.checkInCheckOutContent}>
<Typography variant="Title/Overline/sm">
<h4 className={styles.subheading}>
{intl.formatMessage({ defaultMessage: "Hours" })}
</h4>
</Typography>
<Divider />
<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,70 @@
"use client"
import { useIntl } from "react-intl"
import AccordionItem from "@scandic-hotels/design-system/Accordion/AccordionItem"
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
import ParkingInformation from "@scandic-hotels/design-system/ParkingInformation"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useTrackingContext } from "../../trackingContext"
import styles from "./sidePeekAccordion.module.css"
import type { Parking } from "@scandic-hotels/trpc/types/hotel"
interface ParkingAccordionItemProps {
parkingPageHref?: string | null
parking: Parking[]
elevatorPitch?: string
}
export default function ParkingAccordionItem({
parking,
elevatorPitch,
parkingPageHref,
}: ParkingAccordionItemProps) {
const intl = useIntl()
const tracking = useTrackingContext()
if (!parking.length && !elevatorPitch && !parkingPageHref) {
return null
}
return (
<AccordionItem
title={intl.formatMessage({
defaultMessage: "Parking",
})}
iconName={IconName.Parking}
variant="sidepeek"
className={styles.accordionItem}
onOpen={() => tracking.trackAccordionItemOpen("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} />
))}
{parkingPageHref ? (
<ButtonLink
href={parkingPageHref}
variant="Secondary"
color="Primary"
size="Medium"
typography="Body/Paragraph/mdBold"
>
{intl.formatMessage({
defaultMessage: "About parking",
})}
</ButtonLink>
) : null}
</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-md);
background: var(--Surface-Secondary-Default);
}
.subheading {
color: var(--Text-Secondary);
}

View File

@@ -0,0 +1,305 @@
import { IconByIconName } from "@scandic-hotels/design-system/Icons/IconByIconName"
import { IconName } from "@scandic-hotels/design-system/Icons/iconName"
import { FacilityEnum } from "@scandic-hotels/trpc/enums/facilities"
import type {
IconProps,
NucleoIconProps,
} from "@scandic-hotels/design-system/Icons"
import type { MaterialIconSetIconProps } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import type { JSX } from "react"
const facilityToIconMap: Record<FacilityEnum, IconName> = {
[FacilityEnum.AccessibleBathingControls]: IconName.StarFilled,
[FacilityEnum.AccessibleBathtubs]: IconName.StarFilled,
[FacilityEnum.AccessibleElevators]: IconName.StarFilled,
[FacilityEnum.AccessibleLightSwitch]: IconName.StarFilled,
[FacilityEnum.AccessibleRoomsAtHotel1]: IconName.StarFilled,
[FacilityEnum.AccessibleRoomsAtHotel2]: IconName.StarFilled,
[FacilityEnum.AccessibleToilets]: IconName.StarFilled,
[FacilityEnum.AccessibleWashBasins]: IconName.StarFilled,
[FacilityEnum.AdaptedRoomDoors]: IconName.StarFilled,
[FacilityEnum.AdjoiningConventionCentre]: IconName.ConventionCentre,
[FacilityEnum.AirConAirCooling]: IconName.AirConAirCooling,
[FacilityEnum.AirConditioningInRoom]: IconName.AirConditioningInRoom,
[FacilityEnum.AirportMaxDistance8Km]: IconName.Airplane,
[FacilityEnum.AlarmsContinuouslyMonitored]: IconName.StarFilled,
[FacilityEnum.AlarmsHaveStrobeLightsForDeafHardHearingInAllGuestRooms]:
IconName.StarFilled,
[FacilityEnum.AlarmsHaveStrobeLightsForDeafHardHearingInAllHallways]:
IconName.StarFilled,
[FacilityEnum.AlarmsHaveStrobeLightsForDeafHardHearingInAllPublicAreas]:
IconName.StarFilled,
[FacilityEnum.AllAudibleSmokeAlarmsHardwired]: IconName.StarFilled,
[FacilityEnum.AllExteriorDoorsRequireKeyAccessAtNightOrAutomaticallyLock]:
IconName.StarFilled,
[FacilityEnum.AllGuestRoomDoorsHaveViewports]: IconName.StarFilled,
[FacilityEnum.AllGuestRoomDoorsSelfClosing]: IconName.StarFilled,
[FacilityEnum.AllParkingAreasPatrolled]: IconName.StarFilled,
[FacilityEnum.AllParkingAreasWellLit]: IconName.StarFilled,
[FacilityEnum.AllStairsWellsVentilated]: IconName.StarFilled,
[FacilityEnum.ArmchairBed]: IconName.ArmChair,
[FacilityEnum.AudibleAlarms]: IconName.StarFilled,
[FacilityEnum.AudibleSmokeAlarmsInAllHalls]: IconName.StarFilled,
[FacilityEnum.AudibleSmokeAlarmsInAllPublicAreas]: IconName.StarFilled,
[FacilityEnum.AudibleSmokeAlarmsInAllRooms]: IconName.StarFilled,
[FacilityEnum.AudioVisualEquipmentAvailable]: IconName.StarFilled,
[FacilityEnum.AutolinkFireDepartment]: IconName.StarFilled,
[FacilityEnum.AutomatedExternalDefibrillatorOnSiteAED]: IconName.StarFilled,
[FacilityEnum.AutomaticFireDoors]: IconName.StarFilled,
[FacilityEnum.AutoRecallElevators]: IconName.StarFilled,
[FacilityEnum.BalconiesAccessibleToAdjoiningRooms]: IconName.StarFilled,
[FacilityEnum.Ballroom]: IconName.StarFilled,
[FacilityEnum.Banquet]: IconName.StarFilled,
[FacilityEnum.Bar]: IconName.LocalBar,
[FacilityEnum.BasicMedicalEquipmentOnSite]: IconName.StarFilled,
[FacilityEnum.BathroomsAdaptedForDisabledGuests]: IconName.StarFilled,
[FacilityEnum.Beach]: IconName.Beach,
[FacilityEnum.Beach0To1Km]: IconName.Beach,
[FacilityEnum.BeautySalon]: IconName.BeautySalon,
[FacilityEnum.BedroomsWithWheelchairAccess]: IconName.StarFilled,
[FacilityEnum.BikesForLoan]: IconName.Bike,
[FacilityEnum.Bowling]: IconName.Bowling,
[FacilityEnum.BrailleLargePrintHotelLiterature]: IconName.StarFilled,
[FacilityEnum.BrailleLargePrintMenus]: IconName.StarFilled,
[FacilityEnum.Breakfast]: IconName.Breakfast,
[FacilityEnum.Business1]: IconName.BusinessCentre,
[FacilityEnum.Business2]: IconName.BusinessCentre,
[FacilityEnum.BusinessCentre]: IconName.BusinessCentre,
[FacilityEnum.Cafe]: IconName.Restaurant,
[FacilityEnum.CashFree8pmTill6am]: IconName.CashFree,
[FacilityEnum.CashFreeHotel]: IconName.CashFree,
[FacilityEnum.ChildrenWelcome]: IconName.StarFilled,
[FacilityEnum.City]: IconName.City,
[FacilityEnum.CoffeeInReceptionAtCharge]: IconName.CoffeeInReceptionAtCharge,
[FacilityEnum.CoffeeShop]: IconName.CoffeeShop,
[FacilityEnum.CoffeeTeaFacilities]: IconName.CoffeeAlt,
[FacilityEnum.ColourTVInRoomsAllScandicHotels]: IconName.StarFilled,
[FacilityEnum.ComplimentaryColdRefreshments]:
IconName.ComplimentaryColdRefreshments,
[FacilityEnum.CongressHall]: IconName.StarFilled,
[FacilityEnum.ConventionCentre]: IconName.ConventionCentre,
[FacilityEnum.Couples]: IconName.StarFilled,
[FacilityEnum.DeadboltsOnConnectingDoors]: IconName.StarFilled,
[FacilityEnum.DeadboltsSecondaryLocksOnAllGuestRoomDoors]:
IconName.StarFilled,
[FacilityEnum.Defibrillator]: IconName.StarFilled,
[FacilityEnum.Desk]: IconName.Desk,
[FacilityEnum.DirectDialPhoneInRoomsAllScandic]: IconName.DirectDial,
[FacilityEnum.DisabledEmergencyPlan1]: IconName.StarFilled,
[FacilityEnum.DisabledEmergencyPlan2]: IconName.StarFilled,
[FacilityEnum.DisabledParking]: IconName.Wheelchair,
[FacilityEnum.DiscoNightClub]: IconName.Nightlife,
[FacilityEnum.DJLiveMusic]: IconName.Nightlife,
[FacilityEnum.DO_NOT_USE_Restaurant]: IconName.StarFilled,
[FacilityEnum.Downtown]: IconName.StarFilled,
[FacilityEnum.DrinkableTapWater]: IconName.StarFilled,
[FacilityEnum.DVDPlayer]: IconName.StarFilled,
[FacilityEnum.EBikesChargingStation]: IconName.ElectricBike,
[FacilityEnum.ElectronicKeyCards]: IconName.StarFilled,
[FacilityEnum.Elevator]: IconName.Elevator,
[FacilityEnum.EmergencyBackUpGenerators]: IconName.StarFilled,
[FacilityEnum.EmergencyCallButtonOnPhone]: IconName.StarFilled,
[FacilityEnum.EmergencyCodesOrButtonsInRooms]: IconName.StarFilled,
[FacilityEnum.EmergencyEvacuationPlan1]: IconName.StarFilled,
[FacilityEnum.EmergencyEvacuationPlan2]: IconName.StarFilled,
[FacilityEnum.EmergencyEvaluationDrillFrequency]: IconName.StarFilled,
[FacilityEnum.EmergencyInfoInAllRooms]: IconName.StarFilled,
[FacilityEnum.EmergencyLightingAllScandic]: IconName.StarFilled,
[FacilityEnum.EmergencyLightningInAllPublicAreas]: IconName.StarFilled,
[FacilityEnum.EmergencyServiceResponseTimeInMinutes]: IconName.StarFilled,
[FacilityEnum.Entertainment]: IconName.Theatre,
[FacilityEnum.EventVenue]: IconName.StarFilled,
[FacilityEnum.ExchangeFacility]: IconName.StarFilled,
[FacilityEnum.ExitMapsInRooms]: IconName.StarFilled,
[FacilityEnum.ExitSignsLit]: IconName.StarFilled,
[FacilityEnum.ExtraFamilyFriendly]: IconName.ExtraFamilyFriendly,
[FacilityEnum.Families]: IconName.ExtraFamilyFriendly,
[FacilityEnum.FaxFacilityInRoom]: IconName.Fax,
[FacilityEnum.Financial]: IconName.StarFilled,
[FacilityEnum.FireDetectorsAllScandic]: IconName.StarFilled,
[FacilityEnum.FireDetectorsInAllHalls]: IconName.StarFilled,
[FacilityEnum.FireDetectorsInAllPublicAreas]: IconName.StarFilled,
[FacilityEnum.FireDetectorsInAllRooms]: IconName.StarFilled,
[FacilityEnum.FireExtinguishersInAllPublicAreas]: IconName.StarFilled,
[FacilityEnum.FireExtinguishersInPublicAreasAllScandic]: IconName.StarFilled,
[FacilityEnum.FireSafetyAllScandic]: IconName.StarFilled,
[FacilityEnum.FirstAidAvailable]: IconName.StarFilled,
[FacilityEnum.FoodDrinks247]: IconName.FoodDrinks247,
[FacilityEnum.FreeWiFi]: IconName.Wifi,
[FacilityEnum.GiftShop]: IconName.Gift,
[FacilityEnum.Golf]: IconName.Golf,
[FacilityEnum.GolfCourse0To30Km]: IconName.Golf,
[FacilityEnum.GuestRoomDoorsHaveASecondLock]: IconName.StarFilled,
[FacilityEnum.Gym]: IconName.Fitness,
[FacilityEnum.GymTrainingFacilities]: IconName.Fitness,
[FacilityEnum.Hairdresser]: IconName.Hairdresser,
[FacilityEnum.HairdryerInRoomAllScandic]: IconName.HairdryerInRoomAllScandic,
[FacilityEnum.HandicapFacilities]: IconName.StarFilled,
[FacilityEnum.HandrailsInBathrooms]: IconName.StarFilled,
[FacilityEnum.HearingInductionLoops]: IconName.StarFilled,
[FacilityEnum.Highway1]: IconName.StarFilled,
[FacilityEnum.Highway2]: IconName.StarFilled,
[FacilityEnum.Hiking0To3Km]: IconName.Hiking,
[FacilityEnum.HotelCompliesWithAAASecurityStandards]: IconName.StarFilled,
[FacilityEnum.HotelIsFollowingScandicsSafetySecurityPolicy]:
IconName.StarFilled,
[FacilityEnum.HotelWorksAccordingToScandicsAccessibilityConcepts]:
IconName.StarFilled,
[FacilityEnum.IceMachine]: IconName.IceMachine,
[FacilityEnum.IceMachineReception]: IconName.IceMachine,
[FacilityEnum.IDRequiredToReplaceAGuestRoomKey]: IconName.StarFilled,
[FacilityEnum.IfNoWhatAreTheHoursUse24ClockEx0000To0600]: IconName.StarFilled,
[FacilityEnum.InCountry]: IconName.StarFilled,
[FacilityEnum.IndustrialPark]: IconName.StarFilled,
[FacilityEnum.InternetHighSpeedInternetConnectionAllScandic]:
IconName.StarFilled,
[FacilityEnum.InternetHotSpotsAllScandic]: IconName.StarFilled,
[FacilityEnum.IroningRoom]: IconName.Ironing,
[FacilityEnum.IronIroningBoardAllScandic]: IconName.Ironing,
[FacilityEnum.Jacuzzi]: IconName.Jacuzzi,
[FacilityEnum.JacuzziInRoom]: IconName.Jacuzzi,
[FacilityEnum.KayaksForLoan]: IconName.Kayaking,
[FacilityEnum.KeyAccessOnlySecuredFloorsAvailable]: IconName.StarFilled,
[FacilityEnum.KeyAccessOnlyToHealthClubGym]: IconName.Fitness,
[FacilityEnum.KidsPlayRoom]: IconName.StarFilled,
[FacilityEnum.KidsUpToAndIncluding12YearsStayForFree]: IconName.StarFilled,
[FacilityEnum.KitchenInRoom]: IconName.Kitchen,
[FacilityEnum.Lake0To1Km]: IconName.Houseboat,
[FacilityEnum.LakeOrSea0To1Km]: IconName.Houseboat,
[FacilityEnum.LaptopSafe]: IconName.LaptopSafe,
[FacilityEnum.LateCheckOutUntil1400Guaranteed]: IconName.Business,
[FacilityEnum.LaundryRoom]: IconName.LaundryMachine,
[FacilityEnum.LaundryService]: IconName.LaundryMachine,
[FacilityEnum.LaundryServiceExpress]: IconName.TshirtWash,
[FacilityEnum.Leisure]: IconName.StarFilled,
[FacilityEnum.LifestyleConcierge]: IconName.Concierge,
[FacilityEnum.LuggageLockers]: IconName.LuggageLockers,
[FacilityEnum.LuggageStorageAdditionalCost]: IconName.Luggage,
[FacilityEnum.LuggageStorageNoCost]: IconName.Luggage,
[FacilityEnum.Massage]: IconName.Massage,
[FacilityEnum.MeetingArea]: IconName.Business,
[FacilityEnum.MeetingConferenceFacilities]: IconName.Business,
[FacilityEnum.MeetingRooms]: IconName.Business,
[FacilityEnum.MinibarInRoom]: IconName.Minibar,
[FacilityEnum.MobileLift]: IconName.StarFilled,
[FacilityEnum.Mountains0To1Km]: IconName.Landscape,
[FacilityEnum.MovieChannelsInRoomAllScandic]: IconName.TVRemote,
[FacilityEnum.MultipleExitsOnEachFloor]: IconName.StarFilled,
[FacilityEnum.NonSmokingRoomsAllScandic]: IconName.NonSmoking,
[FacilityEnum.OnSiteTrainingFacilities]: IconName.Fitness,
[FacilityEnum.OtherExplainInBriefDescription]: IconName.StarFilled,
[FacilityEnum.OutdoorTerrace]: IconName.Deck,
[FacilityEnum.OvernightSecurity]: IconName.Guard,
[FacilityEnum.ParkingAdditionalCost]: IconName.Parking,
[FacilityEnum.ParkingAttendant]: IconName.StarFilled,
[FacilityEnum.ParkingElectricCharging]: IconName.ElectricCar,
[FacilityEnum.ParkingFreeParking]: IconName.Parking,
[FacilityEnum.ParkingGarage]: IconName.Garage,
[FacilityEnum.ParkingOutdoor]: IconName.ParkingOutdoor,
[FacilityEnum.PCHookUpInRoom]: IconName.StarFilled,
[FacilityEnum.PetFriendlyRooms]: IconName.Pets,
[FacilityEnum.PillowAlarmsAvailable]: IconName.StarFilled,
[FacilityEnum.PlayStationInPlayArea]: IconName.Gaming,
[FacilityEnum.Pool]: IconName.Swim,
[FacilityEnum.PoolSwimmingPoolJacuzziAtHotel]: IconName.Swim,
[FacilityEnum.PrintingService]: IconName.StarFilled,
[FacilityEnum.PropertyMeetsRequirementsFireSafety]: IconName.StarFilled,
[FacilityEnum.PublicAddressSystem]: IconName.StarFilled,
[FacilityEnum.RelaxationSuite]: IconName.StarFilled,
[FacilityEnum.Restaurant]: IconName.Restaurant,
[FacilityEnum.RestrictedRoomAccessAllScandic]: IconName.StarFilled,
[FacilityEnum.RooftopBar]: IconName.Deck,
[FacilityEnum.RoomsAccessibleFromTheInterior]: IconName.StarFilled,
[FacilityEnum.RoomService]: IconName.RoomService,
[FacilityEnum.RoomWindowsOpen]: IconName.StarFilled,
[FacilityEnum.RoomWindowsThatOpenHaveLockingDevice]: IconName.StarFilled,
[FacilityEnum.Rural1]: IconName.StarFilled,
[FacilityEnum.Rural2]: IconName.StarFilled,
[FacilityEnum.SafeDepositBoxInRoomsAllScandic]: IconName.SafetyBox,
[FacilityEnum.SafeDepositBoxInRoomsCanHoldA17InchLaptop]: IconName.SafetyBox,
[FacilityEnum.SafeDepositBoxInRoomsCannotHoldALaptop]: IconName.SafetyBox,
[FacilityEnum.SafetyChainsOnGuestRoomDoor]: IconName.StarFilled,
[FacilityEnum.Sauna]: IconName.Sauna,
[FacilityEnum.ScandicShop24Hrs]: IconName.ConvenienceStore24h,
[FacilityEnum.SecondaryLocksOnSlidingGlassDoors]: IconName.StarFilled,
[FacilityEnum.SecondaryLocksOnWindows]: IconName.StarFilled,
[FacilityEnum.Security24Hours]: IconName.Guard,
[FacilityEnum.SecurityEscortsAvailableOnRequest]: IconName.Guard,
[FacilityEnum.SecurityPersonnelOnSite]: IconName.Guard,
[FacilityEnum.SeparateFloorsForWomen]: IconName.StarFilled,
[FacilityEnum.ServesBreakfastAlwaysIncluded]: IconName.Breakfast,
[FacilityEnum.ServesBreakfastNotAlwaysIncluded]: IconName.Breakfast,
[FacilityEnum.ServesOrganicBreakfastAlwaysIncluded]: IconName.Breakfast,
[FacilityEnum.ServesOrganicBreakfastNotAlwaysIncluded]: IconName.Breakfast,
[FacilityEnum.ServiceGuideDogsAllowed]: IconName.StarFilled,
[FacilityEnum.ServiceSecurity24Hrs]: IconName.Guard,
[FacilityEnum.Shopping]: IconName.Shopping,
[FacilityEnum.SkateboardsForLoan]: IconName.Skateboarding,
[FacilityEnum.Skiing0To1Km]: IconName.Skiing,
[FacilityEnum.Skybar]: IconName.LocalBar,
[FacilityEnum.SmokeDetectorsAllScandic]: IconName.StarFilled,
[FacilityEnum.Solarium]: IconName.StarFilled,
[FacilityEnum.SpecialNeedsMenus]: IconName.StarFilled,
[FacilityEnum.Sports]: IconName.Sports,
[FacilityEnum.SprinklersAllScandic]: IconName.StarFilled,
[FacilityEnum.SprinklersInAllHalls]: IconName.StarFilled,
[FacilityEnum.SprinklersInAllPublicAreas]: IconName.StarFilled,
[FacilityEnum.SprinklersInAllRooms]: IconName.StarFilled,
[FacilityEnum.StaffInDuplicateKeys]: IconName.StarFilled,
[FacilityEnum.StaffRedCrossCertifiedInCPR]: IconName.StarFilled,
[FacilityEnum.StaffTrainedForDisabledGuests]: IconName.StarFilled,
[FacilityEnum.StaffTrainedInAutomatedExternalDefibrillatorUsageAED]:
IconName.StarFilled,
[FacilityEnum.StaffTrainedInCPR]: IconName.StarFilled,
[FacilityEnum.StaffTrainedInFirstAid]: IconName.StarFilled,
[FacilityEnum.StaffTrainedInFirstAidTechniques]: IconName.StarFilled,
[FacilityEnum.StaffTrainedToCaterForDisabledGuestsAllScandic]:
IconName.StarFilled,
[FacilityEnum.Suburbs]: IconName.StarFilled,
[FacilityEnum.SwingboltLock]: IconName.StarFilled,
[FacilityEnum.TeleConferencingFacilitiesAvailable]: IconName.StarFilled,
[FacilityEnum.TelevisionsWithSubtitlesOrClosedCaptions]: IconName.StarFilled,
[FacilityEnum.Tennis1]: IconName.Sports,
[FacilityEnum.Tennis2]: IconName.Sports,
[FacilityEnum.TennisPadel]: IconName.Sports,
[FacilityEnum.Theatre]: IconName.Theatre,
[FacilityEnum.TrouserPress]: IconName.Ironing,
[FacilityEnum.TVWithChromecast1]: IconName.TvCasting,
[FacilityEnum.TVWithChromecast2]: IconName.TvCasting,
[FacilityEnum.UniformSecurityOnPremises]: IconName.StarFilled,
[FacilityEnum.UtilityRoomForIroning]: IconName.Ironing,
[FacilityEnum.VendingMachineWithNecessities]: IconName.Dining,
[FacilityEnum.VideoSurveillanceInHallways]: IconName.StarFilled,
[FacilityEnum.VideoSurveillanceInPublicAreas]: IconName.StarFilled,
[FacilityEnum.VideoSurveillanceMonitored24HrsADay]: IconName.StarFilled,
[FacilityEnum.VideoSurveillanceOfAllParkingAreas]: IconName.StarFilled,
[FacilityEnum.VideoSurveillanceOfExteriorFrontEntrance]: IconName.StarFilled,
[FacilityEnum.VideoSurveillanceRecorded24HrsADayParkingArea]:
IconName.StarFilled,
[FacilityEnum.WallMountedCycleRack]: IconName.Bike,
[FacilityEnum.WellLitWalkways]: IconName.StarFilled,
[FacilityEnum.WellnessAndSaunaEntranceFeeAdmission16PlusYears]: IconName.Spa,
[FacilityEnum.WellnessPoolSaunaEntranceFeeAdmission16PlusYears]: IconName.Spa,
[FacilityEnum.WheelchairAccess]: IconName.Wheelchair,
[FacilityEnum.WideCorridors]: IconName.StarFilled,
[FacilityEnum.WideEntrance]: IconName.StarFilled,
[FacilityEnum.WideRestaurantEntrance]: IconName.StarFilled,
[FacilityEnum.WiFiWirelessInternetAccessAllScandic]: IconName.StarFilled,
}
interface mapFacilityToIconProps {
id: FacilityEnum
}
export function FacilityToIcon({
id,
...props
}: mapFacilityToIconProps &
(
| MaterialIconSetIconProps
| NucleoIconProps
| IconProps
)): JSX.Element | null {
const iconName = facilityToIconMap[id]
return <IconByIconName iconName={iconName} {...props} />
}

View File

@@ -0,0 +1,35 @@
import BedBunkExtraIcon from "@scandic-hotels/design-system/Icons/BedBunkExtraIcon"
import BedGenericIcon from "@scandic-hotels/design-system/Icons/BedGenericIcon"
import BedKingIcon from "@scandic-hotels/design-system/Icons/BedKingIcon"
import BedPullOutExtraIcon from "@scandic-hotels/design-system/Icons/BedPullOutExtraIcon"
import BedQueenIcon from "@scandic-hotels/design-system/Icons/BedQueenIcon"
import BedSingleIcon from "@scandic-hotels/design-system/Icons/BedSingleIcon"
import BedSofaExtraIcon from "@scandic-hotels/design-system/Icons/BedSofaExtraIcon"
import BedTwinIcon from "@scandic-hotels/design-system/Icons/BedTwinIcon"
import BedWallExtraIcon from "@scandic-hotels/design-system/Icons/BedWallExtraIcon"
import type { IconProps } from "@scandic-hotels/design-system/Icons"
import type {
BedTypeEnum,
ExtraBedTypeEnum,
} from "@scandic-hotels/trpc/enums/bedType"
import type { JSX } from "react"
export type BedTypes = keyof typeof BedTypeEnum | keyof typeof ExtraBedTypeEnum
export const BED_TYPE_ICONS: Record<
BedTypes,
(props: IconProps) => JSX.Element
> = {
King: BedKingIcon,
Queen: BedQueenIcon,
Single: BedSingleIcon,
Twin: BedTwinIcon,
CustomOccupancy: BedGenericIcon,
SofaBed: BedSofaExtraIcon,
WallBed: BedWallExtraIcon,
BunkBed: BedBunkExtraIcon,
PullOutBed: BedPullOutExtraIcon,
Other: BedGenericIcon,
}

View File

@@ -0,0 +1,62 @@
import { create } from "zustand"
export enum SidePeekEnum {
hotelDetails = "hotel-detail-side-peek",
roomDetails = "room-detail-side-peek",
bookedRoomDetails = "booked-room-detail-side-peek",
}
interface SidePeekState {
activeSidePeek: SidePeekEnum | null
hotelId: string | null
roomTypeCode: string | null
showCTA: boolean
confirmationNumber: string
openSidePeek: ({
key,
hotelId,
roomTypeCode,
showCTA,
confirmationNumber,
}: {
key: SidePeekEnum | null
hotelId: string
roomTypeCode?: string
showCTA?: boolean
confirmationNumber?: string
}) => void
closeSidePeek: () => void
}
const useSidePeekStore = create<SidePeekState>((set) => ({
activeSidePeek: null,
hotelId: null,
roomTypeCode: null,
showCTA: true,
user: null,
confirmationNumber: "",
openSidePeek: ({
key,
hotelId,
roomTypeCode,
showCTA,
confirmationNumber,
}) => {
set({
activeSidePeek: key,
hotelId,
roomTypeCode,
showCTA,
confirmationNumber,
})
},
closeSidePeek: () =>
set({
activeSidePeek: null,
hotelId: null,
roomTypeCode: null,
confirmationNumber: "",
}),
}))
export default useSidePeekStore

View File

@@ -7,6 +7,7 @@ export type TrackingFunctions = {
searchTerm: string,
searchType: "hotel" | "destination"
) => void
trackAccordionItemOpen: (option: string) => void
}
export const TrackingContext = createContext<TrackingFunctions | undefined>(

View File

@@ -20,8 +20,16 @@
"./utils/url": "./lib/utils/url.ts",
"./hooks/useSearchHistory": "./lib/hooks/useSearchHistory.ts",
"./searchType": "./lib/misc/searchType.ts",
"./bedTypeIcons": "./lib/misc/bedTypeIcons.ts",
"./stores/bookingCode-filter": "./lib/stores/bookingCode-filter.ts",
"./components/TripAdvisorChip": "./lib/components/TripAdvisorChip/index.tsx"
"./stores/sidepeek": "./lib/stores/sidepeek.ts",
"./components/TripAdvisorChip": "./lib/components/TripAdvisorChip/index.tsx",
"./components/AdditionalAmenities": "./lib/components/AdditionalAmenities/index.tsx",
"./components/HotelReservationSidePeek": "./lib/components/HotelReservationSidePeek/index.tsx",
"./components/RoomSidePeekContent": "./lib/components/RoomSidePeek/RoomSidePeekContent/index.tsx",
"./components/SidePeekAccordions/BreakfastAccordionItem": "./lib/components/SidePeekAccordions/BreakfastAccordionItem.tsx",
"./components/SidePeekAccordions/CheckInCheckOutAccordionItem": "./lib/components/SidePeekAccordions/CheckInCheckOutAccordionItem.tsx",
"./components/SidePeekAccordions/ParkingAccordionItem": "./lib/components/SidePeekAccordions/ParkingAccordionItem.tsx"
},
"dependencies": {
"@hookform/resolvers": "^5.0.1",

View File

@@ -5,10 +5,7 @@
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["**/node_modules/**"]

View File

@@ -1,4 +1,4 @@
import { setProjectAnnotations } from '@storybook/react-vite'
import * as previewAnnotations from './preview'
const annotations = setProjectAnnotations([previewAnnotations])
setProjectAnnotations([previewAnnotations])

View File

@@ -0,0 +1,234 @@
import BathroomCabinetIcon from './Nucleo/Amenities_Facilities/bathroom-cabinet-2'
import BedHotelIcon from './Customised/Amenities_Facilities/BedHotel'
import BowlingPinsIcon from './Nucleo/Experiences/bowling-pins'
import BunkBedIcon from './Customised/Amenities_Facilities/MdiBunkBedOutline'
import ConferenceRoomIcon from './Nucleo/Amenities_Facilities/conference-room'
import CoolingIcon from './Customised/Amenities_Facilities/Cooling'
import CouchTableIcon from './Customised/Amenities_Facilities/CouchTable'
import DoorIcon from './Nucleo/Amenities_Facilities/door-2'
import FootStoolIcon from './Customised/Amenities_Facilities/FootStool'
import HairdresserIcon from './Nucleo/Amenities_Facilities/hairdresser-1'
import HairdryerIcon from './Customised/Amenities_Facilities/Hairdryer'
import HandSoapIcon from './Customised/Amenities_Facilities/HandSoap'
import IceMachineIcon from './Customised/Amenities_Facilities/IceMachine'
import MassageIcon from './Customised/Amenities_Facilities/Massage'
import { MaterialIcon, type MaterialIconSetIconProps } from './MaterialIcon'
import MirrorIcon from './Customised/Amenities_Facilities/Mirror'
import MovingBedsIcon from './Customised/Amenities_Facilities/MovingBeds'
import RecordPlayerIcon from './Nucleo/Amenities_Facilities/record-player-3'
import RoadIcon from './Customised/Amenities_Facilities/Road'
import RugIcon from './Customised/Amenities_Facilities/Rug'
import SafetyBoxIcon from './Customised/Amenities_Facilities/SafetyBox'
import SlippersIcon from './Customised/Amenities_Facilities/Slippers'
import ToiletIcon from './Nucleo/Amenities_Facilities/toilet-2'
import TowelIcon from './Customised/Amenities_Facilities/Towel'
import UserPoliceIcon from './Nucleo/Amenities_Facilities/user-police-2'
import ViewIcon from './Customised/Amenities_Facilities/View'
import WardIcon from './Customised/Amenities_Facilities/Ward'
import WindowNotAvailableIcon from './Customised/Amenities_Facilities/WindowNotAvailable'
import WoodFloorIcon from './Customised/Amenities_Facilities/WoodFloor'
import type { IconProps, NucleoIconProps } from '.'
import type { MaterialSymbolProps } from './MaterialIcon/MaterialSymbol'
import type { JSX } from 'react'
interface FacilityIconProps {
name: string | undefined
}
export function FacilityIcon({
name,
...props
}:
| (FacilityIconProps & MaterialIconSetIconProps)
| NucleoIconProps
| IconProps): JSX.Element {
if (!name) {
return <MaterialIcon icon="favorite" {...props} />
}
const normalizedName = name.toLowerCase()
const customIcon = CustomIconMappings.find(
(icon) => icon.name.toLowerCase() === normalizedName
)
if (customIcon) {
return <customIcon.icon {...props} />
}
const materialSymbolIcon = MaterialIconMappings.find(
(icon) => icon.name.toLowerCase() === normalizedName
)
return materialSymbolIcon ? (
<MaterialIcon icon={materialSymbolIcon.icon} {...props} />
) : (
<MaterialIcon icon="favorite" {...props} />
)
}
const MaterialIconMappings: {
icon: MaterialSymbolProps['icon']
name: string
}[] = [
{ icon: 'air_purifier_gen', name: 'aircondition' },
{ icon: 'travel', name: 'airport' },
{ icon: 'balcony', name: 'balcony' },
{ icon: 'deck', name: 'balconyorterrace' },
{ icon: 'balcony', name: 'frenchbalcony' },
{ icon: 'bathtub', name: 'bathroomwithbathtub' },
{ icon: 'bathtub', name: 'bathroomwithshowerandbathtub' },
{ icon: 'bed', name: 'setoftwopillows' },
{ icon: 'chair', name: 'armchairbed' },
{ icon: 'meeting_room', name: 'separatebedroom' },
{ icon: 'pedal_bike', name: 'bikeforloan' },
{ icon: 'bakery_dining', name: 'breakfast' },
{ icon: 'bakery_dining', name: 'servesbreakfastalwaysincluded' },
{ icon: 'business_center', name: 'meetingconferencefacilities' },
{ icon: 'business_center', name: 'meetingrooms' },
{ icon: 'router', name: 'internetwithcableintheroom' },
{ icon: 'chair', name: 'armchair' },
{ icon: 'chair', name: 'chair' },
{ icon: 'meeting_room', name: 'connectingrooms' },
{ icon: 'location_city', name: 'viewcityview' },
{ icon: 'coffee_maker', name: 'nespressomachine' },
{ icon: 'dining', name: 'café' },
{ icon: 'coffee', name: 'coffee' },
{ icon: 'coffee_maker', name: 'coffeemachine' },
{ icon: 'concierge', name: 'lifestyleconcierge' },
{ icon: 'mode_fan', name: 'aircooling' },
{ icon: 'mode_fan', name: 'cooler' },
{ icon: 'desk', name: 'deskandchair' },
{ icon: 'desk', name: 'desk' },
{ icon: 'dining', name: 'diningarea' },
{ icon: 'table_bar', name: 'tablefordining' },
{ icon: 'electric_bike', name: 'ebikeschargingstation' },
{ icon: 'family_restroom', name: 'extrafamilyfriendly' },
{ icon: 'exercise', name: 'gym' },
{ icon: 'health_and_beauty', name: 'beautysalon' },
{ icon: 'bathroom', name: 'bathroom2separatebathrooms' },
{ icon: 'credit_card_heart', name: 'cashfree800pmtill0600am' },
{ icon: 'credit_card_heart', name: 'cashfreehotel' },
{ icon: 'confirmation_number', name: 'coffeevoucher' },
{ icon: 'water_full', name: 'complimentarycoldrefreshments' },
{ icon: 'groups', name: 'conventioncentre' },
{ icon: 'accessible', name: 'disabledparking' },
{ icon: 'charging_station', name: 'dockingstationforipodipad' },
{ icon: 'cool_to_dry', name: 'dryingcabinet' },
{ icon: 'assistant_navigation', name: 'easyaccess' },
{ icon: 'laundry', name: 'garmentsteamer' },
{ icon: 'stairs', name: 'highfloor' },
{ icon: 'hot_tub', name: 'jaccuzzi' },
{ icon: 'hot_tub', name: 'jacuzzi' },
{ icon: 'countertops', name: 'kitchen' },
{ icon: 'countertops', name: 'kitchenette' },
{ icon: 'checked_bag', name: 'latecheckoutuntil1400guaranteed' },
{ icon: 'music_cast', name: 'livemusicexhibitions' },
{ icon: 'liquor', name: 'minibarincludedinroomrate' },
{ icon: 'local_parking', name: 'parkingadditionalcost' },
{ icon: 'sauna', name: 'privatesauna' },
{ icon: 'kitchen', name: 'refrigerator' },
{ icon: 'airline_seat_recline_normal', name: 'seatingarea' },
{ icon: 'scene', name: 'separatelivingroom' },
{ icon: 'chair', name: 'sofabed' },
{ icon: 'chair', name: 'sofas' },
{ icon: 'chair', name: 'sofa' },
{ icon: 'bedroom_parent', name: 'spaciousroom' },
{ icon: 'local_drink', name: 'stillandsparklingwater' },
{ icon: 'table_bar', name: 'table' },
{ icon: 'coffee_maker', name: 'tassimocoffeemaker' },
{ icon: 'deck', name: 'terrace' },
{ icon: 'iron', name: 'trouserpress' },
{ icon: 'tv_guide', name: 'tv' },
{ icon: 'tv_remote', name: 'tvwithcomplimentarymoviechannels' },
{ icon: 'tv_remote', name: 'tvwithmoviechannels' },
{ icon: 'live_tv', name: 'tvwithstreamingoption' },
{ icon: 'hvac', name: 'ventilationinroom' },
{ icon: 'landscape', name: 'viewfjordview' },
{ icon: 'houseboat', name: 'viewlakeview' },
{ icon: 'panorama', name: 'viewpanoramicview' },
{ icon: 'sailing', name: 'viewseaview' },
{ icon: 'ward', name: 'wallbed' },
{ icon: 'pedal_bike', name: 'wallmountedcyclerack' },
{ icon: 'dresser', name: 'wardrobe' },
{ icon: 'spa', name: 'wellnessandsaunaentrancefeeadmission16years' },
{ icon: 'spa', name: 'wellnesspoolsaunaentrancefeeadmission16years' },
{ icon: 'curtains', name: 'windownook' },
{ icon: 'iron', name: 'ironandironingboard' },
{ icon: 'iron', name: 'ironingroom' },
{ icon: 'kayaking', name: 'kayaksforloan' },
{ icon: 'kettle', name: 'kettle' },
{ icon: 'kettle', name: 'kettlewithcoffee' },
{ icon: 'sync_saved_locally', name: 'laptopsafe' },
{ icon: 'computer', name: 'laptoptray' },
{ icon: 'local_laundry_service', name: 'laundryservice' },
{ icon: 'local_bar', name: 'bar' },
{ icon: 'deck', name: 'rooftopbar' },
{ icon: 'local_bar', name: 'skybar' },
{ icon: 'microwave', name: 'microwave' },
{ icon: 'nature', name: 'viewparkview' },
{ icon: 'nightlife', name: 'disconightclub' },
{ icon: 'smoke_free', name: 'nonsmoking' },
{ icon: 'deck', name: 'outdoorterrace' },
{ icon: 'local_parking', name: 'parking' },
{ icon: 'local_parking', name: 'parkingfreeparking' },
{ icon: 'pets', name: 'petfriendlyrooms' },
{ icon: 'phone', name: 'directdialphoneandvoicemail' },
{ icon: 'restaurant', name: 'restaurant' },
{ icon: 'room_service', name: 'roomservice' },
{ icon: 'sauna', name: 'sauna' },
{ icon: 'shower', name: 'bathroomwithshower' },
{ icon: 'shower', name: 'rainshower' },
{ icon: 'shower', name: 'sharedbathroomwithshower' },
{ icon: 'radio', name: 'musicplayer' },
{ icon: 'shopping_bag', name: 'shop' },
{ icon: 'pool', name: 'pool' },
{ icon: 'laundry', name: 'handwash' },
{ icon: 'connected_tv', name: 'tvwithchromecast' },
{ icon: 'wifi', name: 'freewifi' },
{ icon: 'curtains_closed', name: 'blackoutcurtains' },
{ icon: 'liquor', name: 'minibar' },
{ icon: 'yard', name: 'viewatriumview' },
]
const CustomIconMappings = [
{ icon: BathroomCabinetIcon, name: 'bathroomwithshowerorbathtub' },
{ icon: MovingBedsIcon, name: 'adjustablebeds' },
{ icon: MovingBedsIcon, name: 'pulloutbed' },
{ icon: BedHotelIcon, name: 'extrabed' },
{ icon: CoolingIcon, name: 'coolingcabinet' },
{ icon: FootStoolIcon, name: 'footstool' },
{ icon: HairdryerIcon, name: 'hairdryer' },
{ icon: HairdresserIcon, name: 'hairdresser' },
{ icon: HandSoapIcon, name: 'toiletries' },
{ icon: TowelIcon, name: 'bathrobes' },
{ icon: HandSoapIcon, name: 'bodycareproducts' },
{ icon: HandSoapIcon, name: 'bodylotion' },
{ icon: BowlingPinsIcon, name: 'bowling' },
{ icon: BunkBedIcon, name: 'bunkbed' },
{ icon: BunkBedIcon, name: 'bunkbed80x188cm' },
{ icon: IceMachineIcon, name: 'icemachine' },
{ icon: IceMachineIcon, name: 'icemachinereception' },
{ icon: MassageIcon, name: 'massage' },
{ icon: ConferenceRoomIcon, name: 'meetingarea' },
{ icon: UserPoliceIcon, name: 'overnightsecurity' },
{ icon: UserPoliceIcon, name: 'security24hoours' },
{ icon: UserPoliceIcon, name: 'servicesecurity24h' },
{ icon: FootStoolIcon, name: 'footstool' },
{ icon: WindowNotAvailableIcon, name: 'nowindow' },
{ icon: DoorIcon, name: 'luggagelockers' },
{ icon: MirrorIcon, name: 'cosmeticmirror' },
{ icon: WardIcon, name: 'wallbed' },
{ icon: CouchTableIcon, name: 'sofawithtable' },
{ icon: SafetyBoxIcon, name: 'safetybox' },
{ icon: RecordPlayerIcon, name: 'modernvinylplayer' },
{ icon: RugIcon, name: 'carpetingwalltowallcarpet' },
{ icon: ToiletIcon, name: 'separatetoilet' },
{ icon: WoodFloorIcon, name: 'woodenfloor' },
{ icon: ViewIcon, name: 'view' },
{ icon: HandSoapIcon, name: 'shampoo' },
{ icon: HandSoapIcon, name: 'conditioner' },
{ icon: RoadIcon, name: 'viewstreetview' },
{ icon: SlippersIcon, name: 'slippers' },
{ icon: HandSoapIcon, name: 'showergel' },
{ icon: HandSoapIcon, name: 'showerproductsscentbygrandcentral' },
]

View File

@@ -78,6 +78,7 @@
"./Icons/DiamondAddIcon": "./lib/components/Icons/Customised/Benefits/DiamondAdd.tsx",
"./Icons/DiscountIcon": "./lib/components/Icons/Nucleo/Benefits/discount-2-2.tsx",
"./Icons/FilledDiscountIcon": "./lib/components/Icons/Nucleo/Benefits/FilledDiscount.tsx",
"./Icons/FacilityIcon": "./lib/components/Icons/FacilityIcon.tsx",
"./Icons/DoorIcon": "./lib/components/Icons/Nucleo/Amenities_Facilities/door-2.tsx",
"./Icons/DowntownCamperIcon": "./lib/components/Icons/Logos/DowntownCamper.tsx",
"./Icons/FacebookIcon": "./lib/components/Icons/Customised/Socials/Facebook.tsx",