Merged in feat/SW-1936-ui-room-card (pull request #2268)

feat(SW-1936): update room card ui

* feat(SW-1936): update room card ui


Approved-by: Linus Flood
This commit is contained in:
Bianca Widstam
2025-06-03 12:51:33 +00:00
parent 9580281421
commit 984805ea8d
11 changed files with 154 additions and 95 deletions

View File

@@ -1,7 +1,7 @@
"use client" "use client"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import Caption from "@/components/TempDesignSystem/Text/Caption" import { Typography } from "@scandic-hotels/design-system/Typography"
import type { RoomSizeProps } from "@/types/components/hotelReservation/selectRate/roomListItem" import type { RoomSizeProps } from "@/types/components/hotelReservation/selectRate/roomListItem"
@@ -15,34 +15,42 @@ export default function RoomSize({ roomSize }: RoomSizeProps) {
if (roomSize.min === roomSize.max) { if (roomSize.min === roomSize.max) {
return ( return (
<> <>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} <Typography variant="Body/Supporting text (caption)/smBold">
<Caption color="uiTextMediumContrast"></Caption> {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<Caption color="uiTextMediumContrast"> <p></p>
{intl.formatMessage( </Typography>
{ <Typography variant="Body/Supporting text (caption)/smBold">
defaultMessage: "{roomSize} m²", <h4>
}, {intl.formatMessage(
{ roomSize: roomSize.min } {
)} defaultMessage: "{roomSize} m²",
</Caption> },
{ roomSize: roomSize.min }
)}
</h4>
</Typography>
</> </>
) )
} }
return ( return (
<> <>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} <Typography variant="Body/Supporting text (caption)/smBold">
<Caption color="uiTextMediumContrast"></Caption> {/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<Caption color="uiTextMediumContrast"> <p></p>
{intl.formatMessage( </Typography>
{ <Typography variant="Body/Supporting text (caption)/smBold">
defaultMessage: "{roomSizeMin} - {roomSizeMax} m²", <h4>
}, {intl.formatMessage(
{ {
roomSizeMin: roomSize.min, defaultMessage: "{roomSizeMin} - {roomSizeMax} m²",
roomSizeMax: roomSize.max, },
} {
)} roomSizeMin: roomSize.min,
</Caption> roomSizeMax: roomSize.max,
}
)}
</h4>
</Typography>
</> </>
) )
} }

View File

@@ -7,6 +7,8 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import useSidePeekStore from "@/stores/sidepeek" import useSidePeekStore from "@/stores/sidepeek"
import styles from "./details.module.css"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek" import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps" import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
@@ -30,8 +32,10 @@ export default function ToggleSidePeek({
variant="Text" variant="Text"
wrapping wrapping
typography="Body/Supporting text (caption)/smBold" typography="Body/Supporting text (caption)/smBold"
color="Inverted"
className={styles.sidePeekButton}
> >
{intl.formatMessage({ defaultMessage: "Room details" })} {intl.formatMessage({ defaultMessage: "View room details" })}
<MaterialIcon icon="chevron_right" size={20} color="CurrentColor" /> <MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
</Button> </Button>
) )

View File

@@ -1,27 +1,18 @@
.specification { .specification {
align-items: center; align-items: center;
display: flex; display: flex;
flex-direction: row; justify-content: center;
gap: var(--Spacing-x1); gap: var(--Space-x1);
justify-content: space-between;
}
.toggleSidePeek {
margin-left: auto;
}
.specification .toggleSidePeek button {
padding: 0;
text-align: start;
} }
.roomDetails { .roomDetails {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--Spacing-x1); text-align: center;
padding-bottom: var(--Spacing-x-half); gap: var(--Space-x1);
padding-bottom: var(--Space-x05);
} }
.name { .sidePeekButton {
display: inline-block; width: 100%;
} }

View File

@@ -1,22 +1,17 @@
"use client" "use client"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useRatesStore } from "@/stores/select-rate" import { useRatesStore } from "@/stores/select-rate"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import RoomSize from "./RoomSize" import RoomSize from "./RoomSize"
import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./details.module.css" import styles from "./details.module.css"
export default function Details({ roomTypeCode }: { roomTypeCode: string }) { export default function Details({ roomTypeCode }: { roomTypeCode: string }) {
const intl = useIntl() const intl = useIntl()
const { hotelId, roomCategories } = useRatesStore((state) => ({ const roomCategories = useRatesStore((state) => state.roomCategories)
hotelId: state.booking.hotelId,
roomCategories: state.roomCategories,
}))
const selectedRoom = roomCategories.find((roomCategory) => const selectedRoom = roomCategories.find((roomCategory) =>
roomCategory.roomTypes.find((roomType) => roomType.code === roomTypeCode) roomCategory.roomTypes.find((roomType) => roomType.code === roomTypeCode)
@@ -28,40 +23,34 @@ export default function Details({ roomTypeCode }: { roomTypeCode: string }) {
<> <>
<div className={styles.specification}> <div className={styles.specification}>
{occupancy && ( {occupancy && (
<Caption color="uiTextMediumContrast"> <Typography variant="Body/Supporting text (caption)/smBold">
{occupancy.max === occupancy.min <h4>
? intl.formatMessage( {occupancy.max === occupancy.min
{ ? intl.formatMessage(
defaultMessage: {
"{guests, plural, one {# guest} other {# guests}}", defaultMessage:
}, "{guests, plural, one {# guest} other {# guests}}",
{ guests: occupancy.max } },
) { guests: occupancy.max }
: intl.formatMessage( )
{ : intl.formatMessage(
defaultMessage: "{min}-{max} guests", {
}, defaultMessage: "{min}-{max} guests",
{ },
min: occupancy.min, {
max: occupancy.max, min: occupancy.min,
} max: occupancy.max,
)} }
</Caption> )}
</h4>
</Typography>
)} )}
<RoomSize roomSize={roomSize} /> <RoomSize roomSize={roomSize} />
<div className={styles.toggleSidePeek}>
{roomTypeCode && (
<ToggleSidePeek hotelId={hotelId} roomTypeCode={roomTypeCode} />
)}
</div>
</div> </div>
<div className={styles.roomDetails}> <div className={styles.roomDetails}>
<Subtitle className={styles.name} type="two"> <Typography variant="Title/Subtitle/lg">
{name} <h2>{name}</h2>
</Subtitle> </Typography>
{/* Out of scope for now
<Body>{descriptions?.short}</Body>
*/}
</div> </div>
</> </>
) )

View File

@@ -0,0 +1,7 @@
.message {
display: flex;
align-items: center;
text-align: center;
gap: var(--Space-x1);
white-space: nowrap;
}

View File

@@ -2,14 +2,18 @@
import { useSession } from "next-auth/react" import { useSession } from "next-auth/react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useRatesStore } from "@/stores/select-rate" import { useRatesStore } from "@/stores/select-rate"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Divider from "@/components/TempDesignSystem/Divider"
import { useRoomContext } from "@/contexts/SelectRate/Room" import { useRoomContext } from "@/contexts/SelectRate/Room"
import { isValidClientSession } from "@/utils/clientSession" import { isValidClientSession } from "@/utils/clientSession"
import { getBreakfastMessage } from "./getBreakfastMessage" import { getBreakfastMessage } from "./getBreakfastMessage"
import styles from "./breakfastMessage.module.css"
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter" import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
export default function BreakfastMessage({ export default function BreakfastMessage({
@@ -61,8 +65,12 @@ export default function BreakfastMessage({
} }
return ( return (
<span> <div className={styles.message}>
<Caption color="uiTextHighContrast">{breakfastMessage}</Caption> <Divider color="borderDividerSubtle" />
</span> <Typography variant={"Body/Supporting text (caption)/smRegular"}>
<p>{breakfastMessage}</p>
</Typography>
<Divider color="borderDividerSubtle" />
</div>
) )
} }

View File

@@ -30,3 +30,19 @@ div[data-multiroom="true"] .imageContainer {
max-width: 100%; max-width: 100%;
object-fit: cover; object-fit: cover;
} }
.toggleSidePeek {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
bottom: 0;
color: var(--Component-Button-Brand-Secondary-On-fill-Inverted);
background-color: var(--Surface-Brand-Primary-1-OnSurface-Default);
height: 40px;
width: 100%;
}
.inventory {
color: var(--Text-Interactive-Default);
}

View File

@@ -1,14 +1,17 @@
"use client" "use client"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useRatesStore } from "@/stores/select-rate" import { useRatesStore } from "@/stores/select-rate"
import { IconForFeatureCode } from "@/components/HotelReservation/utils" import { IconForFeatureCode } from "@/components/HotelReservation/utils"
import ImageGallery from "@/components/ImageGallery" import ImageGallery from "@/components/ImageGallery"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import { useRoomContext } from "@/contexts/SelectRate/Room" import { useRoomContext } from "@/contexts/SelectRate/Room"
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery" import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
import ToggleSidePeek from "../Details/ToggleSidePeek"
import styles from "./image.module.css" import styles from "./image.module.css"
import type { RoomListItemImageProps } from "@/types/components/hotelReservation/selectRate/roomListItem" import type { RoomListItemImageProps } from "@/types/components/hotelReservation/selectRate/roomListItem"
@@ -21,7 +24,10 @@ export default function RoomImage({
}: RoomListItemImageProps) { }: RoomListItemImageProps) {
const intl = useIntl() const intl = useIntl()
const { selectedPackages } = useRoomContext() const { selectedPackages } = useRoomContext()
const roomCategories = useRatesStore((state) => state.roomCategories) const { roomCategories, hotelId } = useRatesStore((state) => ({
roomCategories: state.roomCategories,
hotelId: state.booking.hotelId,
}))
const showLowInventory = roomsLeft > 0 && roomsLeft < 5 const showLowInventory = roomsLeft > 0 && roomsLeft < 5
@@ -36,14 +42,16 @@ export default function RoomImage({
<div className={styles.chipContainer}> <div className={styles.chipContainer}>
{showLowInventory ? ( {showLowInventory ? (
<span className={styles.chip}> <span className={styles.chip}>
<Footnote color="burgundy" textTransform="uppercase"> <Typography variant="Tag/sm">
{intl.formatMessage( <p className={styles.inventory}>
{ {intl.formatMessage(
defaultMessage: "{amount, number} left", {
}, defaultMessage: "{amount, number} left",
{ amount: roomsLeft } },
)} { amount: roomsLeft }
</Footnote> )}
</p>
</Typography>
</span> </span>
) : null} ) : null}
{roomPackages {roomPackages
@@ -56,7 +64,17 @@ export default function RoomImage({
</span> </span>
))} ))}
</div> </div>
<ImageGallery images={galleryImages} title={roomType} fill /> <ImageGallery
images={galleryImages}
title={roomType}
fill
imageCountPosition="top"
/>
<div className={styles.toggleSidePeek}>
{roomTypeCode && (
<ToggleSidePeek hotelId={hotelId} roomTypeCode={roomTypeCode} />
)}
</div>
</div> </div>
) )
} }

View File

@@ -19,6 +19,16 @@
color: var(--Text-Inverted); color: var(--Text-Inverted);
} }
.imageCountBottom {
bottom: var(--Space-x2);
top: auto;
}
.imageCountTop {
top: var(--Space-x2);
bottom: auto;
}
.triggerArea { .triggerArea {
background-color: transparent; background-color: transparent;
border-width: 0; border-width: 0;

View File

@@ -21,6 +21,7 @@ function ImageGallery({
height = 280, height = 280,
sizes, sizes,
hideLabel, hideLabel,
imageCountPosition = "bottom",
}: ImageGalleryProps) { }: ImageGalleryProps) {
const intl = useIntl() const intl = useIntl()
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
@@ -44,7 +45,13 @@ function ImageGallery({
{...imageProps} {...imageProps}
/> />
<Typography variant={"Body/Supporting text (caption)/smRegular"}> <Typography variant={"Body/Supporting text (caption)/smRegular"}>
<span className={styles.imageCount}> <span
className={`${styles.imageCount} ${
imageCountPosition === "top"
? styles.imageCountTop
: styles.imageCountBottom
}`}
>
<MaterialIcon icon="filter" color="Icon/Inverted" size={16} /> <MaterialIcon icon="filter" color="Icon/Inverted" size={16} />
<span>{images.length}</span> <span>{images.length}</span>
</span> </span>

View File

@@ -13,4 +13,5 @@ export type ImageGalleryProps = {
height?: number height?: number
sizes?: string sizes?: string
hideLabel?: boolean hideLabel?: boolean
imageCountPosition?: "top" | "bottom"
} }