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

@@ -0,0 +1,77 @@
import {
getHotelData,
getProfileSafely,
getRoomAvailability,
} from "@/lib/trpc/memoizedRequests"
import { HotelIncludeEnum } from "@/server/routers/hotels/input"
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { formatNumber } from "@/utils/format"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { LangParams, PageArgs, SearchParams } from "@/types/params"
export default async function SummaryPage({
params,
searchParams,
}: PageArgs<LangParams, SearchParams<SelectRateSearchParams>>) {
const selectRoomParams = new URLSearchParams(searchParams)
const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } =
getQueryParamsForEnterDetails(selectRoomParams)
const user = await getProfileSafely()
const hotelData = await getHotelData(hotel, params.lang, undefined, [HotelIncludeEnum.RoomCategories])
const availability = await getRoomAvailability({
hotelId: parseInt(hotel),
adults,
children,
roomStayStartDate: fromDate,
roomStayEndDate: toDate,
})
if (!hotelData?.data || !hotelData?.included || !availability) {
console.error("No hotel or availability data", hotelData, availability)
// TODO: handle this case
return null
}
const chosenRoom = availability.roomConfigurations.find(
(availRoom) => availRoom.roomTypeCode === roomTypeCode
)
if (!chosenRoom) {
// TODO: handle this case
console.error("No chosen room", chosenRoom)
return null
}
const cancellationText =
availability?.rateDefinitions.find((rate) => rate.rateCode === rateCode)
?.cancellationText ?? ""
const memberPrice =
chosenRoom.products.find(
(rate) => rate.productType.member?.rateCode === rateCode
)?.productType.member?.localPrice.pricePerStay ?? "0"
const publicPrice =
chosenRoom.products.find(
(rate) => rate.productType.public?.rateCode === rateCode
)?.productType.public?.localPrice.pricePerStay ?? "0"
const price = user ? memberPrice : publicPrice
return (
<Summary
isMember={!!user}
room={{
roomType: chosenRoom.roomType,
price: formatNumber(parseInt(price)),
adults,
cancellationText,
}}
/>
)
}

View File

@@ -1,6 +1,5 @@
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
import { setLang } from "@/i18n/serverContext"
import { preload } from "./page"
@@ -11,6 +10,7 @@ import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import type { LangParams, LayoutArgs } from "@/types/params"
export default async function StepLayout({
summary,
children,
hotelHeader,
params,
@@ -19,6 +19,7 @@ export default async function StepLayout({
LayoutArgs<LangParams & { step: StepEnum }> & {
hotelHeader: React.ReactNode
sidePeek: React.ReactNode
summary: React.ReactNode
}>) {
setLang(params.lang)
preload()
@@ -29,9 +30,7 @@ export default async function StepLayout({
<div className={styles.content}>
<SelectedRoom />
{children}
<aside className={styles.summary}>
<Summary isMember={false} />
</aside>
<aside className={styles.summary}>{summary}</aside>
</div>
{sidePeek}
</main>

View File

@@ -7,6 +7,7 @@ import {
getProfileSafely,
getRoomAvailability,
} from "@/lib/trpc/memoizedRequests"
import { HotelIncludeEnum } from "@/server/routers/hotels/input"
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
@@ -14,11 +15,12 @@ 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 getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { getIntl } from "@/i18n"
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import type { LangParams, PageArgs } from "@/types/params"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { LangParams, PageArgs, } from "@/types/params"
export function preload() {
void getProfileSafely()
@@ -32,35 +34,52 @@ function isValidStep(step: string): step is StepEnum {
export default async function StepPage({
params,
searchParams,
}: PageArgs<LangParams & { step: StepEnum }, { hotel: string }>) {
}: PageArgs<
LangParams & { step: StepEnum },
SelectRateSearchParams
>) {
if (!searchParams.hotel) {
redirect(`/${params.lang}`)
}
void getBreakfastPackages(searchParams.hotel)
const stepParams = new URLSearchParams(searchParams)
const paramsObject = getHotelReservationQueryParams(stepParams)
void getRoomAvailability({
hotelId: paramsObject.hotel,
adults: paramsObject.room[0].adults,
roomStayStartDate: paramsObject.fromDate,
roomStayEndDate: paramsObject.toDate,
})
const intl = await getIntl()
const hotel = await getHotelData(searchParams.hotel, params.lang)
const intl = await getIntl()
const selectRoomParams = new URLSearchParams(searchParams)
const {
hotel: hotelId,
adults,
children,
roomTypeCode,
rateCode,
fromDate,
toDate,
} = getQueryParamsForEnterDetails(selectRoomParams)
void getRoomAvailability({
hotelId: parseInt(hotelId),
adults,
children,
roomStayStartDate: fromDate,
roomStayEndDate: toDate,
rateCode
})
const hotelData = await getHotelData(hotelId, params.lang, undefined, [HotelIncludeEnum.RoomCategories])
const user = await getProfileSafely()
const savedCreditCards = await getCreditCardsSafely()
const breakfastPackages = await getBreakfastPackages(searchParams.hotel)
const roomAvailability = await getRoomAvailability({
hotelId: paramsObject.hotel,
adults: paramsObject.room[0].adults,
roomStayStartDate: paramsObject.fromDate,
roomStayEndDate: paramsObject.toDate,
rateCode: paramsObject.room[0].ratecode,
hotelId: parseInt(hotelId),
adults,
children,
roomStayStartDate: fromDate,
roomStayEndDate: toDate,
rateCode
})
if (!isValidStep(params.step) || !hotel || !roomAvailability) {
if (!isValidStep(params.step) || !hotelData || !roomAvailability) {
return notFound()
}
@@ -79,16 +98,32 @@ export default async function StepPage({
id: "Select payment method",
})
const availableRoom = roomAvailability?.roomConfigurations
.filter((room) => room.status === "Available")
.find((room) => room.roomTypeCode === roomTypeCode)?.roomType
const roomTypes = hotelData.included
?.find((room) => room.name === availableRoom)
?.roomTypes.map((room) => ({
description: room.mainBed.description,
size: room.mainBed.widthRange,
value: room.code,
}))
return (
<section>
<HistoryStateManager />
<SectionAccordion
header={intl.formatMessage({ id: "Select bed" })}
step={StepEnum.selectBed}
label={intl.formatMessage({ id: "Request bedtype" })}
>
<BedType />
</SectionAccordion>
{/* TODO: How to handle no beds found? */}
{roomTypes ? (
<SectionAccordion
header="Select bed"
step={StepEnum.selectBed}
label={intl.formatMessage({ id: "Request bedtype" })}
>
<BedType roomTypes={roomTypes} />
</SectionAccordion>
) : null}
<SectionAccordion
header={intl.formatMessage({ id: "Food options" })}
step={StepEnum.breakfast}
@@ -111,13 +146,13 @@ export default async function StepPage({
<Payment
hotelId={searchParams.hotel}
otherPaymentOptions={
hotel.data.attributes.merchantInformationData
hotelData.data.attributes.merchantInformationData
.alternatePaymentOptions
}
savedCreditCards={savedCreditCards}
mustBeGuaranteed={mustBeGuaranteed}
/>
</SectionAccordion>
</section>
</section >
)
}

View File

@@ -10,7 +10,7 @@ import {
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { ChevronRightIcon } from "@/components/Icons"
import StaticMap from "@/components/Maps/StaticMap"
import Link from "@/components/TempDesignSystem/Link"

View File

@@ -2,10 +2,11 @@ import { notFound } from "next/navigation"
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server"
import { HotelIncludeEnum } from "@/server/routers/hotels/input"
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
import Rooms from "@/components/HotelReservation/SelectRate/Rooms"
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { setLang } from "@/i18n/serverContext"
import { generateChildrenString } from "../select-hotel/utils"
@@ -38,7 +39,7 @@ export default async function SelectRatePage({
serverClient().hotel.hotelData.get({
hotelId: searchParams.hotel,
language: params.lang,
include: ["RoomCategories"],
include: [HotelIncludeEnum.RoomCategories],
}),
serverClient().hotel.availability.rooms({
hotelId: parseInt(searchParams.hotel, 10),

View File

@@ -11,7 +11,6 @@ import { CloseLargeIcon } from "@/components/Icons"
import { debounce } from "@/utils/debounce"
import { getFormattedUrlQueryParams } from "@/utils/url"
import getHotelReservationQueryParams from "../HotelReservation/SelectRate/RoomSelection/utils"
import MobileToggleButton from "./MobileToggleButton"
import styles from "./bookingWidget.module.css"
@@ -41,10 +40,10 @@ export default function BookingWidgetClient({
const bookingWidgetSearchData: BookingWidgetSearchParams | undefined =
searchParams
? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), {
adults: "number",
age: "number",
bed: "number",
}) as BookingWidgetSearchParams)
adults: "number",
age: "number",
bed: "number",
}) as BookingWidgetSearchParams)
: undefined
const getLocationObj = (destination: string): Location | undefined => {
@@ -70,9 +69,9 @@ export default function BookingWidgetClient({
const selectedLocation = bookingWidgetSearchData
? getLocationObj(
(bookingWidgetSearchData.city ??
bookingWidgetSearchData.hotel) as string
)
(bookingWidgetSearchData.city ??
bookingWidgetSearchData.hotel) as string
)
: undefined
const methods = useForm<BookingWidgetSchema>({

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,
}
}

View File

@@ -16,7 +16,6 @@ const config = {
textHighContrast: styles.textHighContrast,
white: styles.white,
peach50: styles.peach50,
baseTextMediumContrast: styles.baseTextMediumContrast,
uiTextHighContrast: styles.uiTextHighContrast,
uiTextMediumContrast: styles.uiTextMediumContrast,
uiTextPlaceholder: styles.uiTextPlaceholder,

View File

@@ -1,5 +1,7 @@
{
"<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)",
"<b>Total price</b> (incl VAT)": "<b>Samlet pris</b> (inkl. moms)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/nat pr. voksen",
"A destination or hotel name is needed to be able to search for a hotel room.": "Et destinations- eller hotelnavn er nødvendigt for at kunne søge efter et hotelværelse.",
"A photo of the room": "Et foto af værelset",
"ACCE": "Tilgængelighed",
@@ -35,6 +37,7 @@
"Attractions": "Attraktioner",
"Back to scandichotels.com": "Tilbage til scandichotels.com",
"Bar": "Bar",
"Based on availability": "Baseret på tilgængelighed",
"Bed type": "Seng type",
"Birth date": "Fødselsdato",
"Book": "Book",
@@ -242,12 +245,14 @@
"Points needed to stay on level": "Point nødvendige for at holde sig på niveau",
"Previous": "Forudgående",
"Previous victories": "Tidligere sejre",
"Price details": "Prisoplysninger",
"Proceed to login": "Fortsæt til login",
"Proceed to payment method": "Fortsæt til betalingsmetode",
"Provide a payment card in the next step": "Giv os dine betalingsoplysninger i næste skridt",
"Public price from": "Offentlig pris fra",
"Public transport": "Offentlig transport",
"Queen bed": "Queensize-seng",
"Rate details": "Oplysninger om værelsespris",
"Read more": "Læs mere",
"Read more & book a table": "Read more & book a table",
"Read more about the hotel": "Læs mere om hotellet",
@@ -405,6 +410,5 @@
"uppercase letter": "stort bogstav",
"{amount} out of {total}": "{amount} ud af {total}",
"{amount} {currency}": "{amount} {currency}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
"{width} cm × {length} cm": "{width} cm × {length} cm"
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
}

View File

@@ -1,5 +1,7 @@
{
"<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)",
"<b>Total price</b> (incl VAT)": "<b>Gesamtpreis</b> (inkl. MwSt.)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/Nacht pro Erwachsener",
"A destination or hotel name is needed to be able to search for a hotel room.": "Ein Reiseziel oder Hotelname wird benötigt, um nach einem Hotelzimmer suchen zu können.",
"A photo of the room": "Ein Foto des Zimmers",
"ACCE": "Zugänglichkeit",
@@ -35,6 +37,7 @@
"Attraction": "Attraktion",
"Back to scandichotels.com": "Zurück zu scandichotels.com",
"Bar": "Bar",
"Based on availability": "Je nach Verfügbarkeit",
"Bed type": "Bettentyp",
"Birth date": "Geburtsdatum",
"Book": "Buchen",
@@ -240,12 +243,14 @@
"Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben",
"Previous": "Früher",
"Previous victories": "Bisherige Siege",
"Price details": "Preisdetails",
"Proceed to login": "Weiter zum Login",
"Proceed to payment method": "Weiter zur Zahlungsmethode",
"Provide a payment card in the next step": "Geben Sie Ihre Zahlungskarteninformationen im nächsten Schritt an",
"Public price from": "Öffentlicher Preis ab",
"Public transport": "Öffentliche Verkehrsmittel",
"Queen bed": "Queensize-Bett",
"Rate details": "Preisdetails",
"Read more": "Mehr lesen",
"Read more & book a table": "Read more & book a table",
"Read more about the hotel": "Lesen Sie mehr über das Hotel",
@@ -404,6 +409,5 @@
"uppercase letter": "großbuchstabe",
"{amount} out of {total}": "{amount} von {total}",
"{amount} {currency}": "{amount} {currency}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
"{width} cm × {length} cm": "{width} cm × {length} cm"
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
}

View File

@@ -1,5 +1,7 @@
{
"<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)",
"<b>Total price</b> (incl VAT)": "<b>Total price</b> (incl VAT)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/night per adult",
"A destination or hotel name is needed to be able to search for a hotel room.": "A destination or hotel name is needed to be able to search for a hotel room.",
"A photo of the room": "A photo of the room",
"ACCE": "Accessibility",
@@ -38,6 +40,7 @@
"Attractions": "Attractions",
"Back to scandichotels.com": "Back to scandichotels.com",
"Bar": "Bar",
"Based on availability": "Based on availability",
"Bed": "Bed",
"Bed type": "Bed type",
"Birth date": "Birth date",
@@ -252,6 +255,7 @@
"Points needed to stay on level": "Points needed to stay on level",
"Previous": "Previous",
"Previous victories": "Previous victories",
"Price details": "Price details",
"Print confirmation": "Print confirmation",
"Proceed to login": "Proceed to login",
"Proceed to payment method": "Proceed to payment method",
@@ -259,6 +263,7 @@
"Public price from": "Public price from",
"Public transport": "Public transport",
"Queen bed": "Queen bed",
"Rate details": "Rate details",
"Read more": "Read more",
"Read more & book a table": "Read more & book a table",
"Read more about the hotel": "Read more about the hotel",
@@ -329,7 +334,6 @@
"Total cost": "Total cost",
"Total price": "Total price",
"Total Points": "Total Points",
"Total price (incl VAT)": "Total price (incl VAT)",
"Tourist": "Tourist",
"Transaction date": "Transaction date",
"Transactions": "Transactions",
@@ -427,6 +431,5 @@
"{amount} out of {total}": "{amount} out of {total}",
"{amount} {currency}": "{amount} {currency}",
"{card} ending with {cardno}": "{card} ending with {cardno}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
"{width} cm × {length} cm": "{width} cm × {length} cm"
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
}

View File

@@ -1,5 +1,7 @@
{
"<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)",
"<b>Total price</b> (incl VAT)": "<b>Kokonaishinta</b> (sis. ALV)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/yö per aikuinen",
"A destination or hotel name is needed to be able to search for a hotel room.": "Kohteen tai hotellin nimi tarvitaan, jotta hotellihuonetta voidaan hakea.",
"A photo of the room": "Kuva huoneesta",
"ACCE": "Saavutettavuus",
@@ -35,6 +37,7 @@
"Attractions": "Nähtävyydet",
"Back to scandichotels.com": "Takaisin scandichotels.com",
"Bar": "Bar",
"Based on availability": "Saatavuuden mukaan",
"Bed type": "Vuodetyyppi",
"Birth date": "Syntymäaika",
"Book": "Varaa",
@@ -242,12 +245,14 @@
"Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet",
"Previous": "Aikaisempi",
"Previous victories": "Edelliset voitot",
"Price details": "Hintatiedot",
"Proceed to login": "Jatka kirjautumiseen",
"Proceed to payment method": "Siirry maksutavalle",
"Provide a payment card in the next step": "Anna maksukortin tiedot seuraavassa vaiheessa",
"Public price from": "Julkinen hinta alkaen",
"Public transport": "Julkinen liikenne",
"Queen bed": "Queen-vuode",
"Rate details": "Hintatiedot",
"Read more": "Lue lisää",
"Read more & book a table": "Read more & book a table",
"Read more about the hotel": "Lue lisää hotellista",
@@ -404,6 +409,5 @@
"uppercase letter": "iso kirjain",
"{amount} out of {total}": "{amount}/{total}",
"{amount} {currency}": "{amount} {currency}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
"{width} cm × {length} cm": "{width} cm × {length} cm"
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
}

View File

@@ -1,5 +1,7 @@
{
"<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)",
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl. mva)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/natt per voksen",
"A destination or hotel name is needed to be able to search for a hotel room.": "Et reisemål eller hotellnavn er nødvendig for å kunne søke etter et hotellrom.",
"A photo of the room": "Et bilde av rommet",
"ACCE": "Tilgjengelighet",
@@ -35,6 +37,7 @@
"Attractions": "Attraksjoner",
"Back to scandichotels.com": "Tilbake til scandichotels.com",
"Bar": "Bar",
"Based on availability": "Basert på tilgjengelighet",
"Bed type": "Seng type",
"Birth date": "Fødselsdato",
"Book": "Bestill",
@@ -240,12 +243,14 @@
"Points needed to stay on level": "Poeng som trengs for å holde seg på nivå",
"Previous": "Tidligere",
"Previous victories": "Tidligere seire",
"Price details": "Prisdetaljer",
"Proceed to login": "Fortsett til innlogging",
"Proceed to payment method": "Fortsett til betalingsmetode",
"Provide a payment card in the next step": "Gi oss dine betalingskortdetaljer i neste steg",
"Public price from": "Offentlig pris fra",
"Public transport": "Offentlig transport",
"Queen bed": "Queen-size-seng",
"Rate details": "Prisdetaljer",
"Read more": "Les mer",
"Read more & book a table": "Read more & book a table",
"Read more about the hotel": "Les mer om hotellet",
@@ -402,6 +407,5 @@
"uppercase letter": "stor bokstav",
"{amount} out of {total}": "{amount} av {total}",
"{amount} {currency}": "{amount} {currency}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
"{width} cm × {length} cm": "{width} cm × {length} cm"
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
}

View File

@@ -1,5 +1,7 @@
{
"<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)",
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl moms)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/natt per vuxen",
"A destination or hotel name is needed to be able to search for a hotel room.": "Ett destinations- eller hotellnamn behövs för att kunna söka efter ett hotellrum.",
"A photo of the room": "Ett foto av rummet",
"ACCE": "Tillgänglighet",
@@ -35,6 +37,7 @@
"Attractions": "Sevärdheter",
"Back to scandichotels.com": "Tillbaka till scandichotels.com",
"Bar": "Bar",
"Based on availability": "Baserat på tillgänglighet",
"Bed type": "Sängtyp",
"Birth date": "Födelsedatum",
"Book": "Boka",
@@ -240,12 +243,14 @@
"Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå",
"Previous": "Föregående",
"Previous victories": "Tidigare segrar",
"Price details": "Prisdetaljer",
"Proceed to login": "Fortsätt till inloggning",
"Proceed to payment method": "Gå vidare till betalningsmetod",
"Provide a payment card in the next step": "Ge oss dina betalkortdetaljer i nästa steg",
"Public price from": "Offentligt pris från",
"Public transport": "Kollektivtrafik",
"Queen bed": "Queen size-säng",
"Rate details": "Detaljer om rumspriset",
"Read more": "Läs mer",
"Read more & book a table": "Read more & book a table",
"Read more about the hotel": "Läs mer om hotellet",
@@ -405,6 +410,5 @@
"paying": "betalar",
"uppercase letter": "stor bokstav",
"{amount} {currency}": "{amount} {currency}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
"{width} cm × {length} cm": "{width} cm × {length} cm"
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
}

View File

@@ -1,6 +1,10 @@
import { cache } from "react"
import { Lang } from "@/constants/languages"
import {
GetRoomsAvailabilityInput,
HotelIncludeEnum,
} from "@/server/routers/hotels/input"
import { serverClient } from "../server"
@@ -53,12 +57,14 @@ export const getUserTracking = cache(async function getMemoizedUserTracking() {
export const getHotelData = cache(async function getMemoizedHotelData(
hotelId: string,
language: string,
isCardOnlyPayment?: boolean
isCardOnlyPayment?: boolean,
include?: HotelIncludeEnum[]
) {
return serverClient().hotel.hotelData.get({
hotelId,
language,
isCardOnlyPayment,
include,
})
})
@@ -71,17 +77,9 @@ export const getRoomAvailability = cache(
children,
promotionCode,
rateCode,
}: {
hotelId: string
adults: number
roomStayStartDate: string
roomStayEndDate: string
children?: string
promotionCode?: string
rateCode?: string
}) {
}: GetRoomsAvailabilityInput) {
return serverClient().hotel.availability.rooms({
hotelId: parseInt(hotelId),
hotelId,
adults,
roomStayStartDate,
roomStayEndDate,

View File

@@ -29,17 +29,26 @@ export const getRoomsAvailabilityInputSchema = z.object({
rateCode: z.string().optional(),
})
export type GetRoomsAvailabilityInput = z.input<
typeof getRoomsAvailabilityInputSchema
>
export const getRatesInputSchema = z.object({
hotelId: z.string(),
})
export const getlHotelDataInputSchema = z.object({
export enum HotelIncludeEnum {
"RoomCategories",
"NearbyHotels",
"Restaurants",
"City",
}
export const getHotelDataInputSchema = z.object({
hotelId: z.string(),
language: z.string(),
isCardOnlyPayment: z.boolean().optional(),
include: z
.array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"]))
.optional(),
include: z.array(z.nativeEnum(HotelIncludeEnum)).optional(),
})
export const getBreakfastPackageInput = z.object({

View File

@@ -32,9 +32,9 @@ import {
} from "./schemas/packages"
import {
getBreakfastPackageInput,
getHotelDataInputSchema,
getHotelInputSchema,
getHotelsAvailabilityInputSchema,
getlHotelDataInputSchema,
getRatesInputSchema,
getRoomsAvailabilityInputSchema,
} from "./input"
@@ -584,7 +584,7 @@ export const hotelQueryRouter = router({
}),
hotelData: router({
get: serviceProcedure
.input(getlHotelDataInputSchema)
.input(getHotelDataInputSchema)
.query(async ({ ctx, input }) => {
const { hotelId, language, include, isCardOnlyPayment } = input

View File

@@ -87,8 +87,11 @@ export const roomSchema = z
name: data.attributes.name,
occupancy: data.attributes.occupancy,
roomSize: data.attributes.roomSize,
roomTypes: data.attributes.roomTypes,
sortOrder: data.attributes.sortOrder,
type: data.type,
roomFacilities: data.attributes.roomFacilities,
}
})
export type RoomType = Pick<z.output<typeof roomSchema>, "roomTypes" | "name">

View File

@@ -6,21 +6,20 @@ import { create, useStore } from "zustand"
import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema"
import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema"
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import type { 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 { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import { BedTypeEnum } from "@/types/enums/bedType"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData"
import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
const SESSION_STORAGE_KEY = "enterDetails"
interface EnterDetailsState {
userData: {
bedType: BedTypeEnum | undefined
bedType: string | undefined
breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined
} & DetailsSchema
roomData: BookingData

View File

@@ -5,23 +5,21 @@ interface Child {
interface Room {
adults: number
roomtypecode: string
ratecode: string
child: Child[]
roomtypecode?: string
ratecode?: string
child?: Child[]
}
export interface BookingData {
hotel: string
fromdate: string
todate: string
fromDate: string
toDate: string
room: Room[]
}
export type RoomsData = {
roomType: string
memberPrice: string | undefined
publicPrice: string | undefined
price: string
adults: number
children: Child[]
cancellationText: string | undefined
children?: Child[]
cancellationText: string
}

View File

@@ -7,7 +7,7 @@ export interface Child {
interface Room {
adults: number
roomcode?: string
roomtypecode?: string
ratecode?: string
child?: Child[]
}