Merged in feat/SW-3289-replace-sidepeek-hotel-reservation (pull request #2686)

feat(SW-3289): replace sidepeek

* fix(SW-3289): replace sidepeek

* fix(SW-3289): add wrapping prop and change prop name to buttonVariant

* fix(SW-3289): replace body with typography

* fix(SW-3289): fix intl message


Approved-by: Joakim Jäderberg
This commit is contained in:
Bianca Widstam
2025-08-22 11:43:39 +00:00
parent e2544f9f89
commit d9b858c823
47 changed files with 527 additions and 708 deletions

View File

@@ -125,6 +125,7 @@ export default async function DetailsPage(
searchParamsStr={selectRoomParams.toString()}
user={user}
vat={hotel.vat}
roomCategories={hotelData.roomCategories}
>
<HotelHeader hotelData={hotelData} />
<div className={styles.container}>

View File

@@ -1,5 +1,3 @@
import SidePeek from "@scandic-hotels/booking-flow/components/HotelReservationSidePeek"
import styles from "./layout.module.css"
import type { LangParams, LayoutArgs } from "@/types/params"
@@ -7,10 +5,5 @@ import type { LangParams, LayoutArgs } from "@/types/params"
export default function StandardHotelReservationLayout({
children,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
return (
<div className={styles.layout}>
{children}
<SidePeek />
</div>
)
return <div className={styles.layout}>{children}</div>
}

View File

@@ -1,48 +0,0 @@
"use client"
import { DialogTrigger } from "react-aria-components"
import { useIntl } from "react-intl"
import { RoomSidePeekContent } from "@scandic-hotels/booking-flow/components/RoomSidePeekContent"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { getBookedHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
import SidePeekSelfControlled from "@/components/TempDesignSystem/SidePeekSelfControlled"
interface RoomDetailsSidePeekProps {
roomTypeCode: string
}
export default function RoomDetailsSidePeek({
roomTypeCode,
}: RoomDetailsSidePeekProps) {
const { roomCategories } = useBookingConfirmationStore((state) => ({
roomCategories: state.roomCategories,
}))
const room = getBookedHotelRoom(roomCategories, roomTypeCode)
const intl = useIntl()
if (!room) {
return null
}
return (
<DialogTrigger>
<Button
variant="Text"
color="Primary"
size="Small"
typography="Body/Supporting text (caption)/smBold"
>
{intl.formatMessage({ defaultMessage: "View room details" })}
<MaterialIcon icon="chevron_right" size={14} color="CurrentColor" />
</Button>
<SidePeekSelfControlled title={room.name}>
<RoomSidePeekContent room={room} />
</SidePeekSelfControlled>
</DialogTrigger>
)
}

View File

@@ -11,13 +11,14 @@ import Caption from "@scandic-hotels/design-system/Caption"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Image from "@scandic-hotels/design-system/Image"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { getHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
import { CancellationRuleEnum } from "@/constants/booking"
import { useBookingConfirmationStore } from "@/stores/booking-confirmation"
import RoomDetailsSidePeek from "@/components/SidePeeks/RoomDetailsSidePeek"
import useLang from "@/hooks/useLang"
import RoomDetailsSidePeek from "./RoomDetailsSidePeek"
import styles from "./room.module.css"
import type { RoomProps } from "@/types/components/hotelReservation/bookingConfirmation/rooms/room"
@@ -31,6 +32,10 @@ export default function Room({
}: RoomProps) {
const intl = useIntl()
const lang = useLang()
const { roomCategories } = useBookingConfirmationStore((state) => ({
roomCategories: state.roomCategories,
}))
const room = getHotelRoom(roomCategories, booking.roomTypeCode)
const guestName = `${booking.guest.firstName} ${booking.guest.lastName}`
const fromDate = dt(booking.checkInDate).locale(lang)
@@ -116,7 +121,18 @@ export default function Room({
<Typography variant="Title/Subtitle/md">
<h2>{roomName}</h2>
</Typography>
<RoomDetailsSidePeek roomTypeCode={booking.roomTypeCode} />
{room && (
<RoomDetailsSidePeek
hotelId={booking.hotelId}
room={room}
roomTypeCode={booking.roomTypeCode}
buttonVariant="primary"
triggerLabel={intl.formatMessage({
defaultMessage: "View room details",
})}
wrapping={false}
/>
)}
</div>
<Typography variant="Body/Paragraph/mdRegular">
<ul className={styles.details}>

View File

@@ -1,41 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import useSidePeekStore, {
SidePeekEnum,
} from "@scandic-hotels/booking-flow/stores/sidepeek"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { trackOpenSidePeekEvent } from "@/utils/tracking"
import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
export default function ToggleSidePeek({ hotelId }: ToggleSidePeekProps) {
const intl = useIntl()
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
return (
<Button
onPress={() => {
openSidePeek({ key: SidePeekEnum.hotelDetails, hotelId })
trackOpenSidePeekEvent({
name: SidePeekEnum.hotelDetails,
hotelId,
includePathname: true,
})
}}
size="Small"
variant="Secondary"
color="Inverted"
wrapping
typography="Body/Paragraph/mdBold"
>
{intl.formatMessage({
defaultMessage: "See hotel details",
})}
<MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
</Button>
)
}

View File

@@ -2,17 +2,18 @@ import Image from "@scandic-hotels/design-system/Image"
import Title from "@scandic-hotels/design-system/Title"
import { Typography } from "@scandic-hotels/design-system/Typography"
import ToggleSidePeek from "./ToggleSidePeek"
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
import { getIntl } from "@/i18n"
import styles from "./header.module.css"
import type { HotelHeaderProps } from "@/types/components/hotelReservation/enterDetails/hotelHeader"
export default async function HotelHeader({
hotelData: { hotel },
hotelData: { hotel, url, restaurants, additionalData },
}: HotelHeaderProps) {
const image = hotel.hotelContent?.images
const intl = await getIntl()
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`
return (
@@ -49,7 +50,16 @@ export default async function HotelHeader({
<div className={styles.address}>{addressStr}</div>
</Typography>
</div>
<ToggleSidePeek hotelId={hotel.operaId} />
<HotelDetailsSidePeek
hotel={{ ...hotel, url: url }}
restaurants={restaurants}
additionalHotelData={additionalData}
triggerLabel={intl.formatMessage({
defaultMessage: "See hotel details",
})}
buttonVariant={"secondary"}
/>
</div>
</div>
</header>

View File

@@ -1,46 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import useSidePeekStore, {
SidePeekEnum,
} from "@scandic-hotels/booking-flow/stores/sidepeek"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { trackOpenSidePeekEvent } from "@/utils/tracking"
import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
export default function ToggleSidePeek({
hotelId,
roomTypeCode,
title,
}: ToggleSidePeekProps) {
const intl = useIntl()
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
return (
<Button
onPress={() => {
openSidePeek({ key: SidePeekEnum.roomDetails, hotelId, roomTypeCode })
trackOpenSidePeekEvent({
name: SidePeekEnum.roomDetails,
hotelId,
roomTypeCode,
includePathname: true,
})
}}
size="Small"
variant="Text"
wrapping
typography="Body/Paragraph/mdBold"
>
{title ||
intl.formatMessage({
defaultMessage: "See room details",
})}
<MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
</Button>
)
}

View File

@@ -9,14 +9,14 @@ import { Button } from "@scandic-hotels/design-system/Button"
import Footnote from "@scandic-hotels/design-system/Footnote"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Subtitle from "@scandic-hotels/design-system/Subtitle"
import { getHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
import { useEnterDetailsStore } from "@/stores/enter-details"
import RoomDetailsSidePeek from "@/components/SidePeeks/RoomDetailsSidePeek"
import { useRoomContext } from "@/contexts/Details/Room"
import useLang from "@/hooks/useLang"
import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./selectedRoom.module.css"
export default function SelectedRoom() {
@@ -25,10 +25,13 @@ export default function SelectedRoom() {
const router = useRouter()
const [isPending, startTransition] = useTransition()
const { room, idx } = useRoomContext()
const { hotelId, searchParamsStr } = useEnterDetailsStore((state) => ({
hotelId: state.booking.hotelId,
searchParamsStr: state.searchParamString,
}))
const { hotelId, roomCategories, searchParamsStr } = useEnterDetailsStore(
(state) => ({
hotelId: state.booking.hotelId,
roomCategories: state.roomCategories,
searchParamsStr: state.searchParamString,
})
)
function changeRoom() {
const searchParams = new URLSearchParams(searchParamsStr)
@@ -39,6 +42,8 @@ export default function SelectedRoom() {
})
}
const selectedRoom = getHotelRoom(roomCategories, room.roomTypeCode)
return (
<div className={styles.wrapper} data-available={room.isAvailable}>
<div className={styles.main}>
@@ -79,6 +84,7 @@ export default function SelectedRoom() {
size="Small"
onPress={changeRoom}
isDisabled={isPending}
wrapping={false}
typography="Body/Supporting text (caption)/smBold"
>
<MaterialIcon icon="edit_square" size={20} color="CurrentColor" />
@@ -87,11 +93,17 @@ export default function SelectedRoom() {
})}
</Button>
</div>
{room.roomTypeCode && (
{room.roomTypeCode && selectedRoom && (
<div className={styles.details}>
<ToggleSidePeek
<RoomDetailsSidePeek
hotelId={hotelId}
roomTypeCode={room.roomTypeCode}
room={selectedRoom}
buttonVariant="primary"
triggerLabel={intl.formatMessage({
defaultMessage: "See room details",
})}
wrapping={false}
/>
</div>
)}

View File

@@ -48,6 +48,7 @@
.details {
display: flex;
justify-content: flex-start;
margin-top: var(--Space-x05);
}
@media screen and (min-width: 768px) {

View File

@@ -10,9 +10,7 @@ import {
import { memo } from "react"
import { useIntl } from "react-intl"
import OpenSidePeekButton from "@scandic-hotels/booking-flow/components/OpenSidePeekButton"
import TripAdvisorChip from "@scandic-hotels/booking-flow/components/TripAdvisorChip"
import { SidePeekEnum } from "@scandic-hotels/booking-flow/stores/sidepeek"
import {
alternativeHotelsMap,
selectHotelMap,
@@ -30,6 +28,7 @@ import { useHotelsMapStore } from "@/stores/hotels-map"
import BookingCodeChip from "@/components/BookingCodeChip"
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
import HotelChequeCard from "./HotelChequeCard"
@@ -47,7 +46,7 @@ import { HotelCardListingTypeEnum } from "@/types/components/hotelReservation/se
import type { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps"
function HotelCard({
hotelData: { availability, hotel },
hotelData: { availability, hotel, additionalData, restaurants, url },
isUserLoggedIn,
state = "default",
type = HotelCardListingTypeEnum.PageListing,
@@ -184,13 +183,14 @@ function HotelCard({
</div>
))}
</div>
<OpenSidePeekButton
label={intl.formatMessage({
<HotelDetailsSidePeek
hotel={{ ...hotel, url: url }}
restaurants={restaurants}
additionalHotelData={additionalData}
triggerLabel={intl.formatMessage({
defaultMessage: "See hotel details",
})}
hotelId={hotel.operaId}
showCTA={true}
sidePeekKey={SidePeekEnum.hotelDetails}
buttonVariant="primary"
/>
</div>
<PricesWrapper

View File

@@ -4,9 +4,11 @@ import { Button as ButtonRAC, DialogTrigger } from "react-aria-components"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import { trackOpenSidePeekEvent } from "@/utils/tracking"
import styles from "./sidePeek.module.css"
import { SidePeekEnum } from "@/types/sidepeek"
import type { Room as MyStayRoom } from "@/types/stores/my-stay"
import type { SafeUser } from "@/types/user"
@@ -21,7 +23,16 @@ export default function RoomDetailsSidePeek({
}: RoomDetailsSidePeekProps) {
return (
<DialogTrigger>
<ButtonRAC className={styles.trigger}>
<ButtonRAC
className={styles.trigger}
onPress={() => {
trackOpenSidePeekEvent({
name: SidePeekEnum.bookedRoomDetails,
hotelId: booking.hotelId,
includePathname: true,
})
}}
>
<MaterialIcon icon="pan_zoom" color="CurrentColor" />
</ButtonRAC>
<BookedRoomSidePeek hotelRoom={booking.room} room={booking} user={user} />

View File

@@ -9,7 +9,9 @@ import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton
import { useMyStayStore } from "@/stores/my-stay"
import BookedRoomSidePeek from "@/components/SidePeeks/BookedRoomSidePeek"
import { trackOpenSidePeekEvent } from "@/utils/tracking"
import { SidePeekEnum } from "@/types/sidepeek"
import type { SafeUser } from "@/types/user"
interface RoomDetailsSidePeekProps {
@@ -21,9 +23,23 @@ export default function RoomDetailsSidePeek({
}: RoomDetailsSidePeekProps) {
const intl = useIntl()
const bookedRoom = useMyStayStore((state) => state.bookedRoom)
return (
<DialogTrigger>
<Button intent="text" size="small" theme="base" variant="icon" wrapping>
<Button
intent="text"
size="small"
theme="base"
variant="icon"
wrapping
onPress={() => {
trackOpenSidePeekEvent({
name: SidePeekEnum.bookedRoomDetails,
hotelId: bookedRoom.hotelId,
includePathname: true,
})
}}
>
{intl.formatMessage({ defaultMessage: "See room details" })}
<MaterialIcon icon="chevron_right" size={14} color="CurrentColor" />
</Button>

View File

@@ -1,7 +1,6 @@
import { cookies } from "next/headers"
import { notFound } from "next/navigation"
import SidePeek from "@scandic-hotels/booking-flow/components/HotelReservationSidePeek"
import { dt } from "@scandic-hotels/common/dt"
import { logger } from "@scandic-hotels/common/logger"
import * as maskValue from "@scandic-hotels/common/utils/maskValue"
@@ -268,7 +267,6 @@ export default async function MyStay(props: {
)}
</div>
</main>
<SidePeek />
</MyStayProvider>
)
}

View File

@@ -9,7 +9,11 @@ import { getLang } from "@/i18n/serverContext"
import type { HotelsAvailabilityItem } from "@scandic-hotels/trpc/types/availability"
import type { Child } from "@scandic-hotels/trpc/types/child"
import type { AdditionalData, Hotel } from "@scandic-hotels/trpc/types/hotel"
import type {
AdditionalData,
Hotel,
Restaurant,
} from "@scandic-hotels/trpc/types/hotel"
import type {
HotelLocation,
Location,
@@ -32,6 +36,8 @@ export interface HotelResponse {
availability: HotelsAvailabilityItem
hotel: Hotel
additionalData: AdditionalData
url: string | null
restaurants: Restaurant[]
}
type Result = AvailabilityResponse | null
@@ -55,6 +61,8 @@ async function enhanceHotels(hotels: HotelsAvailabilityItem[]) {
availability,
hotel: hotelData.hotel,
additionalData: hotelData.additionalData,
url: hotelData.url,
restaurants: hotelData.restaurants,
}
})
)

View File

@@ -4,25 +4,32 @@ import { useState } from "react"
import { Button as ButtonRAC } from "react-aria-components"
import { useIntl } from "react-intl"
import OpenSidePeekButton from "@scandic-hotels/booking-flow/components/OpenSidePeekButton"
import { SidePeekEnum } from "@scandic-hotels/booking-flow/stores/sidepeek"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
import Alert from "@/components/TempDesignSystem/Alert"
import styles from "./hotelDescription.module.css"
import type { Hotel } from "@scandic-hotels/trpc/types/hotel"
import type {
AdditionalData,
Hotel,
Restaurant,
} from "@scandic-hotels/trpc/types/hotel"
export default function HotelDescription({
description,
hotel,
sortedFacilities,
restaurants,
additionalData,
}: {
description?: string
hotel: Hotel
sortedFacilities: Hotel["detailedFacilities"]
restaurants: Restaurant[]
additionalData: AdditionalData | undefined
}) {
const intl = useIntl()
@@ -69,13 +76,14 @@ export default function HotelDescription({
{expanded && (
<div className={styles.expandedContent}>
<OpenSidePeekButton
label={intl.formatMessage({
<HotelDetailsSidePeek
hotel={{ ...hotel, url: null }}
restaurants={restaurants}
additionalHotelData={additionalData}
triggerLabel={intl.formatMessage({
defaultMessage: "See all amenities",
})}
hotelId={hotel.operaId}
showCTA={false}
sidePeekKey={SidePeekEnum.hotelDetails}
buttonVariant="primary"
/>
{hotel.specialAlerts.map((alert) => (
<Alert

View File

@@ -1,6 +1,4 @@
import OpenSidePeekButton from "@scandic-hotels/booking-flow/components/OpenSidePeekButton"
import TripAdvisorChip from "@scandic-hotels/booking-flow/components/TripAdvisorChip"
import { SidePeekEnum } from "@scandic-hotels/booking-flow/stores/sidepeek"
import { dt } from "@scandic-hotels/common/dt"
import { getSingleDecimal } from "@scandic-hotels/common/utils/numberFormatting"
import { Divider } from "@scandic-hotels/design-system/Divider"
@@ -9,6 +7,7 @@ import SkeletonShimmer from "@scandic-hotels/design-system/SkeletonShimmer"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { FacilityToIcon } from "@/components/ContentType/HotelPage/data"
import HotelDetailsSidePeek from "@/components/SidePeeks/HotelDetailsSidePeek"
import Alert from "@/components/TempDesignSystem/Alert"
import { getIntl } from "@/i18n"
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
@@ -18,16 +17,27 @@ import HotelDescription from "./HotelDescription"
import styles from "./hotelInfoCard.module.css"
import type { Hotel } from "@scandic-hotels/trpc/types/hotel"
import type {
AdditionalData,
Hotel,
Restaurant,
} from "@scandic-hotels/trpc/types/hotel"
import type { SelectRateBooking } from "@/types/components/hotelReservation/selectRate/selectRate"
export type HotelInfoCardProps = {
booking: SelectRateBooking
hotel: Hotel
restaurants: Restaurant[]
additionalData: AdditionalData | undefined
}
export async function HotelInfoCard({ booking, hotel }: HotelInfoCardProps) {
export async function HotelInfoCard({
booking,
hotel,
restaurants,
additionalData,
}: HotelInfoCardProps) {
const intl = await getIntl()
const sortedFacilities = hotel.detailedFacilities
@@ -85,6 +95,8 @@ export async function HotelInfoCard({ booking, hotel }: HotelInfoCardProps) {
key={hotel.operaId}
description={hotel.hotelContent.texts.descriptions?.medium}
hotel={hotel}
restaurants={restaurants}
additionalData={additionalData}
sortedFacilities={sortedFacilities}
/>
</div>
@@ -101,13 +113,14 @@ export async function HotelInfoCard({ booking, hotel }: HotelInfoCardProps) {
</div>
))}
</div>
<OpenSidePeekButton
label={intl.formatMessage({
<HotelDetailsSidePeek
hotel={{ ...hotel, url: null }}
restaurants={restaurants}
additionalHotelData={additionalData}
triggerLabel={intl.formatMessage({
defaultMessage: "See all amenities",
})}
hotelId={hotel.operaId}
showCTA={false}
sidePeekKey={SidePeekEnum.hotelDetails}
buttonVariant="primary"
/>
</div>
</div>

View File

@@ -1,50 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import useSidePeekStore, {
SidePeekEnum,
} from "@scandic-hotels/booking-flow/stores/sidepeek"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { trackOpenSidePeekEvent } from "@/utils/tracking"
import styles from "./details.module.css"
import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
export default function ToggleSidePeek({
hotelId,
roomTypeCode,
}: ToggleSidePeekProps) {
const intl = useIntl()
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
return (
<Button
onClick={() => {
openSidePeek({
key: SidePeekEnum.roomDetails,
hotelId,
roomTypeCode,
})
trackOpenSidePeekEvent({
name: SidePeekEnum.roomDetails,
hotelId,
roomTypeCode,
includePathname: true,
})
}}
size="Small"
variant="Text"
wrapping
typography="Body/Supporting text (caption)/smBold"
color="Inverted"
className={styles.sidePeekButton}
>
{intl.formatMessage({ defaultMessage: "View room details" })}
<MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
</Button>
)
}

View File

@@ -4,12 +4,13 @@ import { useIntl } from "react-intl"
import ImageGallery from "@scandic-hotels/design-system/ImageGallery"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { getHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
import RoomDetailsSidePeek from "@/components/SidePeeks/RoomDetailsSidePeek"
import { useSelectRateContext } from "@/contexts/SelectRate/SelectRateContext"
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
import ToggleSidePeek from "../Details/ToggleSidePeek"
import styles from "./image.module.css"
import type { ApiImage } from "@scandic-hotels/trpc/types/hotel"
@@ -34,7 +35,9 @@ const RoomImage = memo(function RoomImage({
hotelId,
}: RoomListItemImageProps) {
const galleryImages = mapApiImagesToGalleryImages(images || [])
const { hotel } = useSelectRateContext()
const room = getHotelRoom(hotel?.data?.roomCategories ?? [], roomTypeCode)
const intl = useIntl()
return (
<div className={styles.imageContainer}>
<div className={styles.chipContainer}>
@@ -53,8 +56,16 @@ const RoomImage = memo(function RoomImage({
imageCountPosition="top"
/>
<div className={styles.toggleSidePeek}>
{roomTypeCode && (
<ToggleSidePeek hotelId={hotelId} roomTypeCode={roomTypeCode} />
{roomTypeCode && room && (
<RoomDetailsSidePeek
hotelId={hotelId}
room={room}
roomTypeCode={roomTypeCode}
triggerLabel={intl.formatMessage({
defaultMessage: "View room details",
})}
buttonVariant="secondary"
/>
)}
</div>
</div>

View File

@@ -29,7 +29,12 @@ export default async function SelectRatePage({
}
return (
<>
<HotelInfoCard hotel={hotelData.hotel} booking={booking} />
<HotelInfoCard
hotel={hotelData.hotel}
restaurants={hotelData.restaurants}
additionalData={hotelData.additionalData}
booking={booking}
/>
{isInValidFNF ? (
<FnFNotAllowedAlert />

View File

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

View File

@@ -0,0 +1,107 @@
import { useIntl } from "react-intl"
import AdditionalAmenities from "@scandic-hotels/booking-flow/components/AdditionalAmenities"
import Contact from "@scandic-hotels/booking-flow/components/Contact"
import BreakfastAccordionItem from "@scandic-hotels/booking-flow/components/SidePeekAccordions/BreakfastAccordionItem"
import CheckInCheckOutAccordionItem from "@scandic-hotels/booking-flow/components/SidePeekAccordions/CheckInCheckOutAccordionItem"
import ParkingAccordionItem from "@scandic-hotels/booking-flow/components/SidePeekAccordions/ParkingAccordionItem"
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 { Typography } from "@scandic-hotels/design-system/Typography"
import { trackAccordionClick } from "@/utils/tracking"
import styles from "./hotelSidePeek.module.css"
import type {
AdditionalData,
Hotel,
Restaurant,
} from "@scandic-hotels/trpc/types/hotel"
interface HotelSidePeekContentProps {
hotel: Hotel & { url: string | null }
restaurants: Restaurant[]
additionalHotelData: AdditionalData | undefined
}
export function HotelSidePeekContent({
hotel,
restaurants,
additionalHotelData,
}: HotelSidePeekContentProps) {
const intl = useIntl()
return (
<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>
)
}
type AccessibilityAccordionItemProps = {
elevatorPitch?: string
}
function AccessibilityAccordionItem({
elevatorPitch,
}: AccessibilityAccordionItemProps) {
const intl = useIntl()
if (!elevatorPitch) {
return null
}
return (
<AccordionItem
title={intl.formatMessage({
defaultMessage: "Accessibility",
})}
iconName={IconName.Accessibility}
className={styles.accordionItem}
variant="sidepeek"
onOpen={() => trackAccordionClick("amenities:accessibility")}
>
<div className={styles.accessibilityContent}>
<Typography variant="Body/Paragraph/mdRegular">
<p>{elevatorPitch}</p>
</Typography>
</div>
</AccordionItem>
)
}

View File

@@ -0,0 +1,89 @@
"use client"
import { DialogTrigger } from "react-aria-components"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import SidePeekSelfControlled from "@/components/TempDesignSystem/SidePeekSelfControlled"
import { trackOpenSidePeekEvent } from "@/utils/tracking"
import { HotelSidePeekContent } from "./HotelSidePeekContent"
import type {
AdditionalData,
Hotel,
Restaurant,
} from "@scandic-hotels/trpc/types/hotel"
import { SidePeekEnum } from "@/types/sidepeek"
interface HotelDetailsSidePeekProps {
hotel: Hotel & { url: string | null }
restaurants: Restaurant[]
additionalHotelData: AdditionalData | undefined
triggerLabel: string
buttonVariant: "primary" | "secondary"
}
const buttonPropsMap: Record<
HotelDetailsSidePeekProps["buttonVariant"],
Pick<
React.ComponentProps<typeof Button>,
"variant" | "color" | "size" | "typography"
>
> = {
primary: {
variant: "Text",
color: "Primary",
size: "Medium",
typography: "Body/Paragraph/mdBold",
},
secondary: {
variant: "Secondary",
color: "Inverted",
size: "Small",
typography: "Body/Supporting text (caption)/smBold",
},
}
export default function HotelDetailsSidePeek({
hotel,
restaurants,
additionalHotelData,
triggerLabel,
buttonVariant,
}: HotelDetailsSidePeekProps) {
const buttonProps = buttonPropsMap[buttonVariant]
return (
<DialogTrigger>
<Button
{...buttonProps}
wrapping
onPress={() =>
trackOpenSidePeekEvent({
name: SidePeekEnum.hotelDetails,
hotelId: hotel.operaId,
includePathname: true,
})
}
>
{triggerLabel}
<MaterialIcon
icon="chevron_right"
size={buttonVariant === "primary" ? 24 : 20}
color="CurrentColor"
/>
</Button>
<SidePeekSelfControlled title={hotel.name}>
<HotelSidePeekContent
hotel={hotel}
restaurants={restaurants}
additionalHotelData={additionalHotelData}
/>
</SidePeekSelfControlled>
</DialogTrigger>
)
}

View File

@@ -0,0 +1,176 @@
import { useIntl } from "react-intl"
import {
BED_TYPE_ICONS,
type BedTypes,
} from "@scandic-hotels/booking-flow/bedTypeIcons"
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 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,62 @@
.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 > span:nth-child(2) {
overflow: hidden;
word-wrap: break-word;
}
.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,80 @@
"use client"
import { DialogTrigger } from "react-aria-components"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import SidePeekSelfControlled from "@/components/TempDesignSystem/SidePeekSelfControlled"
import { trackOpenSidePeekEvent } from "@/utils/tracking"
import { RoomSidePeekContent } from "./RoomSidePeekContent"
import type { Room } from "@scandic-hotels/trpc/types/hotel"
import { SidePeekEnum } from "@/types/sidepeek"
interface RoomDetailsSidePeekProps {
hotelId: string
room: Room
triggerLabel: string
roomTypeCode?: string
buttonVariant?: "primary" | "secondary"
wrapping?: boolean
}
const buttonPropsMap: Record<
NonNullable<RoomDetailsSidePeekProps["buttonVariant"]>,
Pick<
React.ComponentProps<typeof Button>,
"variant" | "color" | "size" | "typography"
>
> = {
primary: {
variant: "Text",
color: "Primary",
size: "Medium",
typography: "Body/Paragraph/mdBold",
},
secondary: {
variant: "Text",
color: "Inverted",
size: "Small",
typography: "Body/Supporting text (caption)/smBold",
},
}
export default function RoomDetailsSidePeek({
hotelId,
room,
roomTypeCode,
triggerLabel,
wrapping = true,
buttonVariant: variant = "primary",
}: RoomDetailsSidePeekProps) {
const buttonProps = buttonPropsMap[variant]
return (
<DialogTrigger>
<Button
{...buttonProps}
wrapping={wrapping}
onPress={() =>
trackOpenSidePeekEvent({
name: SidePeekEnum.roomDetails,
hotelId,
roomTypeCode,
includePathname: true,
})
}
>
{triggerLabel}
<MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
</Button>
<SidePeekSelfControlled title={room.name}>
<RoomSidePeekContent room={room} />
</SidePeekSelfControlled>
</DialogTrigger>
)
}

View File

@@ -4,8 +4,8 @@ import { useEffect } from "react"
import { Dialog, Modal, ModalOverlay } from "react-aria-components"
import { useIntl } from "react-intl"
import { IconButton } from "@scandic-hotels/design-system/IconButton"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { OldDSButton as Button } from "@scandic-hotels/design-system/OldDSButton"
import { Typography } from "@scandic-hotels/design-system/Typography"
import useSetOverflowVisibleOnRA from "@/hooks/useSetOverflowVisibleOnRA"
@@ -34,19 +34,20 @@ export default function SidePeekSelfControlled({
<h2>{title}</h2>
</Typography>
) : null}
<Button
<IconButton
theme="Black"
style="Muted"
onPress={close}
aria-label={intl.formatMessage({
defaultMessage: "Close",
})}
className={styles.closeButton}
intent="text"
onPress={close}
>
<MaterialIcon
icon="close"
size={24}
color="Icon/Interactive/Default"
/>
</Button>
</IconButton>
</header>
<div className={styles.sidePeekContent}>{children}</div>
<KeepBodyVisible />

View File

@@ -72,10 +72,6 @@
justify-content: space-between;
}
.closeButton {
padding: 0;
}
.heading {
color: var(--Text-Heading);
}

View File

@@ -32,6 +32,7 @@ export default function EnterDetailsProvider({
searchParamsStr,
user,
vat,
roomCategories,
}: DetailsProviderProps) {
// This state is needed to be able to use defaultValues for
// react-hook-form since values needs to be there on mount
@@ -70,6 +71,7 @@ export default function EnterDetailsProvider({
isFlexRate: room.isFlexRate,
})),
vat,
roomCategories,
}
storeRef.current = createDetailsStore(

View File

@@ -97,6 +97,7 @@ export function createDetailsStore(
return create<DetailsState>()((set) => ({
availableBeds,
booking: initialState.booking,
roomCategories: initialState.roomCategories,
breakfastPackages,
canProceedToPayment: false,
isSubmitting: false,

View File

@@ -3,7 +3,7 @@ import { produce } from "immer"
import { useContext } from "react"
import { create, useStore } from "zustand"
import { getBookedHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
import { getHotelRoom } from "@scandic-hotels/trpc/routers/booking/helpers"
import { mapRoomDetails } from "@/components/HotelReservation/MyStay/utils/mapRoomDetails"
import { MyStayContext } from "@/contexts/MyStay"
@@ -39,7 +39,7 @@ export function createMyStayStore({
}
const mappedRooms = rooms.map((booking, idx) => {
const room = getBookedHotelRoom(roomCategories, booking.roomTypeCode)
const room = getHotelRoom(roomCategories, booking.roomTypeCode)
return mapRoomDetails({
booking,
rates,

View File

@@ -1,14 +0,0 @@
import type { SidePeekEnum } from "@scandic-hotels/booking-flow/stores/sidepeek"
import type {
AdditionalData,
Hotel,
Restaurant,
} from "@scandic-hotels/trpc/types/hotel"
export type AmenitiesSidePeekProps = {
hotel: Hotel & { url: string | null }
restaurants: Restaurant[]
additionalHotelData: AdditionalData | undefined
activeSidePeek: SidePeekEnum
close: () => void
}

View File

@@ -1,5 +1,5 @@
import type { HotelData } from "@scandic-hotels/trpc/types/hotel"
export interface HotelHeaderProps {
hotelData: HotelData
hotelData: HotelData & { url: string | null }
}

View File

@@ -1,9 +0,0 @@
import type { SafeUser } from "@/types/user"
export type ToggleSidePeekProps = {
hotelId: string
roomTypeCode?: string
title?: string
user?: SafeUser
confirmationNumber?: string
}

View File

@@ -1,4 +1,5 @@
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { RoomCategories } from "@scandic-hotels/trpc/types/hotel"
import type { Room } from "@scandic-hotels/trpc/types/room"
import type { SafeUser } from "@/types/user"
@@ -13,4 +14,5 @@ export interface DetailsProviderProps extends React.PropsWithChildren {
searchParamsStr: string
user: SafeUser
vat: number
roomCategories: RoomCategories
}

View File

@@ -0,0 +1,5 @@
export enum SidePeekEnum {
hotelDetails = "hotel-detail-side-peek",
roomDetails = "room-detail-side-peek",
bookedRoomDetails = "booked-room-detail-side-peek",
}

View File

@@ -1,6 +1,7 @@
import type { CurrencyEnum } from "@scandic-hotels/common/constants/currency"
import type { BedTypeSelection } from "@scandic-hotels/trpc/types/bedTypeSelection"
import type { Child } from "@scandic-hotels/trpc/types/child"
import type { RoomCategories } from "@scandic-hotels/trpc/types/hotel"
import type { Packages } from "@scandic-hotels/trpc/types/packages"
import type {
@@ -80,6 +81,7 @@ export type InitialState = {
booking: DetailsBooking
rooms: InitialRoomData[]
vat: number
roomCategories: RoomCategories
}
export interface DetailsState {
@@ -100,6 +102,7 @@ export interface DetailsState {
searchParamString: string
totalPrice: Price
vat: number
roomCategories: RoomCategories
defaultCurrency: CurrencyEnum
preSubmitCallbacks: Record<string, () => void>
}