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"
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"
@@ -15,34 +15,42 @@ export default function RoomSize({ roomSize }: RoomSizeProps) {
if (roomSize.min === roomSize.max) {
return (
<>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<Caption color="uiTextMediumContrast"></Caption>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{
defaultMessage: "{roomSize} m²",
},
{ roomSize: roomSize.min }
)}
</Caption>
<Typography variant="Body/Supporting text (caption)/smBold">
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<p></p>
</Typography>
<Typography variant="Body/Supporting text (caption)/smBold">
<h4>
{intl.formatMessage(
{
defaultMessage: "{roomSize} m²",
},
{ roomSize: roomSize.min }
)}
</h4>
</Typography>
</>
)
}
return (
<>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<Caption color="uiTextMediumContrast"></Caption>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{
defaultMessage: "{roomSizeMin} - {roomSizeMax} m²",
},
{
roomSizeMin: roomSize.min,
roomSizeMax: roomSize.max,
}
)}
</Caption>
<Typography variant="Body/Supporting text (caption)/smBold">
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<p></p>
</Typography>
<Typography variant="Body/Supporting text (caption)/smBold">
<h4>
{intl.formatMessage(
{
defaultMessage: "{roomSizeMin} - {roomSizeMax} m²",
},
{
roomSizeMin: roomSize.min,
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 styles from "./details.module.css"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import type { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
@@ -30,8 +32,10 @@ export default function ToggleSidePeek({
variant="Text"
wrapping
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" />
</Button>
)

View File

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

View File

@@ -1,22 +1,17 @@
"use client"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
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 ToggleSidePeek from "./ToggleSidePeek"
import styles from "./details.module.css"
export default function Details({ roomTypeCode }: { roomTypeCode: string }) {
const intl = useIntl()
const { hotelId, roomCategories } = useRatesStore((state) => ({
hotelId: state.booking.hotelId,
roomCategories: state.roomCategories,
}))
const roomCategories = useRatesStore((state) => state.roomCategories)
const selectedRoom = roomCategories.find((roomCategory) =>
roomCategory.roomTypes.find((roomType) => roomType.code === roomTypeCode)
@@ -28,40 +23,34 @@ export default function Details({ roomTypeCode }: { roomTypeCode: string }) {
<>
<div className={styles.specification}>
{occupancy && (
<Caption color="uiTextMediumContrast">
{occupancy.max === occupancy.min
? intl.formatMessage(
{
defaultMessage:
"{guests, plural, one {# guest} other {# guests}}",
},
{ guests: occupancy.max }
)
: intl.formatMessage(
{
defaultMessage: "{min}-{max} guests",
},
{
min: occupancy.min,
max: occupancy.max,
}
)}
</Caption>
<Typography variant="Body/Supporting text (caption)/smBold">
<h4>
{occupancy.max === occupancy.min
? intl.formatMessage(
{
defaultMessage:
"{guests, plural, one {# guest} other {# guests}}",
},
{ guests: occupancy.max }
)
: intl.formatMessage(
{
defaultMessage: "{min}-{max} guests",
},
{
min: occupancy.min,
max: occupancy.max,
}
)}
</h4>
</Typography>
)}
<RoomSize roomSize={roomSize} />
<div className={styles.toggleSidePeek}>
{roomTypeCode && (
<ToggleSidePeek hotelId={hotelId} roomTypeCode={roomTypeCode} />
)}
</div>
</div>
<div className={styles.roomDetails}>
<Subtitle className={styles.name} type="two">
{name}
</Subtitle>
{/* Out of scope for now
<Body>{descriptions?.short}</Body>
*/}
<Typography variant="Title/Subtitle/lg">
<h2>{name}</h2>
</Typography>
</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 { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
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 { isValidClientSession } from "@/utils/clientSession"
import { getBreakfastMessage } from "./getBreakfastMessage"
import styles from "./breakfastMessage.module.css"
import { BookingCodeFilterEnum } from "@/types/enums/bookingCodeFilter"
export default function BreakfastMessage({
@@ -61,8 +65,12 @@ export default function BreakfastMessage({
}
return (
<span>
<Caption color="uiTextHighContrast">{breakfastMessage}</Caption>
</span>
<div className={styles.message}>
<Divider color="borderDividerSubtle" />
<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%;
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"
import { useIntl } from "react-intl"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { useRatesStore } from "@/stores/select-rate"
import { IconForFeatureCode } from "@/components/HotelReservation/utils"
import ImageGallery from "@/components/ImageGallery"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import { useRoomContext } from "@/contexts/SelectRate/Room"
import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
import ToggleSidePeek from "../Details/ToggleSidePeek"
import styles from "./image.module.css"
import type { RoomListItemImageProps } from "@/types/components/hotelReservation/selectRate/roomListItem"
@@ -21,7 +24,10 @@ export default function RoomImage({
}: RoomListItemImageProps) {
const intl = useIntl()
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
@@ -36,14 +42,16 @@ export default function RoomImage({
<div className={styles.chipContainer}>
{showLowInventory ? (
<span className={styles.chip}>
<Footnote color="burgundy" textTransform="uppercase">
{intl.formatMessage(
{
defaultMessage: "{amount, number} left",
},
{ amount: roomsLeft }
)}
</Footnote>
<Typography variant="Tag/sm">
<p className={styles.inventory}>
{intl.formatMessage(
{
defaultMessage: "{amount, number} left",
},
{ amount: roomsLeft }
)}
</p>
</Typography>
</span>
) : null}
{roomPackages
@@ -56,7 +64,17 @@ export default function RoomImage({
</span>
))}
</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>
)
}

View File

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

View File

@@ -21,6 +21,7 @@ function ImageGallery({
height = 280,
sizes,
hideLabel,
imageCountPosition = "bottom",
}: ImageGalleryProps) {
const intl = useIntl()
const [isOpen, setIsOpen] = useState(false)
@@ -44,7 +45,13 @@ function ImageGallery({
{...imageProps}
/>
<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} />
<span>{images.length}</span>
</span>

View File

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