feat: consume serach params in summary and step page

This commit is contained in:
Christel Westerberg
2024-10-24 10:53:05 +02:00
parent 85fdefb5ac
commit 7954c704d9
27 changed files with 376 additions and 263 deletions

View File

@@ -16,7 +16,18 @@ import styles from "./bedOptions.module.css"
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
export default function BedType() {
export default function BedType({
roomTypes,
}: {
roomTypes: {
description: string
size: {
min: number
max: number
}
value: string
}[]
}) {
const intl = useIntl()
const bedType = useEnterDetailsStore((state) => state.userData.bedType)
@@ -57,38 +68,25 @@ export default function BedType() {
return (
<FormProvider {...methods}>
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
<RadioCard
Icon={KingBedIcon}
iconWidth={46}
id={BedTypeEnum.KING}
name="bedType"
subtitle={intl.formatMessage(
{ id: "{width} cm × {length} cm" },
{
length: "210",
width: "180",
}
)}
text={text}
title={intl.formatMessage({ id: "King bed" })}
value={BedTypeEnum.KING}
/>
<RadioCard
Icon={KingBedIcon}
iconWidth={46}
id={BedTypeEnum.QUEEN}
name="bedType"
subtitle={intl.formatMessage(
{ id: "{width} cm × {length} cm" },
{
length: "200",
width: "160",
}
)}
text={text}
title={intl.formatMessage({ id: "Queen bed" })}
value={BedTypeEnum.QUEEN}
/>
{roomTypes.map((roomType) => {
const width =
roomType.size.max === roomType.size.min
? roomType.size.max
: `${roomType.size.min} cm - ${roomType.size.max} cm`
return (
<RadioCard
key={roomType.value}
Icon={KingBedIcon}
iconWidth={46}
id={roomType.value}
name="bedType"
subtitle={width}
text={text}
title={roomType.description}
value={roomType.description}
/>
)
})}
</form>
</FormProvider>
)

View File

@@ -3,5 +3,5 @@ import { z } from "zod"
import { BedTypeEnum } from "@/types/enums/bedType"
export const bedTypeSchema = z.object({
bedType: z.nativeEnum(BedTypeEnum),
bedType: z.string(),
})

View File

@@ -56,7 +56,7 @@ export default function Payment({
const intl = useIntl()
const queryParams = useSearchParams()
const { firstName, lastName, email, phoneNumber, countryCode } =
useEnterDetailsStore((state) => state.data)
useEnterDetailsStore((state) => state.userData)
const [confirmationNumber, setConfirmationNumber] = useState<string>("")
const methods = useForm<PaymentFormData>({

View File

@@ -1,52 +1,43 @@
"use client"
import { useEffect, useState } from "react"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { trpc } from "@/lib/trpc/client"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { ArrowRightIcon } from "@/components/Icons"
import LoadingSpinner from "@/components/LoadingSpinner"
import Divider from "@/components/TempDesignSystem/Divider"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { formatNumber } from "@/utils/format"
import styles from "./summary.module.css"
import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData"
export default function Summary({ isMember }: { isMember: boolean }) {
export default function Summary({
isMember,
room,
}: {
isMember: boolean
room: RoomsData
}) {
const [chosenBed, setChosenBed] = useState<string | undefined>()
const [chosenBreakfast, setCosenBreakfast] = useState<string | undefined>()
const intl = useIntl()
const lang = useLang()
const { fromDate, toDate, rooms, hotel, bedType, breakfast } =
useEnterDetailsStore((state) => ({
fromDate: state.roomData.fromdate,
toDate: state.roomData.todate,
const { fromDate, toDate, bedType, breakfast } = useEnterDetailsStore(
(state) => ({
fromDate: state.roomData.fromDate,
toDate: state.roomData.toDate,
rooms: state.roomData.room,
hotel: state.roomData.hotel,
bedType: state.userData.bedType,
breakfast: state.userData.breakfast,
}))
const totalAdults = rooms.reduce((total, room) => total + room.adults, 0)
const {
data: availabilityData,
isLoading,
error,
} = trpc.hotel.availability.rooms.useQuery(
{
hotelId: parseInt(hotel),
adults: totalAdults,
roomStayStartDate: dt(fromDate).format("YYYY-MM-DD"),
roomStayEndDate: dt(toDate).format("YYYY-MM-DD"),
},
{ enabled: !!hotel && !!fromDate && !!toDate }
})
)
const diff = dt(toDate).diff(fromDate, "days")
@@ -56,37 +47,15 @@ export default function Summary({ isMember }: { isMember: boolean }) {
{ totalNights: diff }
)
if (isLoading) {
return <LoadingSpinner />
let color: "uiTextHighContrast" | "red" = "uiTextHighContrast"
if (isMember) {
color = "red"
}
const populatedRooms = rooms
.map((room) => {
const chosenRoom = availabilityData?.roomConfigurations.find(
(availRoom) => room.roomtypecode === availRoom.roomTypeCode
)
const cancellationText = availabilityData?.rateDefinitions.find(
(rate) => rate.rateCode === room.ratecode
)?.cancellationText
if (chosenRoom) {
const memberPrice = chosenRoom.products.find(
(rate) => rate.productType.member?.rateCode === room.ratecode
)?.productType.member?.localPrice.pricePerStay
const publicPrice = chosenRoom.products.find(
(rate) => rate.productType.public?.rateCode === room.ratecode
)?.productType.public?.localPrice.pricePerStay
return {
roomType: chosenRoom.roomType,
memberPrice: memberPrice && formatNumber(parseInt(memberPrice)),
publicPrice: publicPrice && formatNumber(parseInt(publicPrice)),
adults: room.adults,
children: room.child,
cancellationText,
}
}
})
.filter((room): room is RoomsData => room !== undefined)
useEffect(() => {
setChosenBed(bedType)
setCosenBreakfast(breakfast)
}, [bedType, breakfast])
return (
<section className={styles.summary}>
@@ -100,14 +69,48 @@ export default function Summary({ isMember }: { isMember: boolean }) {
</header>
<Divider color="primaryLightSubtle" />
<div className={styles.addOns}>
{populatedRooms.map((room, idx) => (
<RoomBreakdown key={idx} room={room} isMember={isMember} />
))}
{bedType ? (
<div>
<div className={styles.entry}>
<Body color="textHighContrast">{bedType}</Body>
<Caption color="red">
<Body color="textHighContrast">{room.roomType}</Body>
<Caption color={color}>
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: room.price, currency: "SEK" }
)}
</Caption>
</div>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "booking.adults" },
{ totalAdults: room.adults }
)}
</Caption>
{room.children?.length ? (
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "booking.children" },
{ totalChildren: room.children.length }
)}
</Caption>
) : null}
<Caption color="uiTextMediumContrast">
{room.cancellationText}
</Caption>
<Link color="burgundy" href="#" variant="underscored" size="small">
{intl.formatMessage({ id: "Rate details" })}
</Link>
</div>
{chosenBed ? (
<div className={styles.entry}>
<div>
<Body color="textHighContrast">{chosenBed}</Body>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Based on availability" })}
</Caption>
</div>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "0", currency: "SEK" }
@@ -115,10 +118,11 @@ export default function Summary({ isMember }: { isMember: boolean }) {
</Caption>
</div>
) : null}
{breakfast ? (
{chosenBreakfast ? (
<div className={styles.entry}>
<Body color="textHighContrast">{breakfast}</Body>
<Caption color="red">
<Body color="textHighContrast">{chosenBreakfast}</Body>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "0", currency: "SEK" }
@@ -130,77 +134,35 @@ export default function Summary({ isMember }: { isMember: boolean }) {
<Divider color="primaryLightSubtle" />
<div className={styles.total}>
<div className={styles.entry}>
<Body textTransform="bold">
{intl.formatMessage({ id: "Total price (incl VAT)" })}
</Body>
<Body textTransform="bold">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "4686", currency: "SEK" }
)}
</Body>
</div>
<div className={styles.entry}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Approx." })}
</Caption>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "455", currency: "EUR" }
)}
</Caption>
<div>
<Body>
{intl.formatMessage<React.ReactNode>(
{ id: "<b>Total price</b> (incl VAT)" },
{ b: (str) => <b>{str}</b> }
)}
</Body>
<Link color="burgundy" href="#" variant="underscored" size="small">
{intl.formatMessage({ id: "Price details" })}
</Link>
</div>
<div>
<Body textTransform="bold">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: room.price, currency: "SEK" } // TODO: calculate total price
)}
</Body>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Approx." })}{" "}
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "455", currency: "EUR" }
)}
</Caption>
</div>
</div>
<Divider color="primaryLightSubtle" />
</div>
</section>
)
}
function RoomBreakdown({
room,
isMember,
}: {
room: RoomsData
isMember: boolean
}) {
const intl = useIntl()
let color: "uiTextHighContrast" | "red" = "uiTextHighContrast"
let price = room.publicPrice
if (isMember) {
color = "red"
price = room.memberPrice
}
return (
<div>
<div className={styles.entry}>
<Body color="textHighContrast">{room.roomType}</Body>
<Caption color={color}>
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: price, currency: "SEK" }
)}
</Caption>
</div>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "booking.adults" },
{ totalAdults: room.adults }
)}
</Caption>
{room.children?.length ? (
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "booking.children" },
{ totalChildren: room.children.length }
)}
</Caption>
) : null}
<Caption color="uiTextMediumContrast">{room.cancellationText}</Caption>
<Link color="burgundy" href="#" variant="underscored" size="small">
{intl.formatMessage({ id: "Rate details" })}
</Link>
</div>
)
}

View File

@@ -1,11 +1,10 @@
.summary {
background-color: var(--Main-Grey-White);
border: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
border-radius: var(--Corner-radius-Large);
display: flex;
flex-direction: column;
gap: var(--Spacing-x2);
padding: var(--Spacing-x2);
padding: var(--Spacing-x3);
height: 100%;
}
.date {
@@ -31,6 +30,9 @@
justify-content: space-between;
}
.entry > :last-child {
justify-items: flex-end;
}
.total {
display: flex;
flex-direction: column;

View File

@@ -4,7 +4,7 @@ import { useMemo, useState } from "react"
import RateSummary from "./RateSummary"
import RoomCard from "./RoomCard"
import getHotelReservationQueryParams from "./utils"
import { getHotelReservationQueryParams } from "./utils"
import styles from "./roomSelection.module.css"

View File

@@ -2,11 +2,22 @@ import { getFormattedUrlQueryParams } from "@/utils/url"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
function getHotelReservationQueryParams(searchParams: URLSearchParams) {
export function getHotelReservationQueryParams(searchParams: URLSearchParams) {
return getFormattedUrlQueryParams(searchParams, {
adults: "number",
age: "number",
}) as SelectRateSearchParams
}
export default getHotelReservationQueryParams
export function getQueryParamsForEnterDetails(searchParams: URLSearchParams) {
const selectRoomParamsObject = getHotelReservationQueryParams(searchParams)
const { room } = selectRoomParamsObject
return {
...selectRoomParamsObject,
adults: room[0].adults, // TODO: Handle multiple rooms
children: room[0].child?.length.toString(), // TODO: Handle multiple rooms
roomTypeCode: room[0].roomtypecode,
rateCode: room[0].ratecode,
}
}