Merged in feat/SW-1065-meetings-page (pull request #1287)

Feat(SW-1065): Meetings hotel subpage

Approved-by: Erik Tiekstra
This commit is contained in:
Matilda Landström
2025-02-12 15:13:17 +00:00
parent cac090df34
commit c0e4553d9f
31 changed files with 669 additions and 15 deletions

View File

@@ -0,0 +1,147 @@
"use client"
import { useState } from "react"
import { useIntl } from "react-intl"
import { MeasureIcon, PersonIcon } from "@/components/Icons"
import Image from "@/components/Image"
import Divider from "../Divider"
import ShowMoreButton from "../ShowMoreButton"
import Caption from "../Text/Caption"
import Subtitle from "../Text/Subtitle"
import { translateRoomLighting, translateSeatingType } from "./utils"
import styles from "./meetingRoomCard.module.css"
import type { MeetingRoom } from "@/types/components/hotelPage/meetingRooms"
interface MeetingRoomCardProps {
room: MeetingRoom
}
export default function MeetingRoomCard({ room }: MeetingRoomCardProps) {
const intl = useIntl()
const [opened, setOpened] = useState(false)
const roomSeatings = room.seatings.map((seating) => {
return seating.capacity
})
const maxSeatings = Math.max(...roomSeatings)
const image = room.content.images[0]
function handleShowMore() {
setOpened((state) => !state)
}
return (
<article className={styles.card}>
<Image
src={image.imageSizes.small}
alt={image.metaData.altText || image.metaData.altText_En || ""}
className={styles.image}
width={200}
height={200}
/>
<div className={styles.content}>
<Subtitle textAlign="left" type="two" color="black">
{room.name}
</Subtitle>
<div className={styles.capacity}>
<div className={styles.iconText}>
<MeasureIcon color="uiTextPlaceholder" />
<Caption color="uiTextPlaceholder">{room.size} m2</Caption>
</div>
<div className={styles.iconText}>
<PersonIcon color="uiTextPlaceholder" width={16} height={16} />
<Caption color="uiTextPlaceholder">
{intl.formatMessage(
{ id: "max {seatings} pers" },
{ seatings: maxSeatings }
)}
</Caption>
</div>
</div>
{opened && (
<div className={styles.openedInfo}>
<div className={styles.rowItem}>
{room.seatings.map((seating, idx) => (
<div
key={seating.type}
className={styles.capacity}
id={String(seating.capacity) + seating.type + idx}
>
<Caption color="uiTextMediumContrast">
{translateSeatingType(seating.type, intl)}
</Caption>
<Caption color="uiTextHighContrast">
{intl.formatMessage(
{ id: "{number} people" },
{ number: seating.capacity }
)}
</Caption>
</div>
))}
</div>
<Divider color="baseSurfaceSubtleNormal" />
<div className={styles.rowItem}>
<div className={styles.capacity}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({
id: "Location in hotel",
})}
</Caption>
<Caption color="uiTextHighContrast">
{intl.formatMessage(
{ id: "Floor {floorNumber}" },
{
floorNumber: `${room.floorNumber}`,
}
)}
</Caption>
</div>
<div className={styles.capacity}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({
id: "Lighting",
})}
</Caption>
<Caption color="uiTextHighContrast">
{translateRoomLighting(room.lighting, intl)}
</Caption>
</div>
{room.doorHeight && room.doorWidth ? (
<div className={styles.capacity}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({
id: "Access size",
})}
</Caption>
<Caption color="uiTextHighContrast">
{room.doorHeight} x {room.doorWidth} m
</Caption>
</div>
) : null}
{room.length && room.width && room.height ? (
<div className={styles.capacity}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({
id: "Dimensions",
})}
</Caption>
<Caption color="uiTextHighContrast">
{room.length} x {room.width} x {room.height} m
</Caption>
</div>
) : null}
</div>
</div>
)}
<ShowMoreButton
intent="secondary"
loadMoreData={handleShowMore}
showLess={opened}
/>
</div>
</article>
)
}

View File

@@ -0,0 +1,56 @@
.card {
background-color: var(--Base-Surface-Primary-light-Normal);
border-radius: var(--Corner-radius-Medium);
border: 1px solid var(--Base-Border-Subtle);
display: flex;
flex-direction: column;
overflow: hidden;
}
.capacity {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--Spacing-x1);
}
.iconText {
display: flex;
gap: var(--Spacing-x-half);
align-items: center;
}
.rowItem {
display: grid;
gap: var(--Spacing-x-half);
}
.openedInfo {
background-color: var(--Base-Surface-Secondary-light-Normal);
border-radius: var(--Corner-radius-Medium);
padding: var(--Spacing-x2);
display: grid;
gap: var(--Spacing-x2);
}
.image {
width: 100%;
object-fit: cover;
}
.content {
display: grid;
gap: var(--Spacing-x2);
padding: var(--Spacing-x2);
grid-template-rows: auto 1fr auto;
flex-grow: 1;
}
@media (min-width: 1367px) {
.card:not(.alwaysStack) .ctaContainer {
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
}
.card:not(.alwaysStack) .ctaContainer:has(:only-child) {
grid-template-columns: 1fr;
}
}

View File

@@ -0,0 +1,79 @@
import type { IntlShape } from "react-intl/src/types"
import { RoomLighting, SeatingType } from "@/types/enums/meetingRooms"
export function translateRoomLighting(option: string, intl: IntlShape) {
switch (option) {
case RoomLighting.WindowsNaturalDaylight:
return intl.formatMessage({
id: "Windows with natural daylight",
})
case RoomLighting.IndoorWindowsExcellentLighting:
return intl.formatMessage({
id: "Indoor windows and excellent lighting",
})
case RoomLighting.IndoorWindowsFacingHotel:
return intl.formatMessage({
id: "Indoor windows facing the hotel",
})
case RoomLighting.NoWindows:
return intl.formatMessage({
id: "No windows",
})
case RoomLighting.NoWindowsExcellentLighting:
return intl.formatMessage({
id: "No windows but excellent lighting",
})
case RoomLighting.WindowsNaturalDaylightBlackoutFacilities:
return intl.formatMessage({
id: "Windows natural daylight and blackout facilities",
})
case RoomLighting.WindowsNaturalDaylightExcellentView:
return intl.formatMessage({
id: "Windows natural daylight and excellent view",
})
default:
console.warn(`Unsupported conference room ligthing option: ${option}`)
return intl.formatMessage({ id: "N/A" })
}
}
export function translateSeatingType(type: string, intl: IntlShape) {
switch (type) {
case SeatingType.Boardroom:
return intl.formatMessage({
id: "Boardroom",
})
case SeatingType.Cabaret:
return intl.formatMessage({
id: "Cabaret seating",
})
case SeatingType.Classroom:
return intl.formatMessage({
id: "Classroom",
})
case SeatingType.FullCircle:
return intl.formatMessage({
id: "Full circle",
})
case SeatingType.HalfCircle:
return intl.formatMessage({
id: "Half circle",
})
case SeatingType.StandingTable:
return intl.formatMessage({
id: "Standing table",
})
case SeatingType.Theatre:
return intl.formatMessage({
id: "Theatre",
})
case SeatingType.UShape:
return intl.formatMessage({
id: "U-shape",
})
default:
console.warn(`Unsupported conference room type : ${type}`)
return intl.formatMessage({ id: "N/A" })
}
}

View File

@@ -13,6 +13,7 @@ import type { ShowMoreButtonProps } from "./showMoreButton"
export default function ShowMoreButton({
className,
buttonIntent,
intent,
disabled,
showLess,
@@ -23,7 +24,7 @@ export default function ShowMoreButton({
const intl = useIntl()
const classNames = showMoreButtonVariants({
className,
intent,
buttonIntent,
})
if (!textShowMore) {
@@ -47,7 +48,9 @@ export default function ShowMoreButton({
variant="icon"
type="button"
theme="base"
intent="text"
intent={intent ?? "text"}
fullWidth={intent ? true : false}
size={intent && "small"}
>
<ChevronDownIcon className={styles.icon} />
{showLess ? textShowLess : textShowMore}

View File

@@ -1,5 +1,6 @@
import type { VariantProps } from "class-variance-authority"
import type { ButtonPropsRAC } from "../Button/button"
import type { showMoreButtonVariants } from "./variants"
export interface ShowMoreButtonProps
@@ -10,4 +11,5 @@ export interface ShowMoreButtonProps
textShowMore?: string
textShowLess?: string
loadMoreData: () => void
intent?: ButtonPropsRAC["intent"]
}

View File

@@ -4,7 +4,7 @@ import styles from "./showMoreButton.module.css"
export const showMoreButtonVariants = cva(styles.container, {
variants: {
intent: {
buttonIntent: {
table: styles.table,
},
},