Merged in feat/SW-613-refactor-hotelreservation-sidepeek (pull request #805)

Feat/SW-613 refactor hotelreservation sidepeek

* feat(SW-613): move sidepeek paralell route to apply for all of hotelreservation

* feat(SW-613): refactor sidepeek logic to a unified approach for hotelreservation flow

* feat(SW-613): fix issue where room was not selected properly in sidepeek

* fix(SW-613): move back preload to layout

* fix(SW-613): move preload to dedicated file

* fix(SW-613): refactor sidepeek to work with hotel page

* feat(SW-613): added sidepeek button for room card


Approved-by: Simon.Emanuelsson
This commit is contained in:
Tobias Johansson
2024-11-06 12:09:27 +00:00
parent ea6b9da9e0
commit 0cbffc7904
33 changed files with 507 additions and 352 deletions

View File

@@ -0,0 +1,27 @@
import { getHotelData } from "@/lib/trpc/memoizedRequests"
import { HotelIncludeEnum } from "@/server/routers/hotels/input"
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import SidePeek from "@/components/HotelReservation/SidePeek"
import type { LangParams, PageArgs } from "@/types/params"
export default async function HotelSidePeek({
params,
searchParams,
}: PageArgs<LangParams, { hotel: string }>) {
const search = new URLSearchParams(searchParams)
const { hotel: hotelId } = getQueryParamsForEnterDetails(search)
if (!hotelId) {
return <SidePeek hotel={null} />
}
const hotel = await getHotelData({
hotelId: hotelId,
language: params.lang,
include: [HotelIncludeEnum.RoomCategories],
})
return <SidePeek hotel={hotel} />
}

View File

@@ -1,24 +0,0 @@
import { redirect } from "next/navigation"
import { getHotelData } from "@/lib/trpc/memoizedRequests"
import SidePeek from "@/components/HotelReservation/EnterDetails/SidePeek"
import type { LangParams, PageArgs } from "@/types/params"
export default async function HotelSidePeek({
params,
searchParams,
}: PageArgs<LangParams, { hotel: string }>) {
if (!searchParams.hotel) {
redirect(`/${params.lang}`)
}
const hotel = await getHotelData({
hotelId: searchParams.hotel,
language: params.lang,
})
if (!hotel?.data) {
redirect(`/${params.lang}`)
}
return <SidePeek hotel={hotel.data.attributes} />
}

View File

@@ -0,0 +1,6 @@
import { getCreditCardsSafely , getProfileSafely } from "@/lib/trpc/memoizedRequests"
export function preload() {
void getProfileSafely()
void getCreditCardsSafely()
}

View File

@@ -1,10 +1,12 @@
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
import {
getCreditCardsSafely,
getProfileSafely,
} from "@/lib/trpc/memoizedRequests"
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
import { setLang } from "@/i18n/serverContext"
import { preload } from "./page"
import { preload } from "./_preload"
import styles from "./layout.module.css"
@@ -16,11 +18,9 @@ export default async function StepLayout({
children,
hotelHeader,
params,
sidePeek,
}: React.PropsWithChildren<
LayoutArgs<LangParams & { step: StepEnum }> & {
hotelHeader: React.ReactNode
sidePeek: React.ReactNode
summary: React.ReactNode
}
>) {
@@ -34,7 +34,6 @@ export default async function StepLayout({
<main className={styles.layout}>
{hotelHeader}
<div className={styles.content}>
<SelectedRoom />
{children}
<aside className={styles.summaryContainer}>
<div className={styles.hider} />
@@ -42,7 +41,6 @@ export default async function StepLayout({
<div className={styles.shadow} />
</aside>
</div>
{sidePeek}
</main>
</EnterDetailsProvider>
)

View File

@@ -15,6 +15,7 @@ import Details from "@/components/HotelReservation/EnterDetails/Details"
import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager"
import Payment from "@/components/HotelReservation/EnterDetails/Payment"
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
import {
generateChildrenString,
getQueryParamsForEnterDetails,
@@ -25,11 +26,6 @@ import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { LangParams, PageArgs } from "@/types/params"
export function preload() {
void getProfileSafely()
void getCreditCardsSafely()
}
function isValidStep(step: string): step is StepEnum {
return Object.values(StepEnum).includes(step as StepEnum)
}
@@ -106,6 +102,9 @@ export default async function StepPage({
return (
<section>
<HistoryStateManager />
<SelectedRoom hotelId={hotelId} room={roomAvailability.selectedRoom} />
{/* TODO: How to handle no beds found? */}
{roomAvailability.bedTypes ? (
<SectionAccordion

View File

@@ -8,9 +8,17 @@ import { LangParams, LayoutArgs } from "@/types/params"
export default function HotelReservationLayout({
children,
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
sidePeek,
}: React.PropsWithChildren<LayoutArgs<LangParams>> & {
sidePeek: React.ReactNode
}) {
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <div className={styles.layout}>{children}</div>
return (
<div className={styles.layout}>
{children}
{sidePeek}
</div>
)
}

View File

@@ -4,15 +4,16 @@ import { useIntl } from "react-intl"
import { GalleryIcon } from "@/components/Icons"
import Image from "@/components/Image"
import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import RoomDetailsButton from "../RoomDetailsButton"
import styles from "./roomCard.module.css"
import type { RoomCardProps } from "@/types/components/hotelPage/room"
export function RoomCard({ room }: RoomCardProps) {
export function RoomCard({ hotelId, room }: RoomCardProps) {
const { images, name, roomSize, occupancy, id } = room
const intl = useIntl()
const mainImage = images[0]
@@ -70,7 +71,10 @@ export function RoomCard({ room }: RoomCardProps) {
</Subtitle>
<Body color="grey">{subtitle}</Body>
</div>
<RoomSidePeek room={room} buttonSize="medium" />
<RoomDetailsButton
hotelId={hotelId}
roomTypeCode={room.roomTypes[0].code}
/>
</div>
</article>
)

View File

@@ -0,0 +1,34 @@
"use client"
import { useIntl } from "react-intl"
import useSidePeekStore from "@/stores/sidepeek"
import { ChevronRightSmallIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
export default function RoomDetailsButton({
hotelId,
roomTypeCode,
}: ToggleSidePeekProps) {
const intl = useIntl()
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
return (
<Button
intent="text"
type="button"
size="medium"
theme="base"
onClick={() =>
openSidePeek({ key: SidePeekEnum.roomDetails, hotelId, roomTypeCode })
}
>
{intl.formatMessage({ id: "See room details" })}
<ChevronRightSmallIcon color="burgundy" width={20} height={20} />
</Button>
)
}

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({ rooms }: RoomsProps) {
export function Rooms({ hotelId, rooms }: RoomsProps) {
const intl = useIntl()
const showToggleButton = rooms.length > 3
const [allRoomsVisible, setAllRoomsVisible] = useState(!showToggleButton)
@@ -45,7 +45,7 @@ export function Rooms({ rooms }: RoomsProps) {
>
{rooms.map((room) => (
<div key={room.id}>
<RoomCard room={room} />
<RoomCard hotelId={hotelId} room={room} />
</div>
))}
</Grids.Stackable>

View File

@@ -3,6 +3,7 @@ import { env } from "@/env/server"
import { serverClient } from "@/lib/trpc/server"
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"
@@ -39,6 +40,7 @@ export default async function HotelPage() {
}
const {
hotelId,
hotelName,
hotelDescription,
hotelLocation,
@@ -99,7 +101,7 @@ export default async function HotelPage() {
</div>
) : null}
</div>
<Rooms rooms={roomCategories} />
<Rooms hotelId={hotelId} rooms={roomCategories} />
<Facilities facilities={facilities} activitiesCard={activitiesCard} />
{faq.accordions.length > 0 && (
<AccordionSection accordion={faq.accordions} title={faq.title} />
@@ -168,6 +170,7 @@ export default async function HotelPage() {
</SidePeek>
{/* eslint-enable import/no-named-as-default-member */}
</SidePeekProvider>
<HotelReservationSidePeek hotel={null} />
</div>
)
}

View File

@@ -0,0 +1,33 @@
"use client"
import { useIntl } from "react-intl"
import useSidePeekStore from "@/stores/sidepeek"
import Button from "@/components/TempDesignSystem/Button"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import { ToggleSidePeekProps } from "@/types/components/hotelReservation/toggleSidePeekProps"
export default function ToggleSidePeek({
hotelId,
roomTypeCode,
}: ToggleSidePeekProps) {
const intl = useIntl()
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
return (
<Button
onClick={() =>
openSidePeek({ key: SidePeekEnum.roomDetails, hotelId, roomTypeCode })
}
theme="base"
size="small"
variant="icon"
intent="text"
wrapping
>
{intl.formatMessage({ id: "See room details" })}{" "}
</Button>
)
}

View File

@@ -2,15 +2,25 @@
import { useIntl } from "react-intl"
import { RoomConfiguration } from "@/server/routers/hotels/output"
import { EditIcon, ImageIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./selectedRoom.module.css"
export default function SelectedRoom() {
export default function SelectedRoom({
hotelId,
room,
}: {
hotelId: string
room: RoomConfiguration
}) {
const intl = useIntl()
return (
<article className={styles.container}>
@@ -22,42 +32,50 @@ export default function SelectedRoom() {
/>
</div>
<div className={styles.content}>
<div className={styles.textContainer}>
<Footnote
className={styles.label}
color="uiTextPlaceholder"
textTransform="uppercase"
>
{intl.formatMessage({ id: "Your room" })}
</Footnote>
<div className={styles.text}>
{/**
* [TEMP]
* No translation on Subtitles as they will be derived
* from Room selection.
*/}
<Subtitle
className={styles.room}
color="uiTextHighContrast"
type="two"
<div>
<div className={styles.textContainer}>
<Footnote
className={styles.label}
color="uiTextPlaceholder"
textTransform="uppercase"
>
Cozy cabin
</Subtitle>
<Subtitle
className={styles.invertFontWeight}
color="uiTextMediumContrast"
type="two"
>
Free rebooking
</Subtitle>
<Subtitle
className={styles.invertFontWeight}
color="uiTextMediumContrast"
type="two"
>
Pay now
</Subtitle>
{intl.formatMessage({ id: "Your room" })}
</Footnote>
<div className={styles.text}>
{/**
* [TEMP]
* No translation on Subtitles as they will be derived
* from Room selection.
*/}
<Subtitle
className={styles.room}
color="uiTextHighContrast"
type="two"
>
{room.roomType}
</Subtitle>
<Subtitle
className={styles.invertFontWeight}
color="uiTextMediumContrast"
type="two"
>
Free rebooking
</Subtitle>
<Subtitle
className={styles.invertFontWeight}
color="uiTextMediumContrast"
type="two"
>
Pay now
</Subtitle>
</div>
</div>
{room?.roomTypeCode && (
<ToggleSidePeek
hotelId={hotelId}
roomTypeCode={room.roomTypeCode}
/>
)}
</div>
<Button
asChild

View File

@@ -1,46 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import Contact from "@/components/HotelReservation/Contact"
import Divider from "@/components/TempDesignSystem/Divider"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import Body from "@/components/TempDesignSystem/Text/Body"
import styles from "./enterDetailsSidePeek.module.css"
import {
SidePeekEnum,
SidePeekProps,
} from "@/types/components/hotelReservation/enterDetails/sidePeek"
export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) {
const activeSidePeek = useEnterDetailsStore((state) => state.activeSidePeek)
const close = useEnterDetailsStore((state) => state.closeSidePeek)
const intl = useIntl()
return (
<SidePeek
contentKey={SidePeekEnum.hotelDetails}
title={intl.formatMessage({ id: "About the hotel" })}
isOpen={activeSidePeek === SidePeekEnum.hotelDetails}
handleClose={close}
>
<article className={styles.spacing}>
<Contact hotel={hotel} />
<Divider />
<section className={styles.spacing}>
<Body>{hotel.hotelContent.texts.descriptions.medium}</Body>
{hotel.hotelContent.texts.facilityInformation
.split(/[\n\r]/g)
.filter((p) => p)
.map((paragraph, idx) => (
<Body key={`facilityInfo-${idx}`}>{paragraph}</Body>
))}
</section>
</article>
</SidePeek>
)
}

View File

@@ -1,35 +0,0 @@
"use client"
import { useIntl } from "react-intl"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { ChevronRightSmallIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek"
export default function ToggleSidePeek() {
const intl = useIntl()
const openSidePeek = useEnterDetailsStore((state) => state.openSidePeek)
return (
<Button
onClick={() => {
openSidePeek(SidePeekEnum.hotelDetails)
}}
theme="base"
size="small"
variant="icon"
intent="text"
wrapping
>
{intl.formatMessage({ id: "See room details" })}{" "}
<ChevronRightSmallIcon
color="baseButtonTextOnFillNormal"
height={20}
width={20}
/>
</Button>
)
}

View File

@@ -1,111 +1,29 @@
"use client"
import { useState } from "react"
import { useIntl } from "react-intl"
import useSidePeekStore from "@/stores/sidepeek"
import { ChevronRightIcon } from "@/components/Icons"
import Accordion from "@/components/TempDesignSystem/Accordion"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
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 Contact from "../Contact"
import styles from "./readMore.module.css"
import {
ParkingProps,
ReadMoreProps,
} from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type { Amenities, Hotel } from "@/types/hotel"
import { ReadMoreProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
function getAmenitiesList(hotel: Hotel) {
const detailedAmenities: Amenities = hotel.detailedFacilities.filter(
// Remove Parking facilities since parking accordion is based on hotel.parking
(facility) => !facility.name.startsWith("Parking") && facility.public
)
return detailedAmenities
}
export default function ReadMore({ label, hotel, hotelId }: ReadMoreProps) {
const intl = useIntl()
const [sidePeekOpen, setSidePeekOpen] = useState(false)
const amenitiesList = getAmenitiesList(hotel)
export default function ReadMore({ label, hotelId }: ReadMoreProps) {
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
return (
<>
<Button
onPress={() => {
setSidePeekOpen(true)
}}
intent="text"
theme="base"
wrapping
className={styles.detailsButton}
>
{label}
<ChevronRightIcon color="burgundy" />
</Button>
<SidePeek
title={hotel.name}
isOpen={sidePeekOpen}
contentKey={`${hotelId}`}
handleClose={() => {
setSidePeekOpen(false)
}}
>
<div className={styles.content}>
<Subtitle>
{intl.formatMessage({ id: "Practical information" })}
</Subtitle>
<Contact hotel={hotel} />
<Accordion>
{/* parking */}
{hotel.parking.length ? (
<AccordionItem title={intl.formatMessage({ id: "Parking" })}>
{hotel.parking.map((p) => (
<Parking key={p.name} parking={p} />
))}
</AccordionItem>
) : null}
<AccordionItem title={intl.formatMessage({ id: "Accessibility" })}>
TODO: What content should be in the accessibility section?
</AccordionItem>
{amenitiesList.map((amenity) => {
return (
<div key={amenity.id} className={styles.amenity}>
{amenity.name}
</div>
)
})}
</Accordion>
{/* TODO: handle linking to Hotel Page */}
<Button theme={"base"}>To the hotel</Button>
</div>
</SidePeek>
</>
)
}
function Parking({ parking }: ParkingProps) {
const intl = useIntl()
return (
<div>
<Body>{`${intl.formatMessage({ id: parking.type })} (${parking.name})`}</Body>
<ul className={styles.list}>
<li>
{`${intl.formatMessage({
id: "Number of charging points for electric cars",
})}: ${parking.numberOfChargingSpaces}`}
</li>
<li>{`${intl.formatMessage({ id: "Parking can be reserved in advance" })}: ${parking.canMakeReservation ? intl.formatMessage({ id: "Yes" }) : intl.formatMessage({ id: "No" })}`}</li>
<li>{`${intl.formatMessage({ id: "Number of parking spots" })}: ${parking.numberOfParkingSpots}`}</li>
<li>{`${intl.formatMessage({ id: "Distance to hotel" })}: ${parking.distanceToHotel} m`}</li>
<li>{`${intl.formatMessage({ id: "Address" })}: ${parking.address}`}</li>
</ul>
</div>
<Button
onPress={() => {
openSidePeek({ key: SidePeekEnum.hotelDetails, hotelId })
}}
intent="text"
theme="base"
wrapping
className={styles.detailsButton}
>
{label}
<ChevronRightIcon color="burgundy" />
</Button>
)
}

View File

@@ -5,13 +5,13 @@ import { useIntl } from "react-intl"
import { RateDefinition } from "@/server/routers/hotels/output"
import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek"
import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import RoomSidePeek from "../../../../SidePeeks/RoomSidePeek"
import ImageGallery from "../../ImageGallery"
import { getIconForFeatureCode } from "../../utils"
@@ -21,6 +21,7 @@ import type { RoomCardProps } from "@/types/components/hotelReservation/selectRa
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
export default function RoomCard({
hotelId,
rateDefinitions,
roomConfiguration,
roomCategories,
@@ -87,8 +88,11 @@ export default function RoomCard({
: `${roomSize?.min}-${roomSize?.max}`}
m²
</Caption>
{selectedRoom && (
<RoomSidePeek room={selectedRoom} buttonSize="small" />
{roomConfiguration.roomTypeCode && (
<ToggleSidePeek
hotelId={hotelId}
roomTypeCode={roomConfiguration.roomTypeCode}
/>
)}
</div>
<div className={styles.container}>

View File

@@ -67,6 +67,7 @@ export default function RoomSelection({
{roomConfigurations.map((roomConfiguration) => (
<li key={roomConfiguration.roomTypeCode}>
<RoomCard
hotelId={roomsAvailability.hotelId.toString()}
rateDefinitions={rateDefinitions}
roomConfiguration={roomConfiguration}
roomCategories={roomCategories}

View File

@@ -0,0 +1,62 @@
"use client"
import { trpc } from "@/lib/trpc/client"
import { HotelIncludeEnum } from "@/server/routers/hotels/input"
import useSidePeekStore from "@/stores/sidepeek"
import HotelSidePeek from "@/components/SidePeeks/HotelSidePeek"
import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek"
import useLang from "@/hooks/useLang"
import { HotelData } from "@/types/hotel"
export default function HotelReservationSidePeek({
hotel,
}: {
hotel: HotelData | null
}) {
const activeSidePeek = useSidePeekStore((state) => state.activeSidePeek)
const hotelId = useSidePeekStore((state) => state.hotelId)
const roomTypeCode = useSidePeekStore((state) => state.roomTypeCode)
const close = useSidePeekStore((state) => state.closeSidePeek)
const lang = useLang()
const { data: hotelData } = trpc.hotel.hotelData.get.useQuery(
{
hotelId: hotelId ?? "",
language: lang,
include: [HotelIncludeEnum.RoomCategories],
},
{
enabled: !!hotelId,
initialData: hotel ?? undefined,
}
)
const selectedRoom = hotelData?.included?.find((room) =>
room.roomTypes.some((type) => type.code === roomTypeCode)
)
if (activeSidePeek) {
return (
<>
{hotelData && (
<HotelSidePeek
hotel={hotelData.data?.attributes}
activeSidePeek={activeSidePeek}
close={close}
/>
)}
{selectedRoom && (
<RoomSidePeek
room={selectedRoom}
activeSidePeek={activeSidePeek}
close={close}
/>
)}
</>
)
}
return null
}

View File

@@ -0,0 +1,23 @@
.spacing {
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
}
.content {
display: grid;
gap: var(--Spacing-x2);
}
.amenity {
font-family: var(--typography-Body-Regular-fontFamily);
border-bottom: 1px solid var(--Base-Border-Subtle);
/* padding set to align with AccordionItem which has a different composition */
padding: var(--Spacing-x2)
calc(var(--Spacing-x1) + var(--Spacing-x-one-and-half));
}
.list {
font-family: var(--typography-Body-Regular-fontFamily);
list-style: inside;
}

View File

@@ -0,0 +1,90 @@
import { useIntl } from "react-intl"
import Contact from "@/components/HotelReservation/Contact"
import Accordion from "@/components/TempDesignSystem/Accordion"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
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 styles from "./hotelSidePeek.module.css"
import { HotelSidePeekProps } from "@/types/components/hotelReservation/hotelSidePeek"
import { ParkingProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import { Amenities, Hotel } from "@/types/hotel"
function getAmenitiesList(hotel: Hotel) {
const detailedAmenities: Amenities = hotel.detailedFacilities.filter(
// Remove Parking facilities since parking accordion is based on hotel.parking
(facility) => !facility.name.startsWith("Parking") && facility.public
)
return detailedAmenities
}
export default function HotelSidePeek({
hotel,
activeSidePeek,
close,
}: HotelSidePeekProps) {
const intl = useIntl()
const amenitiesList = getAmenitiesList(hotel)
return (
<SidePeek
title={hotel.name}
isOpen={activeSidePeek === SidePeekEnum.hotelDetails}
handleClose={close}
>
<div className={styles.content}>
<Subtitle>
{intl.formatMessage({ id: "Practical information" })}
</Subtitle>
<Contact hotel={hotel} />
<Accordion>
{/* parking */}
{hotel.parking.length ? (
<AccordionItem title={intl.formatMessage({ id: "Parking" })}>
{hotel.parking.map((p) => (
<Parking key={p.name} parking={p} />
))}
</AccordionItem>
) : null}
<AccordionItem title={intl.formatMessage({ id: "Accessibility" })}>
TODO: What content should be in the accessibility section?
</AccordionItem>
{amenitiesList.map((amenity) => {
return (
<div key={amenity.id} className={styles.amenity}>
{amenity.name}
</div>
)
})}
</Accordion>
{/* TODO: handle linking to Hotel Page */}
<Button theme={"base"}>To the hotel</Button>
</div>
</SidePeek>
)
}
function Parking({ parking }: ParkingProps) {
const intl = useIntl()
return (
<div>
<Body>{`${intl.formatMessage({ id: parking.type })} (${parking.name})`}</Body>
<ul className={styles.list}>
<li>
{`${intl.formatMessage({
id: "Number of charging points for electric cars",
})}: ${parking.numberOfChargingSpaces}`}
</li>
<li>{`${intl.formatMessage({ id: "Parking can be reserved in advance" })}: ${parking.canMakeReservation ? intl.formatMessage({ id: "Yes" }) : intl.formatMessage({ id: "No" })}`}</li>
<li>{`${intl.formatMessage({ id: "Number of parking spots" })}: ${parking.numberOfParkingSpots}`}</li>
<li>{`${intl.formatMessage({ id: "Distance to hotel" })}: ${parking.distanceToHotel} m`}</li>
<li>{`${intl.formatMessage({ id: "Address" })}: ${parking.address}`}</li>
</ul>
</div>
)
}

View File

@@ -1,7 +1,5 @@
import { useState } from "react"
import { useIntl } from "react-intl"
import { ChevronRightSmallIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import Body from "@/components/TempDesignSystem/Text/Body"
@@ -12,10 +10,14 @@ import { getFacilityIcon } from "./facilityIcon"
import styles from "./roomSidePeek.module.css"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import type { RoomSidePeekProps } from "@/types/components/sidePeeks/roomSidePeek"
export default function RoomSidePeek({ room, buttonSize }: RoomSidePeekProps) {
const [isSidePeekOpen, setIsSidePeekOpen] = useState(false)
export default function RoomSidePeek({
room,
activeSidePeek,
close,
}: RoomSidePeekProps) {
const intl = useIntl()
const roomSize = room.roomSize
@@ -24,84 +26,70 @@ export default function RoomSidePeek({ room, buttonSize }: RoomSidePeekProps) {
const images = room.images
return (
<div>
<Button
intent="text"
type="button"
size={buttonSize}
theme="base"
className={styles.button}
onClick={() => setIsSidePeekOpen(true)}
>
{intl.formatMessage({ id: "See room details" })}
<ChevronRightSmallIcon color="burgundy" width={20} height={20} />
</Button>
<SidePeek
title={room.name}
isOpen={isSidePeekOpen}
handleClose={() => setIsSidePeekOpen(false)}
>
<div className={styles.wrapper}>
<div className={styles.mainContent}>
<Body color="baseTextMediumContrast">
{roomSize.min === roomSize.max
? roomSize.min
: `${roomSize.min} - ${roomSize.max}`}
m².{" "}
{intl.formatMessage(
{ id: "booking.accommodatesUpTo" },
{ nrOfGuests: occupancy }
)}
</Body>
{images && (
<div className={styles.imageContainer}>
<ImageGallery images={images} title={room.name} />
</div>
<SidePeek
title={room.name}
isOpen={activeSidePeek === SidePeekEnum.roomDetails}
handleClose={close}
>
<div className={styles.wrapper}>
<div className={styles.mainContent}>
<Body color="baseTextMediumContrast">
{roomSize.min === roomSize.max
? roomSize.min
: `${roomSize.min} - ${roomSize.max}`}
m².{" "}
{intl.formatMessage(
{ id: "booking.accommodatesUpTo" },
{ nrOfGuests: occupancy }
)}
<Body color="uiTextHighContrast">{roomDescription}</Body>
</div>
<div className={styles.listContainer}>
<Subtitle type="two" color="uiTextHighContrast">
{intl.formatMessage({ id: "booking.thisRoomIsEquippedWith" })}
</Subtitle>
<ul className={styles.facilityList}>
{room.roomFacilities
.sort((a, b) => a.sortOrder - b.sortOrder)
.map((facility) => {
const Icon = getFacilityIcon(facility.name)
return (
<li key={facility.name}>
{Icon && <Icon color="uiTextMediumContrast" />}
<Body
asChild
className={!Icon ? styles.noIcon : undefined}
color="uiTextMediumContrast"
>
<span>{facility.name}</span>
</Body>
</li>
)
})}
</ul>
</div>
<div className={styles.listContainer}>
<Subtitle type="two" color="uiTextHighContrast">
{intl.formatMessage({ id: "booking.bedOptions" })}
</Subtitle>
<Body color="grey">
{intl.formatMessage({ id: "booking.basedOnAvailability" })}
</Body>
{/* TODO: Get data for bed options */}
</div>
</Body>
{images && (
<div className={styles.imageContainer}>
<ImageGallery images={images} title={room.name} />
</div>
)}
<Body color="uiTextHighContrast">{roomDescription}</Body>
</div>
<div className={styles.buttonContainer}>
<Button fullWidth theme="base" intent="primary">
{intl.formatMessage({ id: "booking.selectRoom" })}
{/* TODO: Implement logic for select room */}
</Button>
<div className={styles.listContainer}>
<Subtitle type="two" color="uiTextHighContrast">
{intl.formatMessage({ id: "booking.thisRoomIsEquippedWith" })}
</Subtitle>
<ul className={styles.facilityList}>
{room.roomFacilities
.sort((a, b) => a.sortOrder - b.sortOrder)
.map((facility) => {
const Icon = getFacilityIcon(facility.name)
return (
<li key={facility.name}>
{Icon && <Icon color="uiTextMediumContrast" />}
<Body
asChild
className={!Icon ? styles.noIcon : undefined}
color="uiTextMediumContrast"
>
<span>{facility.name}</span>
</Body>
</li>
)
})}
</ul>
</div>
</SidePeek>
</div>
<div className={styles.listContainer}>
<Subtitle type="two" color="uiTextHighContrast">
{intl.formatMessage({ id: "booking.bedOptions" })}
</Subtitle>
<Body color="grey">
{intl.formatMessage({ id: "booking.basedOnAvailability" })}
</Body>
{/* TODO: Get data for bed options */}
</div>
</div>
<div className={styles.buttonContainer}>
<Button fullWidth theme="base" intent="primary">
{intl.formatMessage({ id: "booking.selectRoom" })}
{/* TODO: Implement logic for select room */}
</Button>
</div>
</SidePeek>
)
}

View File

@@ -395,6 +395,7 @@ export const hotelQueryRouter = router({
})
)
return {
hotelId,
hotelName: hotelAttributes.name,
hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short,
hotelLocation: hotelAttributes.location,

View File

@@ -11,10 +11,9 @@ import {
} from "@/components/HotelReservation/EnterDetails/Details/schema"
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData"
import { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData"
import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek"
import { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
@@ -28,7 +27,6 @@ interface EnterDetailsState {
roomData: BookingData
steps: StepEnum[]
currentStep: StepEnum
activeSidePeek: SidePeekEnum | null
isValid: Record<StepEnum, boolean>
completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void
navigate: (
@@ -36,8 +34,6 @@ interface EnterDetailsState {
updatedData?: Record<string, string | boolean | BreakfastPackage>
) => void
setCurrentStep: (step: StepEnum) => void
openSidePeek: (key: SidePeekEnum | null) => void
closeSidePeek: () => void
}
export function initEditDetailsState(
@@ -139,10 +135,7 @@ export function initEditDetailsState(
window.history.pushState({ step }, "", step + window.location.search)
})
),
openSidePeek: (key) => set({ activeSidePeek: key }),
closeSidePeek: () => set({ activeSidePeek: null }),
currentStep,
activeSidePeek: null,
isValid,
completeStep: (updatedData) =>
set(

31
stores/sidepeek.ts Normal file
View File

@@ -0,0 +1,31 @@
import { create } from "zustand"
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
interface SidePeekState {
activeSidePeek: SidePeekEnum | null
hotelId: string | null
roomTypeCode: string | null
openSidePeek: ({
key,
hotelId,
roomTypeCode,
}: {
key: SidePeekEnum | null
hotelId: string
roomTypeCode?: string
}) => void
closeSidePeek: () => void
}
const useSidePeekStore = create<SidePeekState>((set) => ({
activeSidePeek: null,
hotelId: null,
roomTypeCode: null,
openSidePeek: ({ key, hotelId, roomTypeCode }) =>
set({ activeSidePeek: key, hotelId, roomTypeCode }),
closeSidePeek: () =>
set({ activeSidePeek: null, hotelId: null, roomTypeCode: null }),
}))
export default useSidePeekStore

View File

@@ -1,9 +1,11 @@
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,8 @@
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
import { Hotel } from "@/types/hotel"
export type HotelSidePeekProps = {
hotel: Hotel
activeSidePeek: SidePeekEnum
close: () => void
}

View File

@@ -13,6 +13,7 @@ import type { RoomData } from "@/types/hotel"
import type { RoomPackageCodes, RoomPackageData } from "./roomFilter"
export type RoomCardProps = {
hotelId: string
roomConfiguration: RoomConfiguration
rateDefinitions: RateDefinition[]
roomCategories: RoomData[]

View File

@@ -2,6 +2,7 @@ import { Hotel } from "@/types/hotel"
export enum SidePeekEnum {
hotelDetails = "hotel-detail-side-peek",
roomDetails = "room-detail-side-peek",
}
export type SidePeekProps = {

View File

@@ -0,0 +1,4 @@
export type ToggleSidePeekProps = {
hotelId: string
roomTypeCode: string
}

View File

@@ -1,6 +1,9 @@
import { SidePeekEnum } from "../hotelReservation/sidePeek"
import type { RoomData } from "@/types/hotel"
export type RoomSidePeekProps = {
room: RoomData
buttonSize: "small" | "medium"
activeSidePeek: SidePeekEnum | null
close: () => void
}