feat(SW-2944): Filtering roomTypes with CustomOccuppancy type to only show the biggest

Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-06-03 12:52:48 +00:00
parent 984805ea8d
commit b796e91379
5 changed files with 149 additions and 77 deletions

View File

@@ -0,0 +1,62 @@
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { getBedIconName } from "@/components/SidePeeks/RoomSidePeek/bedIcon"
import { getIntl } from "@/i18n"
import { getBedDescriptionText } from "../utils"
import getFilteredRoomTypes from "./utils"
import styles from "./roomTypes.module.css"
import type { Room } from "@/types/hotel"
interface RoomFacilitiesProps {
roomTypes: Room["roomTypes"]
}
export default async function RoomTypes({ roomTypes }: RoomFacilitiesProps) {
const intl = await getIntl()
const filteredRoomTypes = getFilteredRoomTypes(roomTypes)
return (
<>
<div className={styles.intro}>
<Typography variant="Title/Subtitle/md">
<h3>
{intl.formatMessage({
defaultMessage: "Bed options",
})}
</h3>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{intl.formatMessage({
defaultMessage: "Based on availability",
})}
</p>
</Typography>
</div>
<ul className={styles.list}>
{filteredRoomTypes.map((roomType) => {
const iconName = getBedIconName(roomType.mainBed.type)
const descriptionText = getBedDescriptionText(intl, roomType.mainBed)
return (
<li key={roomType.code} className={styles.listItem}>
<MaterialIcon
color="CurrentColor"
icon={iconName}
className={styles.icon}
/>
<Typography variant="Body/Paragraph/mdRegular">
<span>{descriptionText}</span>
</Typography>
</li>
)
})}
</ul>
</>
)
}

View File

@@ -0,0 +1,20 @@
.intro {
display: grid;
gap: var(--Space-x05);
}
.list {
list-style: none;
display: grid;
gap: var(--Space-x05);
}
.listItem {
display: flex;
gap: var(--Space-x1);
color: var(--Text-Secondary);
}
.icon {
flex-shrink: 0;
}

View File

@@ -0,0 +1,49 @@
import type { Room } from "@/types/hotel"
type RoomType = Room["roomTypes"][number]
// This util function filters room types to show only unique bed configurations
export default function getFilteredRoomTypes(roomTypes: RoomType[]) {
const customOccupancyRoomTypes = roomTypes.filter(
(roomType) => roomType.mainBed.type === "CustomOccupancy"
)
const otherRoomTypes = roomTypes.filter(
(roomType) => roomType.mainBed.type !== "CustomOccupancy"
)
const filteredOtherRoomTypes = filterDuplicatesByDescription(otherRoomTypes)
// If there are no CustomOccupancy room types, return just the filtered other types
if (customOccupancyRoomTypes.length === 0) {
return filteredOtherRoomTypes
}
// If there is only one CustomOccupancy room type, no need to find the best one
if (customOccupancyRoomTypes.length === 1) {
return [...filteredOtherRoomTypes, customOccupancyRoomTypes[0]]
}
// Find the CustomOccupancy room type with highest occupancy
const bestCustomOccupancyRoomType = customOccupancyRoomTypes.reduce<RoomType>(
(best, current) => {
return current.occupancy.total > best.occupancy.total ? current : best
},
customOccupancyRoomTypes[0]
)
return [...filteredOtherRoomTypes, bestCustomOccupancyRoomType]
}
// Helper function to just filter duplicates by description
function filterDuplicatesByDescription(roomTypes: RoomType[]): RoomType[] {
const uniqueRoomsByDescription: Record<string, RoomType> = {}
roomTypes.forEach((roomType) => {
const description = roomType.mainBed.description
if (!uniqueRoomsByDescription[description]) {
uniqueRoomsByDescription[description] = roomType
}
})
return Object.values(uniqueRoomsByDescription)
}

View File

@@ -1,13 +1,11 @@
import Link from "next/link"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { selectRateWithParams } from "@/constants/routes/hotelReservation"
import { dt } from "@/lib/dt"
import ImageGallery from "@/components/ImageGallery"
import { getBedIconName } from "@/components/SidePeeks/RoomSidePeek/bedIcon"
import Button from "@/components/TempDesignSystem/Button"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { getIntl } from "@/i18n"
@@ -16,7 +14,7 @@ import { mapApiImagesToGalleryImages } from "@/utils/imageGallery"
import { getRoomNameAsParam } from "../../utils"
import RoomFacilities from "./RoomFacilities"
import { getBedDescriptionText } from "./utils"
import RoomTypes from "./RoomTypes"
import styles from "./room.module.css"
@@ -28,7 +26,15 @@ export default async function RoomSidePeek({
}: RoomSidePeekProps) {
const lang = await getLang()
const intl = await getIntl()
const { roomSize, totalOccupancy, descriptions, images } = room
const {
roomSize,
roomTypes,
roomFacilities,
name,
totalOccupancy,
descriptions,
images,
} = room
const roomDescription = descriptions.medium
const galleryImages = mapApiImagesToGalleryImages(images)
@@ -36,18 +42,13 @@ export default async function RoomSidePeek({
const fromdate = dt().format("YYYY-MM-DD")
const todate = dt().add(1, "day").format("YYYY-MM-DD")
const selectRateURL = selectRateWithParams(lang, hotelId, fromdate, todate)
return (
<SidePeek
contentKey={`room-${getRoomNameAsParam(room.name)}`}
title={room.name}
>
<SidePeek contentKey={`room-${getRoomNameAsParam(name)}`} title={name}>
<div className={styles.content}>
<div className={styles.innerContent}>
<Typography
variant="Body/Paragraph/mdRegular"
className={styles.guests}
>
<p>
<Typography variant="Body/Paragraph/mdRegular">
<p className={styles.guests}>
{intl.formatMessage(
{
defaultMessage:
@@ -83,7 +84,7 @@ export default async function RoomSidePeek({
<div className={styles.imageContainer}>
<ImageGallery
images={galleryImages}
title={room.name}
title={name}
height={280}
hideLabel={true}
/>
@@ -94,48 +95,11 @@ export default async function RoomSidePeek({
</div>
<div className={styles.innerContent}>
<RoomFacilities roomFacilities={room.roomFacilities} />
<RoomFacilities roomFacilities={roomFacilities} />
</div>
<div className={styles.innerContent}>
<div className={styles.bedOptions}>
<Typography variant="Title/Subtitle/md">
<h3>
{intl.formatMessage({
defaultMessage: "Bed options",
})}
</h3>
</Typography>
<Typography variant="Body/Paragraph/mdRegular">
<p>
{intl.formatMessage({
defaultMessage: "Based on availability",
})}
</p>
</Typography>
</div>
<ul className={styles.bedOptions}>
{room.roomTypes.map((roomType) => {
const iconName = getBedIconName(roomType.mainBed.type)
const descriptionText = getBedDescriptionText(
intl,
roomType.mainBed
)
return (
<li key={roomType.code} className={styles.listItem}>
<MaterialIcon
color="CurrentColor"
icon={iconName}
className={styles.icon}
/>
<Typography variant="Body/Paragraph/mdRegular">
<span>{descriptionText}</span>
</Typography>
</li>
)
})}
</ul>
<RoomTypes roomTypes={roomTypes} />
</div>
</div>
<div className={styles.buttonContainer}>

View File

@@ -12,39 +12,16 @@
gap: var(--Spacing-x-one-and-half);
}
.innerContent .guests {
.guests {
color: var(--Text-Accent-Secondary);
}
.bedOptions {
gap: var(--Spacing-x-half);
}
.imageContainer {
position: relative;
border-radius: var(--Corner-radius-md);
overflow: hidden;
}
.bedOptions {
display: flex;
flex-direction: column;
}
.listItem {
display: grid;
grid-auto-flow: column;
gap: var(--Spacing-x1);
margin-bottom: var(--Spacing-x-half);
align-items: self-start;
justify-content: flex-start;
color: var(--Text-Secondary);
}
.icon {
flex-shrink: 0;
}
.buttonContainer {
background-color: var(--Base-Background-Primary-Normal);
border-top: 1px solid var(--Base-Border-Subtle);