feat(SW-713): Added sidepeek functionality for rooms

This commit is contained in:
Erik Tiekstra
2024-11-25 13:37:14 +01:00
parent 9612cf117c
commit 27f48bcc7e
9 changed files with 205 additions and 35 deletions

View File

@@ -1,24 +1,23 @@
"use client"
import Link from "next/link"
import { useIntl } from "react-intl"
import useSidePeekStore from "@/stores/sidepeek"
import { ChevronRightSmallIcon } from "@/components/Icons"
import ImageGallery from "@/components/ImageGallery"
import Button from "@/components/TempDesignSystem/Button"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getRoomNameAsParam } from "../../utils"
import styles from "./roomCard.module.css"
import type { RoomCardProps } from "@/types/components/hotelPage/room"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
export function RoomCard({ hotelId, room }: RoomCardProps) {
export function RoomCard({ room }: RoomCardProps) {
const { images, name, roomSize, occupancy } = room
const intl = useIntl()
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
const size =
roomSize?.min === roomSize?.max
@@ -51,21 +50,11 @@ export function RoomCard({ hotelId, room }: RoomCardProps) {
)}
</Body>
</div>
<Button
intent="text"
type="button"
size="medium"
theme="base"
onClick={() =>
openSidePeek({
key: SidePeekEnum.roomDetails,
hotelId,
roomTypeCode: room.roomTypes[0].code,
})
}
>
{intl.formatMessage({ id: "See room details" })}
<ChevronRightSmallIcon color="burgundy" width={20} height={20} />
<Button intent="text" type="button" size="medium" theme="base" asChild>
<Link scroll={false} href={`?s=${getRoomNameAsParam(name)}`}>
{intl.formatMessage({ id: "See room details" })}
<ChevronRightSmallIcon color="burgundy" width={20} height={20} />
</Link>
</Button>
</div>
</article>

View File

@@ -15,7 +15,7 @@ import styles from "./rooms.module.css"
import type { RoomsProps } from "@/types/components/hotelPage/room"
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
export function Rooms({ hotelId, rooms }: RoomsProps) {
export function Rooms({ rooms }: RoomsProps) {
const intl = useIntl()
const showToggleButton = rooms.length > 3
const [allRoomsVisible, setAllRoomsVisible] = useState(!showToggleButton)
@@ -45,7 +45,7 @@ export function Rooms({ hotelId, rooms }: RoomsProps) {
>
{rooms.map((room) => (
<div key={room.id}>
<RoomCard hotelId={hotelId} room={room} />
<RoomCard room={room} />
</div>
))}
</Grids.Stackable>

View File

@@ -0,0 +1,118 @@
import Link from "next/link"
import ImageGallery from "@/components/ImageGallery"
import { getBedIcon } from "@/components/SidePeeks/RoomSidePeek/bedIcon"
import { getFacilityIcon } from "@/components/SidePeeks/RoomSidePeek/facilityIcon"
import Button from "@/components/TempDesignSystem/Button"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n"
import { getRoomNameAsParam } from "../../utils"
import styles from "./room.module.css"
import type { RoomSidePeekProps } from "@/types/components/hotelPage/sidepeek/room"
export default async function RoomSidePeek({ room }: RoomSidePeekProps) {
const intl = await getIntl()
const { roomSize, occupancy, descriptions, images } = room
const roomDescription = descriptions.medium
const totalOccupancy = occupancy.total
// TODO: Not defined where this should lead.
const ctaUrl = ""
return (
<SidePeek contentKey={getRoomNameAsParam(room.name)} title={room.name}>
<div className={styles.content}>
<div className={styles.innerContent}>
<Body color="baseTextMediumContrast">
{roomSize.min === roomSize.max
? roomSize.min
: `${roomSize.min} - ${roomSize.max}`}
m².{" "}
{intl.formatMessage(
{ id: "booking.accommodatesUpTo" },
{ nrOfGuests: totalOccupancy }
)}
</Body>
<div className={styles.imageContainer}>
<ImageGallery images={images} title={room.name} height={280} />
</div>
<Body color="uiTextHighContrast">{roomDescription}</Body>
</div>
<div className={styles.innerContent}>
<Subtitle type="two" color="uiTextHighContrast" asChild>
<h3>
{intl.formatMessage({ id: "booking.thisRoomIsEquippedWith" })}
</h3>
</Subtitle>
<ul className={styles.facilityList}>
{room.roomFacilities
.sort((a, b) => a.sortOrder - b.sortOrder)
.map((facility) => {
const Icon = getFacilityIcon(facility.icon)
return (
<li className={styles.listItem} key={facility.name}>
{Icon && (
<Icon
width={24}
height={24}
color="uiTextMediumContrast"
/>
)}
<Body
asChild
className={!Icon ? styles.noIcon : undefined}
color="uiTextMediumContrast"
>
<span>{facility.name}</span>
</Body>
</li>
)
})}
</ul>
</div>
<div className={styles.innerContent}>
<Subtitle type="two" color="uiTextHighContrast" asChild>
<h3>{intl.formatMessage({ id: "booking.bedOptions" })}</h3>
</Subtitle>
<Body color="grey">
{intl.formatMessage({ id: "booking.basedOnAvailability" })}
</Body>
<ul className={styles.bedOptions}>
{room.roomTypes.map((roomType) => {
const BedIcon = getBedIcon(roomType.mainBed.type)
return (
<li className={styles.listItem} key={roomType.code}>
{BedIcon && (
<BedIcon
color="uiTextMediumContrast"
width={24}
height={24}
/>
)}
<Body color="uiTextMediumContrast" asChild>
<span>{roomType.mainBed.description}</span>
</Body>
</li>
)
})}
</ul>
</div>
</div>
{ctaUrl && (
<div className={styles.buttonContainer}>
<Button fullWidth theme="base" intent="primary" asChild>
<Link href={ctaUrl}>
{intl.formatMessage({ id: "booking.selectRoom" })}
</Link>
</Button>
</div>
)}
</SidePeek>
)
}

View File

@@ -0,0 +1,48 @@
.content {
display: grid;
gap: var(--Spacing-x2);
position: relative;
margin-bottom: calc(
var(--Spacing-x4) * 2 + 80px
); /* Creates space between the wrapper and buttonContainer */
}
.innerContent {
display: grid;
gap: var(--Spacing-x-one-and-half);
}
.imageContainer {
position: relative;
border-radius: var(--Corner-radius-Medium);
overflow: hidden;
}
.facilityList {
column-count: 2;
column-gap: var(--Spacing-x2);
}
.bedOptions {
display: flex;
flex-direction: column;
}
.listItem {
display: flex;
gap: var(--Spacing-x1);
margin-bottom: var(--Spacing-x-half);
}
.noIcon {
margin-left: var(--Spacing-x4);
}
.buttonContainer {
background-color: var(--Base-Background-Primary-Normal);
border-top: 1px solid var(--Base-Border-Subtle);
padding: var(--Spacing-x4) var(--Spacing-x2);
width: 100%;
position: absolute;
left: 0;
bottom: 0;
}

View File

@@ -1,2 +1,3 @@
export { default as AboutTheHotelSidePeek } from "./AboutTheHotel"
export { default as RoomSidePeek } from "./Room"
export { default as WellnessAndExerciseSidePeek } from "./WellnessAndExercise"

View File

@@ -1,11 +1,15 @@
import { notFound } from "next/navigation"
import hotelPageParams from "@/constants/routes/hotelPageParams"
import {
activities,
amenities,
meetingsAndConferences,
restaurantAndBar,
} from "@/constants/routes/hotelPageParams"
import { env } from "@/env/server"
import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests"
import AccordionSection from "@/components/Blocks/Accordion"
import HotelReservationSidePeek from "@/components/HotelReservation/SidePeek"
import SidePeekProvider from "@/components/SidePeeks/SidePeekProvider"
import Alert from "@/components/TempDesignSystem/Alert"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
@@ -24,7 +28,11 @@ import Facilities from "./Facilities"
import IntroSection from "./IntroSection"
import PreviewImages from "./PreviewImages"
import { Rooms } from "./Rooms"
import { AboutTheHotelSidePeek, WellnessAndExerciseSidePeek } from "./SidePeeks"
import {
AboutTheHotelSidePeek,
RoomSidePeek,
WellnessAndExerciseSidePeek,
} from "./SidePeeks"
import TabNavigation from "./TabNavigation"
import styles from "./hotelPage.module.css"
@@ -144,7 +152,7 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
</div>
) : null}
</div>
<Rooms hotelId={hotelId} rooms={roomCategories} />
<Rooms rooms={roomCategories} />
<Facilities facilities={facilities} activitiesCard={activitiesCard} />
{faq.accordions.length > 0 && (
<AccordionSection accordion={faq.accordions} title={faq.title} />
@@ -169,9 +177,8 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
</>
) : null}
<SidePeekProvider>
{/* eslint-disable import/no-named-as-default-member */}
<SidePeek
contentKey={hotelPageParams.amenities[lang]}
contentKey={amenities[lang]}
title={intl.formatMessage({ id: "Amenities" })}
>
{/* TODO: Render amenities as per the design. */}
@@ -186,7 +193,7 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
descriptions={hotelContent.texts}
/>
<SidePeek
contentKey={hotelPageParams.restaurantAndBar[lang]}
contentKey={restaurantAndBar[lang]}
title={intl.formatMessage({ id: "Restaurant & Bar" })}
>
{/* TODO */}
@@ -197,22 +204,23 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
buttonUrl="#"
/>
<SidePeek
contentKey={hotelPageParams.activities[lang]}
contentKey={activities[lang]}
title={intl.formatMessage({ id: "Activities" })}
>
{/* TODO */}
Activities
</SidePeek>
<SidePeek
contentKey={hotelPageParams.meetingsAndConferences[lang]}
contentKey={meetingsAndConferences[lang]}
title={intl.formatMessage({ id: "Meetings & Conferences" })}
>
{/* TODO */}
Meetings & Conferences
</SidePeek>
{/* eslint-enable import/no-named-as-default-member */}
{roomCategories.map((room) => (
<RoomSidePeek key={room.name} room={room} />
))}
</SidePeekProvider>
<HotelReservationSidePeek hotel={null} />
</div>
)
}

View File

@@ -0,0 +1,3 @@
export function getRoomNameAsParam(roomName: string) {
return roomName.replace(/[()]/g, "").replaceAll(" ", "-").toLowerCase()
}

View File

@@ -1,11 +1,9 @@
import type { RoomData } from "@/types/hotel"
export interface RoomCardProps {
hotelId: string
room: RoomData
}
export type RoomsProps = {
hotelId: string
rooms: RoomData[]
}

View File

@@ -0,0 +1,5 @@
import type { RoomData } from "@/types/hotel"
export interface RoomSidePeekProps {
room: RoomData
}