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:
@@ -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} />
|
||||||
|
}
|
||||||
@@ -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} />
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { getCreditCardsSafely , getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
export function preload() {
|
||||||
|
void getProfileSafely()
|
||||||
|
void getCreditCardsSafely()
|
||||||
|
}
|
||||||
@@ -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 EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
|
||||||
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import { preload } from "./page"
|
import { preload } from "./_preload"
|
||||||
|
|
||||||
import styles from "./layout.module.css"
|
import styles from "./layout.module.css"
|
||||||
|
|
||||||
@@ -16,11 +18,9 @@ export default async function StepLayout({
|
|||||||
children,
|
children,
|
||||||
hotelHeader,
|
hotelHeader,
|
||||||
params,
|
params,
|
||||||
sidePeek,
|
|
||||||
}: React.PropsWithChildren<
|
}: React.PropsWithChildren<
|
||||||
LayoutArgs<LangParams & { step: StepEnum }> & {
|
LayoutArgs<LangParams & { step: StepEnum }> & {
|
||||||
hotelHeader: React.ReactNode
|
hotelHeader: React.ReactNode
|
||||||
sidePeek: React.ReactNode
|
|
||||||
summary: React.ReactNode
|
summary: React.ReactNode
|
||||||
}
|
}
|
||||||
>) {
|
>) {
|
||||||
@@ -34,7 +34,6 @@ export default async function StepLayout({
|
|||||||
<main className={styles.layout}>
|
<main className={styles.layout}>
|
||||||
{hotelHeader}
|
{hotelHeader}
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<SelectedRoom />
|
|
||||||
{children}
|
{children}
|
||||||
<aside className={styles.summaryContainer}>
|
<aside className={styles.summaryContainer}>
|
||||||
<div className={styles.hider} />
|
<div className={styles.hider} />
|
||||||
@@ -42,7 +41,6 @@ export default async function StepLayout({
|
|||||||
<div className={styles.shadow} />
|
<div className={styles.shadow} />
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
{sidePeek}
|
|
||||||
</main>
|
</main>
|
||||||
</EnterDetailsProvider>
|
</EnterDetailsProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import Details from "@/components/HotelReservation/EnterDetails/Details"
|
|||||||
import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager"
|
import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager"
|
||||||
import Payment from "@/components/HotelReservation/EnterDetails/Payment"
|
import Payment from "@/components/HotelReservation/EnterDetails/Payment"
|
||||||
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
|
||||||
|
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||||
import {
|
import {
|
||||||
generateChildrenString,
|
generateChildrenString,
|
||||||
getQueryParamsForEnterDetails,
|
getQueryParamsForEnterDetails,
|
||||||
@@ -25,11 +26,6 @@ import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
|||||||
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
import type { LangParams, PageArgs } from "@/types/params"
|
import type { LangParams, PageArgs } from "@/types/params"
|
||||||
|
|
||||||
export function preload() {
|
|
||||||
void getProfileSafely()
|
|
||||||
void getCreditCardsSafely()
|
|
||||||
}
|
|
||||||
|
|
||||||
function isValidStep(step: string): step is StepEnum {
|
function isValidStep(step: string): step is StepEnum {
|
||||||
return Object.values(StepEnum).includes(step as StepEnum)
|
return Object.values(StepEnum).includes(step as StepEnum)
|
||||||
}
|
}
|
||||||
@@ -106,6 +102,9 @@ export default async function StepPage({
|
|||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<HistoryStateManager />
|
<HistoryStateManager />
|
||||||
|
|
||||||
|
<SelectedRoom hotelId={hotelId} room={roomAvailability.selectedRoom} />
|
||||||
|
|
||||||
{/* TODO: How to handle no beds found? */}
|
{/* TODO: How to handle no beds found? */}
|
||||||
{roomAvailability.bedTypes ? (
|
{roomAvailability.bedTypes ? (
|
||||||
<SectionAccordion
|
<SectionAccordion
|
||||||
|
|||||||
@@ -8,9 +8,17 @@ import { LangParams, LayoutArgs } from "@/types/params"
|
|||||||
|
|
||||||
export default function HotelReservationLayout({
|
export default function HotelReservationLayout({
|
||||||
children,
|
children,
|
||||||
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
sidePeek,
|
||||||
|
}: React.PropsWithChildren<LayoutArgs<LangParams>> & {
|
||||||
|
sidePeek: React.ReactNode
|
||||||
|
}) {
|
||||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
return <div className={styles.layout}>{children}</div>
|
return (
|
||||||
|
<div className={styles.layout}>
|
||||||
|
{children}
|
||||||
|
{sidePeek}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { GalleryIcon } from "@/components/Icons"
|
import { GalleryIcon } from "@/components/Icons"
|
||||||
import Image from "@/components/Image"
|
import Image from "@/components/Image"
|
||||||
import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
|
import RoomDetailsButton from "../RoomDetailsButton"
|
||||||
|
|
||||||
import styles from "./roomCard.module.css"
|
import styles from "./roomCard.module.css"
|
||||||
|
|
||||||
import type { RoomCardProps } from "@/types/components/hotelPage/room"
|
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 { images, name, roomSize, occupancy, id } = room
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const mainImage = images[0]
|
const mainImage = images[0]
|
||||||
@@ -70,7 +71,10 @@ export function RoomCard({ room }: RoomCardProps) {
|
|||||||
</Subtitle>
|
</Subtitle>
|
||||||
<Body color="grey">{subtitle}</Body>
|
<Body color="grey">{subtitle}</Body>
|
||||||
</div>
|
</div>
|
||||||
<RoomSidePeek room={room} buttonSize="medium" />
|
<RoomDetailsButton
|
||||||
|
hotelId={hotelId}
|
||||||
|
roomTypeCode={room.roomTypes[0].code}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import styles from "./rooms.module.css"
|
|||||||
import type { RoomsProps } from "@/types/components/hotelPage/room"
|
import type { RoomsProps } from "@/types/components/hotelPage/room"
|
||||||
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
|
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
|
||||||
|
|
||||||
export function Rooms({ rooms }: RoomsProps) {
|
export function Rooms({ hotelId, rooms }: RoomsProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const showToggleButton = rooms.length > 3
|
const showToggleButton = rooms.length > 3
|
||||||
const [allRoomsVisible, setAllRoomsVisible] = useState(!showToggleButton)
|
const [allRoomsVisible, setAllRoomsVisible] = useState(!showToggleButton)
|
||||||
@@ -45,7 +45,7 @@ export function Rooms({ rooms }: RoomsProps) {
|
|||||||
>
|
>
|
||||||
{rooms.map((room) => (
|
{rooms.map((room) => (
|
||||||
<div key={room.id}>
|
<div key={room.id}>
|
||||||
<RoomCard room={room} />
|
<RoomCard hotelId={hotelId} room={room} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</Grids.Stackable>
|
</Grids.Stackable>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { env } from "@/env/server"
|
|||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
import AccordionSection from "@/components/Blocks/Accordion"
|
import AccordionSection from "@/components/Blocks/Accordion"
|
||||||
|
import HotelReservationSidePeek from "@/components/HotelReservation/SidePeek"
|
||||||
import SidePeekProvider from "@/components/SidePeeks/SidePeekProvider"
|
import SidePeekProvider from "@/components/SidePeeks/SidePeekProvider"
|
||||||
import Alert from "@/components/TempDesignSystem/Alert"
|
import Alert from "@/components/TempDesignSystem/Alert"
|
||||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||||
@@ -39,6 +40,7 @@ export default async function HotelPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
hotelId,
|
||||||
hotelName,
|
hotelName,
|
||||||
hotelDescription,
|
hotelDescription,
|
||||||
hotelLocation,
|
hotelLocation,
|
||||||
@@ -99,7 +101,7 @@ export default async function HotelPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<Rooms rooms={roomCategories} />
|
<Rooms hotelId={hotelId} rooms={roomCategories} />
|
||||||
<Facilities facilities={facilities} activitiesCard={activitiesCard} />
|
<Facilities facilities={facilities} activitiesCard={activitiesCard} />
|
||||||
{faq.accordions.length > 0 && (
|
{faq.accordions.length > 0 && (
|
||||||
<AccordionSection accordion={faq.accordions} title={faq.title} />
|
<AccordionSection accordion={faq.accordions} title={faq.title} />
|
||||||
@@ -168,6 +170,7 @@ export default async function HotelPage() {
|
|||||||
</SidePeek>
|
</SidePeek>
|
||||||
{/* eslint-enable import/no-named-as-default-member */}
|
{/* eslint-enable import/no-named-as-default-member */}
|
||||||
</SidePeekProvider>
|
</SidePeekProvider>
|
||||||
|
<HotelReservationSidePeek hotel={null} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,15 +2,25 @@
|
|||||||
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { RoomConfiguration } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
import { EditIcon, ImageIcon } from "@/components/Icons"
|
import { EditIcon, ImageIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
|
import ToggleSidePeek from "./ToggleSidePeek"
|
||||||
|
|
||||||
import styles from "./selectedRoom.module.css"
|
import styles from "./selectedRoom.module.css"
|
||||||
|
|
||||||
export default function SelectedRoom() {
|
export default function SelectedRoom({
|
||||||
|
hotelId,
|
||||||
|
room,
|
||||||
|
}: {
|
||||||
|
hotelId: string
|
||||||
|
room: RoomConfiguration
|
||||||
|
}) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
return (
|
return (
|
||||||
<article className={styles.container}>
|
<article className={styles.container}>
|
||||||
@@ -22,42 +32,50 @@ export default function SelectedRoom() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.textContainer}>
|
<div>
|
||||||
<Footnote
|
<div className={styles.textContainer}>
|
||||||
className={styles.label}
|
<Footnote
|
||||||
color="uiTextPlaceholder"
|
className={styles.label}
|
||||||
textTransform="uppercase"
|
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"
|
|
||||||
>
|
>
|
||||||
Cozy cabin
|
{intl.formatMessage({ id: "Your room" })}
|
||||||
</Subtitle>
|
</Footnote>
|
||||||
<Subtitle
|
<div className={styles.text}>
|
||||||
className={styles.invertFontWeight}
|
{/**
|
||||||
color="uiTextMediumContrast"
|
* [TEMP]
|
||||||
type="two"
|
* No translation on Subtitles as they will be derived
|
||||||
>
|
* from Room selection.
|
||||||
Free rebooking
|
*/}
|
||||||
</Subtitle>
|
<Subtitle
|
||||||
<Subtitle
|
className={styles.room}
|
||||||
className={styles.invertFontWeight}
|
color="uiTextHighContrast"
|
||||||
color="uiTextMediumContrast"
|
type="two"
|
||||||
type="two"
|
>
|
||||||
>
|
{room.roomType}
|
||||||
Pay now
|
</Subtitle>
|
||||||
</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>
|
</div>
|
||||||
|
{room?.roomTypeCode && (
|
||||||
|
<ToggleSidePeek
|
||||||
|
hotelId={hotelId}
|
||||||
|
roomTypeCode={room.roomTypeCode}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
|
|||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,111 +1,29 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import useSidePeekStore from "@/stores/sidepeek"
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { ChevronRightIcon } from "@/components/Icons"
|
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 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 styles from "./readMore.module.css"
|
||||||
|
|
||||||
import {
|
import { ReadMoreProps } from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
||||||
ParkingProps,
|
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
|
||||||
ReadMoreProps,
|
|
||||||
} from "@/types/components/hotelReservation/selectHotel/selectHotel"
|
|
||||||
import type { Amenities, Hotel } from "@/types/hotel"
|
|
||||||
|
|
||||||
function getAmenitiesList(hotel: Hotel) {
|
export default function ReadMore({ label, hotelId }: ReadMoreProps) {
|
||||||
const detailedAmenities: Amenities = hotel.detailedFacilities.filter(
|
const openSidePeek = useSidePeekStore((state) => state.openSidePeek)
|
||||||
// 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)
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Button
|
||||||
<Button
|
onPress={() => {
|
||||||
onPress={() => {
|
openSidePeek({ key: SidePeekEnum.hotelDetails, hotelId })
|
||||||
setSidePeekOpen(true)
|
}}
|
||||||
}}
|
intent="text"
|
||||||
intent="text"
|
theme="base"
|
||||||
theme="base"
|
wrapping
|
||||||
wrapping
|
className={styles.detailsButton}
|
||||||
className={styles.detailsButton}
|
>
|
||||||
>
|
{label}
|
||||||
{label}
|
<ChevronRightIcon color="burgundy" />
|
||||||
<ChevronRightIcon color="burgundy" />
|
</Button>
|
||||||
</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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import { useIntl } from "react-intl"
|
|||||||
|
|
||||||
import { RateDefinition } from "@/server/routers/hotels/output"
|
import { RateDefinition } from "@/server/routers/hotels/output"
|
||||||
|
|
||||||
|
import ToggleSidePeek from "@/components/HotelReservation/EnterDetails/SelectedRoom/ToggleSidePeek"
|
||||||
import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption"
|
import FlexibilityOption from "@/components/HotelReservation/SelectRate/RoomSelection/FlexibilityOption"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
|
|
||||||
import RoomSidePeek from "../../../../SidePeeks/RoomSidePeek"
|
|
||||||
import ImageGallery from "../../ImageGallery"
|
import ImageGallery from "../../ImageGallery"
|
||||||
import { getIconForFeatureCode } from "../../utils"
|
import { getIconForFeatureCode } from "../../utils"
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ import type { RoomCardProps } from "@/types/components/hotelReservation/selectRa
|
|||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
|
||||||
export default function RoomCard({
|
export default function RoomCard({
|
||||||
|
hotelId,
|
||||||
rateDefinitions,
|
rateDefinitions,
|
||||||
roomConfiguration,
|
roomConfiguration,
|
||||||
roomCategories,
|
roomCategories,
|
||||||
@@ -87,8 +88,11 @@ export default function RoomCard({
|
|||||||
: `${roomSize?.min}-${roomSize?.max}`}
|
: `${roomSize?.min}-${roomSize?.max}`}
|
||||||
m²
|
m²
|
||||||
</Caption>
|
</Caption>
|
||||||
{selectedRoom && (
|
{roomConfiguration.roomTypeCode && (
|
||||||
<RoomSidePeek room={selectedRoom} buttonSize="small" />
|
<ToggleSidePeek
|
||||||
|
hotelId={hotelId}
|
||||||
|
roomTypeCode={roomConfiguration.roomTypeCode}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ export default function RoomSelection({
|
|||||||
{roomConfigurations.map((roomConfiguration) => (
|
{roomConfigurations.map((roomConfiguration) => (
|
||||||
<li key={roomConfiguration.roomTypeCode}>
|
<li key={roomConfiguration.roomTypeCode}>
|
||||||
<RoomCard
|
<RoomCard
|
||||||
|
hotelId={roomsAvailability.hotelId.toString()}
|
||||||
rateDefinitions={rateDefinitions}
|
rateDefinitions={rateDefinitions}
|
||||||
roomConfiguration={roomConfiguration}
|
roomConfiguration={roomConfiguration}
|
||||||
roomCategories={roomCategories}
|
roomCategories={roomCategories}
|
||||||
|
|||||||
62
components/HotelReservation/SidePeek/index.tsx
Normal file
62
components/HotelReservation/SidePeek/index.tsx
Normal 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
|
||||||
|
}
|
||||||
23
components/SidePeeks/HotelSidePeek/hotelSidePeek.module.css
Normal file
23
components/SidePeeks/HotelSidePeek/hotelSidePeek.module.css
Normal 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;
|
||||||
|
}
|
||||||
90
components/SidePeeks/HotelSidePeek/index.tsx
Normal file
90
components/SidePeeks/HotelSidePeek/index.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
import { useState } from "react"
|
|
||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
import SidePeek from "@/components/TempDesignSystem/SidePeek"
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
@@ -12,10 +10,14 @@ import { getFacilityIcon } from "./facilityIcon"
|
|||||||
|
|
||||||
import styles from "./roomSidePeek.module.css"
|
import styles from "./roomSidePeek.module.css"
|
||||||
|
|
||||||
|
import { SidePeekEnum } from "@/types/components/hotelReservation/sidePeek"
|
||||||
import type { RoomSidePeekProps } from "@/types/components/sidePeeks/roomSidePeek"
|
import type { RoomSidePeekProps } from "@/types/components/sidePeeks/roomSidePeek"
|
||||||
|
|
||||||
export default function RoomSidePeek({ room, buttonSize }: RoomSidePeekProps) {
|
export default function RoomSidePeek({
|
||||||
const [isSidePeekOpen, setIsSidePeekOpen] = useState(false)
|
room,
|
||||||
|
activeSidePeek,
|
||||||
|
close,
|
||||||
|
}: RoomSidePeekProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
const roomSize = room.roomSize
|
const roomSize = room.roomSize
|
||||||
@@ -24,84 +26,70 @@ export default function RoomSidePeek({ room, buttonSize }: RoomSidePeekProps) {
|
|||||||
const images = room.images
|
const images = room.images
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<SidePeek
|
||||||
<Button
|
title={room.name}
|
||||||
intent="text"
|
isOpen={activeSidePeek === SidePeekEnum.roomDetails}
|
||||||
type="button"
|
handleClose={close}
|
||||||
size={buttonSize}
|
>
|
||||||
theme="base"
|
<div className={styles.wrapper}>
|
||||||
className={styles.button}
|
<div className={styles.mainContent}>
|
||||||
onClick={() => setIsSidePeekOpen(true)}
|
<Body color="baseTextMediumContrast">
|
||||||
>
|
{roomSize.min === roomSize.max
|
||||||
{intl.formatMessage({ id: "See room details" })}
|
? roomSize.min
|
||||||
<ChevronRightSmallIcon color="burgundy" width={20} height={20} />
|
: `${roomSize.min} - ${roomSize.max}`}
|
||||||
</Button>
|
m².{" "}
|
||||||
|
{intl.formatMessage(
|
||||||
<SidePeek
|
{ id: "booking.accommodatesUpTo" },
|
||||||
title={room.name}
|
{ nrOfGuests: occupancy }
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
<Body color="uiTextHighContrast">{roomDescription}</Body>
|
</Body>
|
||||||
</div>
|
{images && (
|
||||||
<div className={styles.listContainer}>
|
<div className={styles.imageContainer}>
|
||||||
<Subtitle type="two" color="uiTextHighContrast">
|
<ImageGallery images={images} title={room.name} />
|
||||||
{intl.formatMessage({ id: "booking.thisRoomIsEquippedWith" })}
|
</div>
|
||||||
</Subtitle>
|
)}
|
||||||
<ul className={styles.facilityList}>
|
<Body color="uiTextHighContrast">{roomDescription}</Body>
|
||||||
{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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.listContainer}>
|
||||||
<Button fullWidth theme="base" intent="primary">
|
<Subtitle type="two" color="uiTextHighContrast">
|
||||||
{intl.formatMessage({ id: "booking.selectRoom" })}
|
{intl.formatMessage({ id: "booking.thisRoomIsEquippedWith" })}
|
||||||
{/* TODO: Implement logic for select room */}
|
</Subtitle>
|
||||||
</Button>
|
<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>
|
||||||
</SidePeek>
|
<div className={styles.listContainer}>
|
||||||
</div>
|
<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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -395,6 +395,7 @@ export const hotelQueryRouter = router({
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
|
hotelId,
|
||||||
hotelName: hotelAttributes.name,
|
hotelName: hotelAttributes.name,
|
||||||
hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short,
|
hotelDescription: hotelAttributes.hotelContent.texts.descriptions.short,
|
||||||
hotelLocation: hotelAttributes.location,
|
hotelLocation: hotelAttributes.location,
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ import {
|
|||||||
} from "@/components/HotelReservation/EnterDetails/Details/schema"
|
} from "@/components/HotelReservation/EnterDetails/Details/schema"
|
||||||
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
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 { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||||
import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
|
import { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek"
|
|
||||||
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
||||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
|
|
||||||
@@ -28,7 +27,6 @@ interface EnterDetailsState {
|
|||||||
roomData: BookingData
|
roomData: BookingData
|
||||||
steps: StepEnum[]
|
steps: StepEnum[]
|
||||||
currentStep: StepEnum
|
currentStep: StepEnum
|
||||||
activeSidePeek: SidePeekEnum | null
|
|
||||||
isValid: Record<StepEnum, boolean>
|
isValid: Record<StepEnum, boolean>
|
||||||
completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void
|
completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void
|
||||||
navigate: (
|
navigate: (
|
||||||
@@ -36,8 +34,6 @@ interface EnterDetailsState {
|
|||||||
updatedData?: Record<string, string | boolean | BreakfastPackage>
|
updatedData?: Record<string, string | boolean | BreakfastPackage>
|
||||||
) => void
|
) => void
|
||||||
setCurrentStep: (step: StepEnum) => void
|
setCurrentStep: (step: StepEnum) => void
|
||||||
openSidePeek: (key: SidePeekEnum | null) => void
|
|
||||||
closeSidePeek: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initEditDetailsState(
|
export function initEditDetailsState(
|
||||||
@@ -139,10 +135,7 @@ export function initEditDetailsState(
|
|||||||
window.history.pushState({ step }, "", step + window.location.search)
|
window.history.pushState({ step }, "", step + window.location.search)
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
openSidePeek: (key) => set({ activeSidePeek: key }),
|
|
||||||
closeSidePeek: () => set({ activeSidePeek: null }),
|
|
||||||
currentStep,
|
currentStep,
|
||||||
activeSidePeek: null,
|
|
||||||
isValid,
|
isValid,
|
||||||
completeStep: (updatedData) =>
|
completeStep: (updatedData) =>
|
||||||
set(
|
set(
|
||||||
|
|||||||
31
stores/sidepeek.ts
Normal file
31
stores/sidepeek.ts
Normal 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
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import type { RoomData } from "@/types/hotel"
|
import type { RoomData } from "@/types/hotel"
|
||||||
|
|
||||||
export interface RoomCardProps {
|
export interface RoomCardProps {
|
||||||
|
hotelId: string
|
||||||
room: RoomData
|
room: RoomData
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RoomsProps = {
|
export type RoomsProps = {
|
||||||
|
hotelId: string
|
||||||
rooms: RoomData[]
|
rooms: RoomData[]
|
||||||
}
|
}
|
||||||
|
|||||||
8
types/components/hotelReservation/hotelSidePeek.ts
Normal file
8
types/components/hotelReservation/hotelSidePeek.ts
Normal 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
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import type { RoomData } from "@/types/hotel"
|
|||||||
import type { RoomPackageCodes, RoomPackageData } from "./roomFilter"
|
import type { RoomPackageCodes, RoomPackageData } from "./roomFilter"
|
||||||
|
|
||||||
export type RoomCardProps = {
|
export type RoomCardProps = {
|
||||||
|
hotelId: string
|
||||||
roomConfiguration: RoomConfiguration
|
roomConfiguration: RoomConfiguration
|
||||||
rateDefinitions: RateDefinition[]
|
rateDefinitions: RateDefinition[]
|
||||||
roomCategories: RoomData[]
|
roomCategories: RoomData[]
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Hotel } from "@/types/hotel"
|
|||||||
|
|
||||||
export enum SidePeekEnum {
|
export enum SidePeekEnum {
|
||||||
hotelDetails = "hotel-detail-side-peek",
|
hotelDetails = "hotel-detail-side-peek",
|
||||||
|
roomDetails = "room-detail-side-peek",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SidePeekProps = {
|
export type SidePeekProps = {
|
||||||
4
types/components/hotelReservation/toggleSidePeekProps.ts
Normal file
4
types/components/hotelReservation/toggleSidePeekProps.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export type ToggleSidePeekProps = {
|
||||||
|
hotelId: string
|
||||||
|
roomTypeCode: string
|
||||||
|
}
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import { SidePeekEnum } from "../hotelReservation/sidePeek"
|
||||||
|
|
||||||
import type { RoomData } from "@/types/hotel"
|
import type { RoomData } from "@/types/hotel"
|
||||||
|
|
||||||
export type RoomSidePeekProps = {
|
export type RoomSidePeekProps = {
|
||||||
room: RoomData
|
room: RoomData
|
||||||
buttonSize: "small" | "medium"
|
activeSidePeek: SidePeekEnum | null
|
||||||
|
close: () => void
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user