Merged in feat/sw-610-summary (pull request #774)
Feat/SW-610 summary Approved-by: Tobias Johansson Approved-by: Simon.Emanuelsson
This commit is contained in:
@@ -14,7 +14,10 @@ export default async function HotelHeader({
|
|||||||
if (!searchParams.hotel) {
|
if (!searchParams.hotel) {
|
||||||
redirect(home)
|
redirect(home)
|
||||||
}
|
}
|
||||||
const hotel = await getHotelData(searchParams.hotel, params.lang)
|
const hotel = await getHotelData({
|
||||||
|
hotelId: searchParams.hotel,
|
||||||
|
language: params.lang,
|
||||||
|
})
|
||||||
if (!hotel?.data) {
|
if (!hotel?.data) {
|
||||||
redirect(home)
|
redirect(home)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ export default async function HotelSidePeek({
|
|||||||
if (!searchParams.hotel) {
|
if (!searchParams.hotel) {
|
||||||
redirect(`/${params.lang}`)
|
redirect(`/${params.lang}`)
|
||||||
}
|
}
|
||||||
const hotel = await getHotelData(searchParams.hotel, params.lang)
|
const hotel = await getHotelData({
|
||||||
|
hotelId: searchParams.hotel,
|
||||||
|
language: params.lang,
|
||||||
|
})
|
||||||
if (!hotel?.data) {
|
if (!hotel?.data) {
|
||||||
redirect(`/${params.lang}`)
|
redirect(`/${params.lang}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
|
import {
|
||||||
|
getProfileSafely,
|
||||||
|
getSelectedRoomAvailability,
|
||||||
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
|
||||||
|
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
if (!roomTypeCode || !rateCode) {
|
||||||
|
console.log("No roomTypeCode or rateCode")
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const availability = await getSelectedRoomAvailability({
|
||||||
|
hotelId: parseInt(hotel),
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
roomStayStartDate: fromDate,
|
||||||
|
roomStayEndDate: toDate,
|
||||||
|
rateCode,
|
||||||
|
roomTypeCode,
|
||||||
|
})
|
||||||
|
const user = await getProfileSafely()
|
||||||
|
|
||||||
|
if (!availability) {
|
||||||
|
console.error("No hotel or availability data", availability)
|
||||||
|
// TODO: handle this case
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const prices = user
|
||||||
|
? {
|
||||||
|
local: {
|
||||||
|
price: availability.memberRate?.localPrice.pricePerStay,
|
||||||
|
currency: availability.memberRate?.localPrice.currency,
|
||||||
|
},
|
||||||
|
euro: {
|
||||||
|
price: availability.memberRate?.requestedPrice?.pricePerStay,
|
||||||
|
currency: availability.memberRate?.requestedPrice?.currency,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
local: {
|
||||||
|
price: availability.publicRate?.localPrice.pricePerStay,
|
||||||
|
currency: availability.publicRate?.localPrice.currency,
|
||||||
|
},
|
||||||
|
euro: {
|
||||||
|
price: availability.publicRate?.requestedPrice?.pricePerStay,
|
||||||
|
currency: availability.publicRate?.requestedPrice?.currency,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Summary
|
||||||
|
isMember={!!user}
|
||||||
|
room={{
|
||||||
|
roomType: availability.selectedRoom.roomType,
|
||||||
|
localPrice: prices.local,
|
||||||
|
euroPrice: prices.euro,
|
||||||
|
adults,
|
||||||
|
cancellationText: availability.cancellationText,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
.layout {
|
.layout {
|
||||||
min-height: 100dvh;
|
|
||||||
background-color: var(--Scandic-Brand-Warm-White);
|
background-color: var(--Scandic-Brand-Warm-White);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9,7 +8,6 @@
|
|||||||
grid-template-columns: 1fr 340px;
|
grid-template-columns: 1fr 340px;
|
||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
margin: var(--Spacing-x5) auto 0;
|
margin: var(--Spacing-x5) auto 0;
|
||||||
padding-top: var(--Spacing-x6);
|
|
||||||
/* simulates padding on viewport smaller than --max-width-navigation */
|
/* simulates padding on viewport smaller than --max-width-navigation */
|
||||||
width: min(
|
width: min(
|
||||||
calc(100dvw - (var(--Spacing-x2) * 2)),
|
calc(100dvw - (var(--Spacing-x2) * 2)),
|
||||||
@@ -17,8 +15,81 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summaryContainer {
|
||||||
align-self: flex-start;
|
|
||||||
grid-column: 2 / 3;
|
grid-column: 2 / 3;
|
||||||
grid-row: 1/-1;
|
grid-row: 1/-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
background-color: var(--Main-Grey-White);
|
||||||
|
|
||||||
|
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
border-radius: var(--Corner-radius-Large);
|
||||||
|
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hider {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 950px) {
|
||||||
|
.summaryContainer {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto 1fr;
|
||||||
|
margin-top: calc(0px - var(--Spacing-x9));
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary {
|
||||||
|
position: sticky;
|
||||||
|
top: calc(
|
||||||
|
var(--booking-widget-desktop-height) +
|
||||||
|
var(--booking-widget-desktop-height) + var(--Spacing-x-one-and-half)
|
||||||
|
);
|
||||||
|
margin-top: calc(0px - var(--Spacing-x9));
|
||||||
|
border-bottom: none;
|
||||||
|
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hider {
|
||||||
|
display: block;
|
||||||
|
background-color: var(--Scandic-Brand-Warm-White);
|
||||||
|
position: sticky;
|
||||||
|
margin-top: var(--Spacing-x4);
|
||||||
|
top: calc(
|
||||||
|
var(--booking-widget-desktop-height) +
|
||||||
|
var(--booking-widget-desktop-height) - 6px
|
||||||
|
);
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow {
|
||||||
|
display: block;
|
||||||
|
background-color: var(--Main-Grey-White);
|
||||||
|
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
|
||||||
|
border-style: solid;
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right-width: 1px;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1367px) {
|
||||||
|
.summary {
|
||||||
|
top: calc(
|
||||||
|
var(--booking-widget-desktop-height) + var(--Spacing-x2) +
|
||||||
|
var(--Spacing-x-half)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hider {
|
||||||
|
top: calc(var(--booking-widget-desktop-height) - 6px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
|
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
|
||||||
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
|
||||||
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
|
|
||||||
import { setLang } from "@/i18n/serverContext"
|
import { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import { preload } from "./page"
|
import { preload } from "./page"
|
||||||
|
|
||||||
import styles from "./layout.module.css"
|
import styles from "./layout.module.css"
|
||||||
|
|
||||||
import { StepEnum } from "@/types/components/enterDetails/step"
|
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
||||||
import type { LangParams, LayoutArgs } from "@/types/params"
|
import type { LangParams, LayoutArgs } from "@/types/params"
|
||||||
|
|
||||||
export default async function StepLayout({
|
export default async function StepLayout({
|
||||||
|
summary,
|
||||||
children,
|
children,
|
||||||
hotelHeader,
|
hotelHeader,
|
||||||
params,
|
params,
|
||||||
@@ -19,19 +19,21 @@ export default async function StepLayout({
|
|||||||
LayoutArgs<LangParams & { step: StepEnum }> & {
|
LayoutArgs<LangParams & { step: StepEnum }> & {
|
||||||
hotelHeader: React.ReactNode
|
hotelHeader: React.ReactNode
|
||||||
sidePeek: React.ReactNode
|
sidePeek: React.ReactNode
|
||||||
}
|
summary: React.ReactNode
|
||||||
>) {
|
}>) {
|
||||||
setLang(params.lang)
|
setLang(params.lang)
|
||||||
preload()
|
preload()
|
||||||
return (
|
return (
|
||||||
<EnterDetailsProvider step={params.step}>
|
<EnterDetailsProvider step={params.step} >
|
||||||
<main className={styles.layout}>
|
<main className={styles.layout}>
|
||||||
{hotelHeader}
|
{hotelHeader}
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<SelectedRoom />
|
<SelectedRoom />
|
||||||
{children}
|
{children}
|
||||||
<aside className={styles.summary}>
|
<aside className={styles.summaryContainer}>
|
||||||
<Summary />
|
<div className={styles.hider} />
|
||||||
|
<div className={styles.summary}>{summary}</div>
|
||||||
|
<div className={styles.shadow} />
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
{sidePeek}
|
{sidePeek}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import {
|
|||||||
getHotelData,
|
getHotelData,
|
||||||
getProfileSafely,
|
getProfileSafely,
|
||||||
getRoomAvailability,
|
getRoomAvailability,
|
||||||
|
getSelectedRoomAvailability,
|
||||||
} from "@/lib/trpc/memoizedRequests"
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
import { HotelIncludeEnum } from "@/server/routers/hotels/input"
|
||||||
|
|
||||||
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
|
import BedType from "@/components/HotelReservation/EnterDetails/BedType"
|
||||||
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
|
||||||
@@ -14,10 +16,11 @@ 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 getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
import { StepEnum } from "@/types/components/enterDetails/step"
|
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
||||||
|
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() {
|
export function preload() {
|
||||||
@@ -32,35 +35,56 @@ function isValidStep(step: string): step is StepEnum {
|
|||||||
export default async function StepPage({
|
export default async function StepPage({
|
||||||
params,
|
params,
|
||||||
searchParams,
|
searchParams,
|
||||||
}: PageArgs<LangParams & { step: StepEnum }, { hotel: string }>) {
|
}: PageArgs<LangParams & { step: StepEnum }, SelectRateSearchParams>) {
|
||||||
if (!searchParams.hotel) {
|
const { lang } = params
|
||||||
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)
|
void getBreakfastPackages(searchParams.hotel)
|
||||||
|
|
||||||
|
const intl = await getIntl()
|
||||||
|
const selectRoomParams = new URLSearchParams(searchParams)
|
||||||
|
const {
|
||||||
|
hotel: hotelId,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
roomTypeCode,
|
||||||
|
rateCode,
|
||||||
|
fromDate,
|
||||||
|
toDate,
|
||||||
|
} = getQueryParamsForEnterDetails(selectRoomParams)
|
||||||
|
|
||||||
|
if (!rateCode || !roomTypeCode) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
void getSelectedRoomAvailability({
|
||||||
|
hotelId: parseInt(searchParams.hotel),
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
roomStayStartDate: fromDate,
|
||||||
|
roomStayEndDate: toDate,
|
||||||
|
rateCode,
|
||||||
|
roomTypeCode,
|
||||||
|
})
|
||||||
|
|
||||||
|
const hotelData = await getHotelData({
|
||||||
|
hotelId,
|
||||||
|
language: lang,
|
||||||
|
include: [HotelIncludeEnum.RoomCategories],
|
||||||
|
})
|
||||||
|
const roomAvailability = await getSelectedRoomAvailability({
|
||||||
|
hotelId: parseInt(searchParams.hotel),
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
roomStayStartDate: fromDate,
|
||||||
|
roomStayEndDate: toDate,
|
||||||
|
rateCode,
|
||||||
|
roomTypeCode,
|
||||||
|
})
|
||||||
|
const breakfastPackages = await getBreakfastPackages(searchParams.hotel)
|
||||||
const user = await getProfileSafely()
|
const user = await getProfileSafely()
|
||||||
const savedCreditCards = await getCreditCardsSafely()
|
const savedCreditCards = await getCreditCardsSafely()
|
||||||
const breakfastPackages = await getBreakfastPackages(searchParams.hotel)
|
|
||||||
|
|
||||||
const roomAvailability = await getRoomAvailability({
|
if (!isValidStep(params.step) || !hotelData || !roomAvailability) {
|
||||||
hotelId: paramsObject.hotel,
|
|
||||||
adults: paramsObject.room[0].adults,
|
|
||||||
roomStayStartDate: paramsObject.fromDate,
|
|
||||||
roomStayEndDate: paramsObject.toDate,
|
|
||||||
rateCode: paramsObject.room[0].ratecode,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!isValidStep(params.step) || !hotel || !roomAvailability) {
|
|
||||||
return notFound()
|
return notFound()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,16 +103,30 @@ export default async function StepPage({
|
|||||||
id: "Select payment method",
|
id: "Select payment method",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const availableRoom = roomAvailability.selectedRoom?.roomType
|
||||||
|
const bedTypes = hotelData.included
|
||||||
|
?.find((room) => room.name === availableRoom)
|
||||||
|
?.roomTypes.map((room) => ({
|
||||||
|
description: room.mainBed.description,
|
||||||
|
size: room.mainBed.widthRange,
|
||||||
|
value: room.code,
|
||||||
|
}))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<HistoryStateManager />
|
<HistoryStateManager />
|
||||||
<SectionAccordion
|
|
||||||
header={intl.formatMessage({ id: "Select bed" })}
|
{/* TODO: How to handle no beds found? */}
|
||||||
step={StepEnum.selectBed}
|
{bedTypes ? (
|
||||||
label={intl.formatMessage({ id: "Request bedtype" })}
|
<SectionAccordion
|
||||||
>
|
header="Select bed"
|
||||||
<BedType />
|
step={StepEnum.selectBed}
|
||||||
</SectionAccordion>
|
label={intl.formatMessage({ id: "Request bedtype" })}
|
||||||
|
>
|
||||||
|
<BedType bedTypes={bedTypes} />
|
||||||
|
</SectionAccordion>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<SectionAccordion
|
<SectionAccordion
|
||||||
header={intl.formatMessage({ id: "Food options" })}
|
header={intl.formatMessage({ id: "Food options" })}
|
||||||
step={StepEnum.breakfast}
|
step={StepEnum.breakfast}
|
||||||
@@ -111,7 +149,7 @@ export default async function StepPage({
|
|||||||
<Payment
|
<Payment
|
||||||
hotelId={searchParams.hotel}
|
hotelId={searchParams.hotel}
|
||||||
otherPaymentOptions={
|
otherPaymentOptions={
|
||||||
hotel.data.attributes.merchantInformationData
|
hotelData.data.attributes.merchantInformationData
|
||||||
.alternatePaymentOptions
|
.alternatePaymentOptions
|
||||||
}
|
}
|
||||||
savedCreditCards={savedCreditCards}
|
savedCreditCards={savedCreditCards}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
.layout {
|
.layout {
|
||||||
min-height: 100dvh;
|
|
||||||
background-color: var(--Base-Background-Primary-Normal);
|
background-color: var(--Base-Background-Primary-Normal);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
|
||||||
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
|
||||||
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
|
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 { ChevronRightIcon } from "@/components/Icons"
|
||||||
import StaticMap from "@/components/Maps/StaticMap"
|
import StaticMap from "@/components/Maps/StaticMap"
|
||||||
import Link from "@/components/TempDesignSystem/Link"
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { notFound } from "next/navigation"
|
|||||||
|
|
||||||
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
||||||
import { serverClient } from "@/lib/trpc/server"
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
import { HotelIncludeEnum } from "@/server/routers/hotels/input"
|
||||||
|
|
||||||
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
|
||||||
import Rooms from "@/components/HotelReservation/SelectRate/Rooms"
|
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 { setLang } from "@/i18n/serverContext"
|
||||||
|
|
||||||
import { generateChildrenString } from "../select-hotel/utils"
|
import { generateChildrenString } from "../select-hotel/utils"
|
||||||
@@ -38,7 +39,7 @@ export default async function SelectRatePage({
|
|||||||
serverClient().hotel.hotelData.get({
|
serverClient().hotel.hotelData.get({
|
||||||
hotelId: searchParams.hotel,
|
hotelId: searchParams.hotel,
|
||||||
language: params.lang,
|
language: params.lang,
|
||||||
include: ["RoomCategories"],
|
include: [HotelIncludeEnum.RoomCategories],
|
||||||
}),
|
}),
|
||||||
serverClient().hotel.availability.rooms({
|
serverClient().hotel.availability.rooms({
|
||||||
hotelId: parseInt(searchParams.hotel, 10),
|
hotelId: parseInt(searchParams.hotel, 10),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { dt } from "@/lib/dt"
|
|||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
import { formatNumber } from "@/utils/format"
|
||||||
import { getMembership } from "@/utils/user"
|
import { getMembership } from "@/utils/user"
|
||||||
|
|
||||||
import type { UserProps } from "@/types/components/myPages/user"
|
import type { UserProps } from "@/types/components/myPages/user"
|
||||||
@@ -16,9 +17,6 @@ export default async function ExpiringPoints({ user }: UserProps) {
|
|||||||
// TODO: handle this case?
|
// TODO: handle this case?
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
// sv hardcoded to force space on thousands
|
|
||||||
const formatter = new Intl.NumberFormat(Lang.sv)
|
|
||||||
const d = dt(membership.pointsExpiryDate)
|
const d = dt(membership.pointsExpiryDate)
|
||||||
|
|
||||||
const dateFormat = getLang() == Lang.fi ? "DD.MM.YYYY" : "YYYY-MM-DD"
|
const dateFormat = getLang() == Lang.fi ? "DD.MM.YYYY" : "YYYY-MM-DD"
|
||||||
@@ -29,7 +27,7 @@ export default async function ExpiringPoints({ user }: UserProps) {
|
|||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "spendable points expiring by" },
|
{ id: "spendable points expiring by" },
|
||||||
{
|
{
|
||||||
points: formatter.format(membership.pointsToExpire),
|
points: formatNumber(membership.pointsToExpire),
|
||||||
date: d.format(dateFormat),
|
date: d.format(dateFormat),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
|
||||||
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import { formatNumber } from "@/utils/format"
|
||||||
|
|
||||||
import { awardPointsVariants } from "./awardPointsVariants"
|
import { awardPointsVariants } from "./awardPointsVariants"
|
||||||
|
|
||||||
@@ -32,12 +31,10 @@ export default function AwardPoints({
|
|||||||
variant,
|
variant,
|
||||||
})
|
})
|
||||||
|
|
||||||
// sv hardcoded to force space on thousands
|
|
||||||
const formatter = new Intl.NumberFormat(Lang.sv)
|
|
||||||
return (
|
return (
|
||||||
<Body textTransform="bold" className={classNames}>
|
<Body textTransform="bold" className={classNames}>
|
||||||
{isCalculated
|
{isCalculated
|
||||||
? formatter.format(awardPoints)
|
? formatNumber(awardPoints)
|
||||||
: intl.formatMessage({ id: "Points being calculated" })}
|
: intl.formatMessage({ id: "Points being calculated" })}
|
||||||
</Body>
|
</Body>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { CloseLargeIcon } from "@/components/Icons"
|
|||||||
import { debounce } from "@/utils/debounce"
|
import { debounce } from "@/utils/debounce"
|
||||||
import { getFormattedUrlQueryParams } from "@/utils/url"
|
import { getFormattedUrlQueryParams } from "@/utils/url"
|
||||||
|
|
||||||
import getHotelReservationQueryParams from "../HotelReservation/SelectRate/RoomSelection/utils"
|
|
||||||
import MobileToggleButton from "./MobileToggleButton"
|
import MobileToggleButton from "./MobileToggleButton"
|
||||||
|
|
||||||
import styles from "./bookingWidget.module.css"
|
import styles from "./bookingWidget.module.css"
|
||||||
@@ -41,10 +40,10 @@ export default function BookingWidgetClient({
|
|||||||
const bookingWidgetSearchData: BookingWidgetSearchParams | undefined =
|
const bookingWidgetSearchData: BookingWidgetSearchParams | undefined =
|
||||||
searchParams
|
searchParams
|
||||||
? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), {
|
? (getFormattedUrlQueryParams(new URLSearchParams(searchParams), {
|
||||||
adults: "number",
|
adults: "number",
|
||||||
age: "number",
|
age: "number",
|
||||||
bed: "number",
|
bed: "number",
|
||||||
}) as BookingWidgetSearchParams)
|
}) as BookingWidgetSearchParams)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const getLocationObj = (destination: string): Location | undefined => {
|
const getLocationObj = (destination: string): Location | undefined => {
|
||||||
@@ -70,9 +69,9 @@ export default function BookingWidgetClient({
|
|||||||
|
|
||||||
const selectedLocation = bookingWidgetSearchData
|
const selectedLocation = bookingWidgetSearchData
|
||||||
? getLocationObj(
|
? getLocationObj(
|
||||||
(bookingWidgetSearchData.city ??
|
(bookingWidgetSearchData.city ??
|
||||||
bookingWidgetSearchData.hotel) as string
|
bookingWidgetSearchData.hotel) as string
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const methods = useForm<BookingWidgetSchema>({
|
const methods = useForm<BookingWidgetSchema>({
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export default function FooterSecondaryNav({
|
|||||||
<div className={styles.secondaryNavigation}>
|
<div className={styles.secondaryNavigation}>
|
||||||
{appDownloads && (
|
{appDownloads && (
|
||||||
<nav className={styles.secondaryNavigationGroup}>
|
<nav className={styles.secondaryNavigationGroup}>
|
||||||
<Body color="peach80" textTransform="uppercase">
|
<Body color="baseTextMediumContrast" textTransform="uppercase">
|
||||||
{appDownloads.title}
|
{appDownloads.title}
|
||||||
</Body>
|
</Body>
|
||||||
<ul className={styles.secondaryNavigationList}>
|
<ul className={styles.secondaryNavigationList}>
|
||||||
@@ -50,7 +50,7 @@ export default function FooterSecondaryNav({
|
|||||||
)}
|
)}
|
||||||
{secondaryLinks.map((link) => (
|
{secondaryLinks.map((link) => (
|
||||||
<nav className={styles.secondaryNavigationGroup} key={link.title}>
|
<nav className={styles.secondaryNavigationGroup} key={link.title}>
|
||||||
<Body color="peach80" textTransform="uppercase">
|
<Body color="baseTextMediumContrast" textTransform="uppercase">
|
||||||
{link.title}
|
{link.title}
|
||||||
</Body>
|
</Body>
|
||||||
<ul className={styles.secondaryNavigationList}>
|
<ul className={styles.secondaryNavigationList}>
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ import { bedTypeSchema } from "./schema"
|
|||||||
|
|
||||||
import styles from "./bedOptions.module.css"
|
import styles from "./bedOptions.module.css"
|
||||||
|
|
||||||
import type { BedTypeSchema } from "@/types/components/enterDetails/bedType"
|
import type {
|
||||||
import { BedTypeEnum } from "@/types/enums/bedType"
|
BedTypeProps,
|
||||||
|
BedTypeSchema,
|
||||||
|
} from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||||
|
|
||||||
export default function BedType() {
|
export default function BedType({ bedTypes }: BedTypeProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const bedType = useEnterDetailsStore((state) => state.data.bedType)
|
const bedType = useEnterDetailsStore((state) => state.userData.bedType)
|
||||||
|
|
||||||
const methods = useForm<BedTypeSchema>({
|
const methods = useForm<BedTypeSchema>({
|
||||||
defaultValues: bedType
|
defaultValues: bedType
|
||||||
@@ -33,10 +35,6 @@ export default function BedType() {
|
|||||||
reValidateMode: "onChange",
|
reValidateMode: "onChange",
|
||||||
})
|
})
|
||||||
|
|
||||||
const text = intl.formatMessage<React.ReactNode>(
|
|
||||||
{ id: "<b>Included</b> (based on availability)" },
|
|
||||||
{ b: (str) => <b>{str}</b> }
|
|
||||||
)
|
|
||||||
const completeStep = useEnterDetailsStore((state) => state.completeStep)
|
const completeStep = useEnterDetailsStore((state) => state.completeStep)
|
||||||
|
|
||||||
const onSubmit = useCallback(
|
const onSubmit = useCallback(
|
||||||
@@ -58,38 +56,24 @@ export default function BedType() {
|
|||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||||
<RadioCard
|
{bedTypes.map((roomType) => {
|
||||||
Icon={KingBedIcon}
|
const width =
|
||||||
iconWidth={46}
|
roomType.size.max === roomType.size.min
|
||||||
id={BedTypeEnum.KING}
|
? `${roomType.size.min} cm`
|
||||||
name="bedType"
|
: `${roomType.size.min} cm - ${roomType.size.max} cm`
|
||||||
subtitle={intl.formatMessage(
|
return (
|
||||||
{ id: "{width} cm × {length} cm" },
|
<RadioCard
|
||||||
{
|
key={roomType.value}
|
||||||
length: "210",
|
Icon={KingBedIcon}
|
||||||
width: "180",
|
iconWidth={46}
|
||||||
}
|
id={roomType.value}
|
||||||
)}
|
name="bedType"
|
||||||
text={text}
|
subtitle={width}
|
||||||
title={intl.formatMessage({ id: "King bed" })}
|
title={roomType.description}
|
||||||
value={BedTypeEnum.KING}
|
value={roomType.description}
|
||||||
/>
|
/>
|
||||||
<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}
|
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import { z } from "zod"
|
|||||||
import { BedTypeEnum } from "@/types/enums/bedType"
|
import { BedTypeEnum } from "@/types/enums/bedType"
|
||||||
|
|
||||||
export const bedTypeSchema = z.object({
|
export const bedTypeSchema = z.object({
|
||||||
bedType: z.nativeEnum(BedTypeEnum),
|
bedType: z.string(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import styles from "./breakfast.module.css"
|
|||||||
import type {
|
import type {
|
||||||
BreakfastFormSchema,
|
BreakfastFormSchema,
|
||||||
BreakfastProps,
|
BreakfastProps,
|
||||||
} from "@/types/components/enterDetails/breakfast"
|
} from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
|
|
||||||
export default function Breakfast({ packages }: BreakfastProps) {
|
export default function Breakfast({ packages }: BreakfastProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|
||||||
const breakfast = useEnterDetailsStore((state) => state.data.breakfast)
|
const breakfast = useEnterDetailsStore((state) => state.userData.breakfast)
|
||||||
|
|
||||||
let defaultValues = undefined
|
let defaultValues = undefined
|
||||||
if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) {
|
if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) {
|
||||||
@@ -76,21 +76,21 @@ export default function Breakfast({ packages }: BreakfastProps) {
|
|||||||
subtitle={
|
subtitle={
|
||||||
pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
|
||||||
? intl.formatMessage<React.ReactNode>(
|
? intl.formatMessage<React.ReactNode>(
|
||||||
{ id: "breakfast.price.free" },
|
{ id: "breakfast.price.free" },
|
||||||
{
|
{
|
||||||
amount: pkg.originalPrice,
|
amount: pkg.originalPrice,
|
||||||
currency: pkg.currency,
|
currency: pkg.currency,
|
||||||
free: (str) => <Highlight>{str}</Highlight>,
|
free: (str) => <Highlight>{str}</Highlight>,
|
||||||
strikethrough: (str) => <s>{str}</s>,
|
strikethrough: (str) => <s>{str}</s>,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
: intl.formatMessage(
|
: intl.formatMessage(
|
||||||
{ id: "breakfast.price" },
|
{ id: "breakfast.price" },
|
||||||
{
|
{
|
||||||
amount: pkg.packagePrice,
|
amount: pkg.packagePrice,
|
||||||
currency: pkg.currency,
|
currency: pkg.currency,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
text={intl.formatMessage({
|
text={intl.formatMessage({
|
||||||
id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.",
|
id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.",
|
||||||
|
|||||||
@@ -22,21 +22,21 @@ import styles from "./details.module.css"
|
|||||||
import type {
|
import type {
|
||||||
DetailsProps,
|
DetailsProps,
|
||||||
DetailsSchema,
|
DetailsSchema,
|
||||||
} from "@/types/components/enterDetails/details"
|
} from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
|
|
||||||
const formID = "enter-details"
|
const formID = "enter-details"
|
||||||
export default function Details({ user }: DetailsProps) {
|
export default function Details({ user }: DetailsProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const initialData = useEnterDetailsStore((state) => ({
|
const initialData = useEnterDetailsStore((state) => ({
|
||||||
countryCode: state.data.countryCode,
|
countryCode: state.userData.countryCode,
|
||||||
email: state.data.email,
|
email: state.userData.email,
|
||||||
firstName: state.data.firstName,
|
firstName: state.userData.firstName,
|
||||||
lastName: state.data.lastName,
|
lastName: state.userData.lastName,
|
||||||
phoneNumber: state.data.phoneNumber,
|
phoneNumber: state.userData.phoneNumber,
|
||||||
join: state.data.join,
|
join: state.userData.join,
|
||||||
dateOfBirth: state.data.dateOfBirth,
|
dateOfBirth: state.userData.dateOfBirth,
|
||||||
zipCode: state.data.zipCode,
|
zipCode: state.userData.zipCode,
|
||||||
termsAccepted: state.data.termsAccepted,
|
termsAccepted: state.userData.termsAccepted,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const methods = useForm<DetailsSchema>({
|
const methods = useForm<DetailsSchema>({
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function Payment({
|
|||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const queryParams = useSearchParams()
|
const queryParams = useSearchParams()
|
||||||
const { firstName, lastName, email, phoneNumber, countryCode } =
|
const { firstName, lastName, email, phoneNumber, countryCode } =
|
||||||
useEnterDetailsStore((state) => state.data)
|
useEnterDetailsStore((state) => state.userData)
|
||||||
const [confirmationNumber, setConfirmationNumber] = useState<string>("")
|
const [confirmationNumber, setConfirmationNumber] = useState<string>("")
|
||||||
|
|
||||||
const methods = useForm<PaymentFormData>({
|
const methods = useForm<PaymentFormData>({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
import { useSearchParams } from "next/navigation"
|
||||||
import { PropsWithChildren, useRef } from "react"
|
import { PropsWithChildren, useRef } from "react"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -7,15 +8,16 @@ import {
|
|||||||
initEditDetailsState,
|
initEditDetailsState,
|
||||||
} from "@/stores/enter-details"
|
} from "@/stores/enter-details"
|
||||||
|
|
||||||
import { StepEnum } from "@/types/components/enterDetails/step"
|
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
||||||
|
|
||||||
export default function EnterDetailsProvider({
|
export default function EnterDetailsProvider({
|
||||||
step,
|
step,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<{ step: StepEnum }>) {
|
}: PropsWithChildren<{ step: StepEnum }>) {
|
||||||
|
const searchParams = useSearchParams()
|
||||||
const initialStore = useRef<EnterDetailsStore>()
|
const initialStore = useRef<EnterDetailsStore>()
|
||||||
if (!initialStore.current) {
|
if (!initialStore.current) {
|
||||||
initialStore.current = initEditDetailsState(step)
|
initialStore.current = initEditDetailsState(step, searchParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import styles from "./enterDetailsSidePeek.module.css"
|
|||||||
import {
|
import {
|
||||||
SidePeekEnum,
|
SidePeekEnum,
|
||||||
SidePeekProps,
|
SidePeekProps,
|
||||||
} from "@/types/components/enterDetails/sidePeek"
|
} from "@/types/components/hotelReservation/enterDetails/sidePeek"
|
||||||
|
|
||||||
export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) {
|
export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) {
|
||||||
const activeSidePeek = useEnterDetailsStore((state) => state.activeSidePeek)
|
const activeSidePeek = useEnterDetailsStore((state) => state.activeSidePeek)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
|
|||||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
|
||||||
import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek"
|
import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek"
|
||||||
|
|
||||||
export default function ToggleSidePeek() {
|
export default function ToggleSidePeek() {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
|
|||||||
@@ -1,153 +1,201 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
import { ArrowRightIcon } from "@/components/Icons"
|
import { ArrowRightIcon } from "@/components/Icons"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
|
import Link from "@/components/TempDesignSystem/Link"
|
||||||
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 { getIntl } from "@/i18n"
|
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import useLang from "@/hooks/useLang"
|
||||||
|
import { formatNumber } from "@/utils/format"
|
||||||
import ToggleSidePeek from "./ToggleSidePeek"
|
|
||||||
|
|
||||||
import styles from "./summary.module.css"
|
import styles from "./summary.module.css"
|
||||||
|
|
||||||
// TEMP
|
import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData"
|
||||||
const rooms = [
|
import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||||
{
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
adults: 1,
|
|
||||||
type: "Cozy cabin",
|
export default function Summary({
|
||||||
},
|
isMember,
|
||||||
]
|
room,
|
||||||
|
}: {
|
||||||
|
isMember: boolean
|
||||||
|
room: RoomsData
|
||||||
|
}) {
|
||||||
|
const [chosenBed, setChosenBed] = useState<string>()
|
||||||
|
const [chosenBreakfast, setChosenBreakfast] = useState<
|
||||||
|
BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST
|
||||||
|
>()
|
||||||
|
const intl = useIntl()
|
||||||
|
const lang = useLang()
|
||||||
|
const { fromDate, toDate, bedType, breakfast } = useEnterDetailsStore(
|
||||||
|
(state) => ({
|
||||||
|
fromDate: state.roomData.fromDate,
|
||||||
|
toDate: state.roomData.toDate,
|
||||||
|
bedType: state.userData.bedType,
|
||||||
|
breakfast: state.userData.breakfast,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
export default async function Summary() {
|
|
||||||
const intl = await getIntl()
|
|
||||||
const lang = getLang()
|
|
||||||
const fromDate = dt().locale(lang).format("ddd, D MMM")
|
|
||||||
const toDate = dt().add(1, "day").locale(lang).format("ddd, D MMM")
|
|
||||||
const diff = dt(toDate).diff(fromDate, "days")
|
const diff = dt(toDate).diff(fromDate, "days")
|
||||||
|
|
||||||
const totalAdults = rooms.reduce((total, room) => total + room.adults, 0)
|
|
||||||
|
|
||||||
const adults = intl.formatMessage(
|
|
||||||
{ id: "booking.adults" },
|
|
||||||
{ totalAdults: totalAdults }
|
|
||||||
)
|
|
||||||
const nights = intl.formatMessage(
|
const nights = intl.formatMessage(
|
||||||
{ id: "booking.nights" },
|
{ id: "booking.nights" },
|
||||||
{ totalNights: diff }
|
{ totalNights: diff }
|
||||||
)
|
)
|
||||||
|
|
||||||
const addOns = [
|
let color: "uiTextHighContrast" | "red" = "uiTextHighContrast"
|
||||||
{
|
if (isMember) {
|
||||||
price: intl.formatMessage({ id: "Included" }),
|
color = "red"
|
||||||
title: intl.formatMessage({ id: "King bed" }),
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
price: intl.formatMessage({ id: "Included" }),
|
|
||||||
title: intl.formatMessage({ id: "Breakfast buffet" }),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const mappedRooms = Array.from(
|
useEffect(() => {
|
||||||
rooms
|
setChosenBed(bedType)
|
||||||
.reduce((acc, room) => {
|
|
||||||
const currentRoom = acc.get(room.type)
|
if (breakfast) {
|
||||||
acc.set(room.type, {
|
setChosenBreakfast(breakfast)
|
||||||
total: currentRoom ? currentRoom.total + 1 : 1,
|
}
|
||||||
type: room.type,
|
}, [bedType, breakfast])
|
||||||
})
|
|
||||||
return acc
|
|
||||||
}, new Map())
|
|
||||||
.values()
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={styles.summary}>
|
<section className={styles.summary}>
|
||||||
<header>
|
<header>
|
||||||
<Body textTransform="bold">
|
<Subtitle type="two">{intl.formatMessage({ id: "Summary" })}</Subtitle>
|
||||||
{mappedRooms.map(
|
<Body className={styles.date} color="baseTextMediumContrast">
|
||||||
(room, idx) =>
|
{dt(fromDate).locale(lang).format("ddd, D MMM")}
|
||||||
`${room.total} x ${room.type}${mappedRooms.length > 1 && idx + 1 !== mappedRooms.length ? ", " : ""}`
|
<ArrowRightIcon color="peach80" height={15} width={15} />
|
||||||
)}
|
{dt(toDate).locale(lang).format("ddd, D MMM")} ({nights})
|
||||||
</Body>
|
</Body>
|
||||||
<Body className={styles.date} color="textMediumContrast">
|
|
||||||
{fromDate}
|
|
||||||
<ArrowRightIcon color="uiTextMediumContrast" height={15} width={15} />
|
|
||||||
{toDate}
|
|
||||||
</Body>
|
|
||||||
|
|
||||||
<ToggleSidePeek />
|
|
||||||
</header>
|
</header>
|
||||||
<Divider color="primaryLightSubtle" />
|
<Divider color="primaryLightSubtle" />
|
||||||
<div className={styles.addOns}>
|
<div className={styles.addOns}>
|
||||||
<div className={styles.entry}>
|
<div>
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Body color="textHighContrast">{room.roomType}</Body>
|
||||||
|
<Caption color={color}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{amount} {currency}" },
|
||||||
|
{
|
||||||
|
amount: formatNumber(parseInt(room.localPrice.price ?? "0")),
|
||||||
|
currency: room.localPrice.currency,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
{`${nights}, ${adults}`}
|
|
||||||
</Caption>
|
|
||||||
<Caption color="uiTextHighContrast">
|
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "{amount} {currency}" },
|
{ id: "booking.adults" },
|
||||||
{ amount: "4536", currency: "SEK" }
|
{ totalAdults: room.adults }
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</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>
|
</div>
|
||||||
{addOns.map((addOn) => (
|
|
||||||
<div className={styles.entry} key={addOn.title}>
|
{chosenBed ? (
|
||||||
<Caption color="uiTextMediumContrast">{addOn.title}</Caption>
|
<div className={styles.entry}>
|
||||||
<Caption color="uiTextHighContrast">{addOn.price}</Caption>
|
<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: room.localPrice.currency }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : null}
|
||||||
|
|
||||||
|
{chosenBreakfast ? (
|
||||||
|
chosenBreakfast === BreakfastPackageEnum.NO_BREAKFAST ? (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Body color="textHighContrast">
|
||||||
|
{intl.formatMessage({ id: "No breakfast" })}
|
||||||
|
</Body>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{amount} {currency}" },
|
||||||
|
{ amount: "0", currency: room.localPrice.currency }
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<Body color="textHighContrast">
|
||||||
|
{intl.formatMessage({ id: "Breakfast buffet" })}
|
||||||
|
</Body>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{ id: "{amount} {currency}" },
|
||||||
|
{
|
||||||
|
amount: chosenBreakfast.totalPrice,
|
||||||
|
currency: chosenBreakfast.currency,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<Divider color="primaryLightSubtle" />
|
<Divider color="primaryLightSubtle" />
|
||||||
<div className={styles.total}>
|
<div className={styles.total}>
|
||||||
<div>
|
<div className={styles.entry}>
|
||||||
<div className={styles.entry}>
|
<div>
|
||||||
<Body textTransform="bold">
|
<Body>
|
||||||
{intl.formatMessage({ id: "Total incl VAT" })}
|
{intl.formatMessage<React.ReactNode>(
|
||||||
</Body>
|
{ id: "<b>Total price</b> (incl VAT)" },
|
||||||
<Body textTransform="bold">
|
{ b: (str) => <b>{str}</b> }
|
||||||
{intl.formatMessage(
|
|
||||||
{ id: "{amount} {currency}" },
|
|
||||||
{ amount: "4686", currency: "SEK" }
|
|
||||||
)}
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
|
<Link color="burgundy" href="#" variant="underscored" size="small">
|
||||||
|
{intl.formatMessage({ id: "Price details" })}
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.entry}>
|
<div>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Body textTransform="bold">
|
||||||
{intl.formatMessage({ id: "Approx." })}
|
|
||||||
</Caption>
|
|
||||||
<Caption color="uiTextMediumContrast">
|
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "{amount} {currency}" },
|
{ id: "{amount} {currency}" },
|
||||||
{ amount: "455", currency: "EUR" }
|
{
|
||||||
)}
|
amount: formatNumber(parseInt(room.localPrice.price ?? "0")),
|
||||||
</Caption>
|
currency: room.localPrice.currency,
|
||||||
</div>
|
}
|
||||||
</div>
|
)}
|
||||||
<div>
|
</Body>
|
||||||
<div className={styles.entry}>
|
<Caption color="uiTextMediumContrast">
|
||||||
<Body color="red" textTransform="bold">
|
{intl.formatMessage({ id: "Approx." })}{" "}
|
||||||
{intl.formatMessage({ id: "Member price" })}
|
{intl.formatMessage(
|
||||||
</Body>
|
{ id: "{amount} {currency}" },
|
||||||
<Body color="red" textTransform="bold">
|
{
|
||||||
{intl.formatMessage(
|
amount: formatNumber(parseInt(room.euroPrice.price ?? "0")),
|
||||||
{ id: "{amount} {currency}" },
|
currency: room.euroPrice.currency,
|
||||||
{ amount: "4219", 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: "412", currency: "EUR" }
|
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Divider color="primaryLightSubtle" />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
.summary {
|
.summary {
|
||||||
background-color: var(--Main-Grey-White);
|
|
||||||
border: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
|
|
||||||
border-radius: var(--Corner-radius-Large);
|
border-radius: var(--Corner-radius-Large);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
padding: var(--Spacing-x2);
|
padding: var(--Spacing-x3);
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date {
|
.date {
|
||||||
@@ -22,7 +21,7 @@
|
|||||||
.addOns {
|
.addOns {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--Spacing-x1);
|
gap: var(--Spacing-x-one-and-half);
|
||||||
}
|
}
|
||||||
|
|
||||||
.entry {
|
.entry {
|
||||||
@@ -31,6 +30,9 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entry > :last-child {
|
||||||
|
justify-items: flex-end;
|
||||||
|
}
|
||||||
.total {
|
.total {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useMemo, useState } from "react"
|
|||||||
|
|
||||||
import RateSummary from "./RateSummary"
|
import RateSummary from "./RateSummary"
|
||||||
import RoomCard from "./RoomCard"
|
import RoomCard from "./RoomCard"
|
||||||
import getHotelReservationQueryParams from "./utils"
|
import { getHotelReservationQueryParams } from "./utils"
|
||||||
|
|
||||||
import styles from "./roomSelection.module.css"
|
import styles from "./roomSelection.module.css"
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,22 @@ import { getFormattedUrlQueryParams } from "@/utils/url"
|
|||||||
|
|
||||||
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
|
||||||
|
|
||||||
function getHotelReservationQueryParams(searchParams: URLSearchParams) {
|
export function getHotelReservationQueryParams(searchParams: URLSearchParams) {
|
||||||
return getFormattedUrlQueryParams(searchParams, {
|
return getFormattedUrlQueryParams(searchParams, {
|
||||||
adults: "number",
|
adults: "number",
|
||||||
age: "number",
|
age: "number",
|
||||||
}) as SelectRateSearchParams
|
}) 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 and children
|
||||||
|
roomTypeCode: room[0].roomtype,
|
||||||
|
rateCode: room[0].ratecode,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -26,7 +26,12 @@ interface TextCardProps extends BaseCardProps {
|
|||||||
text: React.ReactNode
|
text: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CardProps = ListCardProps | TextCardProps
|
interface CleanCardProps extends BaseCardProps {
|
||||||
|
list?: never
|
||||||
|
text?: never
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CardProps = ListCardProps | TextCardProps | CleanCardProps
|
||||||
|
|
||||||
export type CheckboxProps =
|
export type CheckboxProps =
|
||||||
| Omit<ListCardProps, "type">
|
| Omit<ListCardProps, "type">
|
||||||
@@ -34,6 +39,7 @@ export type CheckboxProps =
|
|||||||
export type RadioProps =
|
export type RadioProps =
|
||||||
| Omit<ListCardProps, "type">
|
| Omit<ListCardProps, "type">
|
||||||
| Omit<TextCardProps, "type">
|
| Omit<TextCardProps, "type">
|
||||||
|
| Omit<CleanCardProps, "type">
|
||||||
|
|
||||||
export interface ListProps extends Pick<ListCardProps, "declined"> {
|
export interface ListProps extends Pick<ListCardProps, "declined"> {
|
||||||
list?: ListCardProps["list"]
|
list?: ListCardProps["list"]
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
color: var(--Primary-Dark-On-Surface-Accent);
|
color: var(--Primary-Dark-On-Surface-Accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.peach80 {
|
.baseTextMediumContrast {
|
||||||
color: var(--Base-Text-Medium-contrast);
|
color: var(--Base-Text-Medium-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const config = {
|
|||||||
textHighContrast: styles.textHighContrast,
|
textHighContrast: styles.textHighContrast,
|
||||||
white: styles.white,
|
white: styles.white,
|
||||||
peach50: styles.peach50,
|
peach50: styles.peach50,
|
||||||
peach80: styles.peach80,
|
|
||||||
uiTextHighContrast: styles.uiTextHighContrast,
|
uiTextHighContrast: styles.uiTextHighContrast,
|
||||||
uiTextMediumContrast: styles.uiTextMediumContrast,
|
uiTextMediumContrast: styles.uiTextMediumContrast,
|
||||||
uiTextPlaceholder: styles.uiTextPlaceholder,
|
uiTextPlaceholder: styles.uiTextPlaceholder,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)",
|
"<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 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",
|
"A photo of the room": "Et foto af værelset",
|
||||||
"ACCE": "Tilgængelighed",
|
"ACCE": "Tilgængelighed",
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
"Attractions": "Attraktioner",
|
"Attractions": "Attraktioner",
|
||||||
"Back to scandichotels.com": "Tilbage til scandichotels.com",
|
"Back to scandichotels.com": "Tilbage til scandichotels.com",
|
||||||
"Bar": "Bar",
|
"Bar": "Bar",
|
||||||
|
"Based on availability": "Baseret på tilgængelighed",
|
||||||
"Bed type": "Seng type",
|
"Bed type": "Seng type",
|
||||||
"Birth date": "Fødselsdato",
|
"Birth date": "Fødselsdato",
|
||||||
"Book": "Book",
|
"Book": "Book",
|
||||||
@@ -242,12 +245,14 @@
|
|||||||
"Points needed to stay on level": "Point nødvendige for at holde sig på niveau",
|
"Points needed to stay on level": "Point nødvendige for at holde sig på niveau",
|
||||||
"Previous": "Forudgående",
|
"Previous": "Forudgående",
|
||||||
"Previous victories": "Tidligere sejre",
|
"Previous victories": "Tidligere sejre",
|
||||||
|
"Price details": "Prisoplysninger",
|
||||||
"Proceed to login": "Fortsæt til login",
|
"Proceed to login": "Fortsæt til login",
|
||||||
"Proceed to payment method": "Fortsæt til betalingsmetode",
|
"Proceed to payment method": "Fortsæt til betalingsmetode",
|
||||||
"Provide a payment card in the next step": "Giv os dine betalingsoplysninger i næste skridt",
|
"Provide a payment card in the next step": "Giv os dine betalingsoplysninger i næste skridt",
|
||||||
"Public price from": "Offentlig pris fra",
|
"Public price from": "Offentlig pris fra",
|
||||||
"Public transport": "Offentlig transport",
|
"Public transport": "Offentlig transport",
|
||||||
"Queen bed": "Queensize-seng",
|
"Queen bed": "Queensize-seng",
|
||||||
|
"Rate details": "Oplysninger om værelsespris",
|
||||||
"Read more": "Læs mere",
|
"Read more": "Læs mere",
|
||||||
"Read more & book a table": "Read more & book a table",
|
"Read more & book a table": "Read more & book a table",
|
||||||
"Read more about the hotel": "Læs mere om hotellet",
|
"Read more about the hotel": "Læs mere om hotellet",
|
||||||
@@ -313,8 +318,8 @@
|
|||||||
"Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}",
|
"Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}",
|
||||||
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.",
|
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.",
|
||||||
"Total Points": "Samlet antal point",
|
"Total Points": "Samlet antal point",
|
||||||
"Total incl VAT": "Inkl. moms",
|
|
||||||
"Total price": "Samlet pris",
|
"Total price": "Samlet pris",
|
||||||
|
"Total price (incl VAT)": "Samlet pris (inkl. moms)",
|
||||||
"Tourist": "Turist",
|
"Tourist": "Turist",
|
||||||
"Transaction date": "Overførselsdato",
|
"Transaction date": "Overførselsdato",
|
||||||
"Transactions": "Transaktioner",
|
"Transactions": "Transaktioner",
|
||||||
@@ -405,6 +410,5 @@
|
|||||||
"uppercase letter": "stort bogstav",
|
"uppercase letter": "stort bogstav",
|
||||||
"{amount} out of {total}": "{amount} ud af {total}",
|
"{amount} out of {total}": "{amount} ud af {total}",
|
||||||
"{amount} {currency}": "{amount} {currency}",
|
"{amount} {currency}": "{amount} {currency}",
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
||||||
"{width} cm × {length} cm": "{width} cm × {length} cm"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)",
|
"<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 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",
|
"A photo of the room": "Ein Foto des Zimmers",
|
||||||
"ACCE": "Zugänglichkeit",
|
"ACCE": "Zugänglichkeit",
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
"Attraction": "Attraktion",
|
"Attraction": "Attraktion",
|
||||||
"Back to scandichotels.com": "Zurück zu scandichotels.com",
|
"Back to scandichotels.com": "Zurück zu scandichotels.com",
|
||||||
"Bar": "Bar",
|
"Bar": "Bar",
|
||||||
|
"Based on availability": "Je nach Verfügbarkeit",
|
||||||
"Bed type": "Bettentyp",
|
"Bed type": "Bettentyp",
|
||||||
"Birth date": "Geburtsdatum",
|
"Birth date": "Geburtsdatum",
|
||||||
"Book": "Buchen",
|
"Book": "Buchen",
|
||||||
@@ -240,12 +243,14 @@
|
|||||||
"Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben",
|
"Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben",
|
||||||
"Previous": "Früher",
|
"Previous": "Früher",
|
||||||
"Previous victories": "Bisherige Siege",
|
"Previous victories": "Bisherige Siege",
|
||||||
|
"Price details": "Preisdetails",
|
||||||
"Proceed to login": "Weiter zum Login",
|
"Proceed to login": "Weiter zum Login",
|
||||||
"Proceed to payment method": "Weiter zur Zahlungsmethode",
|
"Proceed to payment method": "Weiter zur Zahlungsmethode",
|
||||||
"Provide a payment card in the next step": "Geben Sie Ihre Zahlungskarteninformationen im nächsten Schritt an",
|
"Provide a payment card in the next step": "Geben Sie Ihre Zahlungskarteninformationen im nächsten Schritt an",
|
||||||
"Public price from": "Öffentlicher Preis ab",
|
"Public price from": "Öffentlicher Preis ab",
|
||||||
"Public transport": "Öffentliche Verkehrsmittel",
|
"Public transport": "Öffentliche Verkehrsmittel",
|
||||||
"Queen bed": "Queensize-Bett",
|
"Queen bed": "Queensize-Bett",
|
||||||
|
"Rate details": "Preisdetails",
|
||||||
"Read more": "Mehr lesen",
|
"Read more": "Mehr lesen",
|
||||||
"Read more & book a table": "Read more & book a table",
|
"Read more & book a table": "Read more & book a table",
|
||||||
"Read more about the hotel": "Lesen Sie mehr über das Hotel",
|
"Read more about the hotel": "Lesen Sie mehr über das Hotel",
|
||||||
@@ -312,8 +317,8 @@
|
|||||||
"Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}",
|
"Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}",
|
||||||
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.",
|
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.",
|
||||||
"Total Points": "Gesamtpunktzahl",
|
"Total Points": "Gesamtpunktzahl",
|
||||||
"Total incl VAT": "Gesamt inkl. MwSt.",
|
|
||||||
"Total price": "Gesamtpreis",
|
"Total price": "Gesamtpreis",
|
||||||
|
"Total price (incl VAT)": "Gesamtpreis (inkl. MwSt.)",
|
||||||
"Tourist": "Tourist",
|
"Tourist": "Tourist",
|
||||||
"Transaction date": "Transaktionsdatum",
|
"Transaction date": "Transaktionsdatum",
|
||||||
"Transactions": "Transaktionen",
|
"Transactions": "Transaktionen",
|
||||||
@@ -404,6 +409,5 @@
|
|||||||
"uppercase letter": "großbuchstabe",
|
"uppercase letter": "großbuchstabe",
|
||||||
"{amount} out of {total}": "{amount} von {total}",
|
"{amount} out of {total}": "{amount} von {total}",
|
||||||
"{amount} {currency}": "{amount} {currency}",
|
"{amount} {currency}": "{amount} {currency}",
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
||||||
"{width} cm × {length} cm": "{width} cm × {length} cm"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)",
|
"<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 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",
|
"A photo of the room": "A photo of the room",
|
||||||
"ACCE": "Accessibility",
|
"ACCE": "Accessibility",
|
||||||
@@ -38,6 +40,7 @@
|
|||||||
"Attractions": "Attractions",
|
"Attractions": "Attractions",
|
||||||
"Back to scandichotels.com": "Back to scandichotels.com",
|
"Back to scandichotels.com": "Back to scandichotels.com",
|
||||||
"Bar": "Bar",
|
"Bar": "Bar",
|
||||||
|
"Based on availability": "Based on availability",
|
||||||
"Bed": "Bed",
|
"Bed": "Bed",
|
||||||
"Bed type": "Bed type",
|
"Bed type": "Bed type",
|
||||||
"Birth date": "Birth date",
|
"Birth date": "Birth date",
|
||||||
@@ -252,6 +255,7 @@
|
|||||||
"Points needed to stay on level": "Points needed to stay on level",
|
"Points needed to stay on level": "Points needed to stay on level",
|
||||||
"Previous": "Previous",
|
"Previous": "Previous",
|
||||||
"Previous victories": "Previous victories",
|
"Previous victories": "Previous victories",
|
||||||
|
"Price details": "Price details",
|
||||||
"Print confirmation": "Print confirmation",
|
"Print confirmation": "Print confirmation",
|
||||||
"Proceed to login": "Proceed to login",
|
"Proceed to login": "Proceed to login",
|
||||||
"Proceed to payment method": "Proceed to payment method",
|
"Proceed to payment method": "Proceed to payment method",
|
||||||
@@ -259,6 +263,7 @@
|
|||||||
"Public price from": "Public price from",
|
"Public price from": "Public price from",
|
||||||
"Public transport": "Public transport",
|
"Public transport": "Public transport",
|
||||||
"Queen bed": "Queen bed",
|
"Queen bed": "Queen bed",
|
||||||
|
"Rate details": "Rate details",
|
||||||
"Read more": "Read more",
|
"Read more": "Read more",
|
||||||
"Read more & book a table": "Read more & book a table",
|
"Read more & book a table": "Read more & book a table",
|
||||||
"Read more about the hotel": "Read more about the hotel",
|
"Read more about the hotel": "Read more about the hotel",
|
||||||
@@ -326,10 +331,9 @@
|
|||||||
"There are no transactions to display": "There are no transactions to display",
|
"There are no transactions to display": "There are no transactions to display",
|
||||||
"Things nearby HOTEL_NAME": "Things nearby {hotelName}",
|
"Things nearby HOTEL_NAME": "Things nearby {hotelName}",
|
||||||
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.",
|
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.",
|
||||||
"Total Points": "Total Points",
|
|
||||||
"Total cost": "Total cost",
|
"Total cost": "Total cost",
|
||||||
"Total incl VAT": "Total incl VAT",
|
|
||||||
"Total price": "Total price",
|
"Total price": "Total price",
|
||||||
|
"Total Points": "Total Points",
|
||||||
"Tourist": "Tourist",
|
"Tourist": "Tourist",
|
||||||
"Transaction date": "Transaction date",
|
"Transaction date": "Transaction date",
|
||||||
"Transactions": "Transactions",
|
"Transactions": "Transactions",
|
||||||
@@ -427,6 +431,5 @@
|
|||||||
"{amount} out of {total}": "{amount} out of {total}",
|
"{amount} out of {total}": "{amount} out of {total}",
|
||||||
"{amount} {currency}": "{amount} {currency}",
|
"{amount} {currency}": "{amount} {currency}",
|
||||||
"{card} ending with {cardno}": "{card} ending with {cardno}",
|
"{card} ending with {cardno}": "{card} ending with {cardno}",
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
||||||
"{width} cm × {length} cm": "{width} cm × {length} cm"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)",
|
"<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 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",
|
"A photo of the room": "Kuva huoneesta",
|
||||||
"ACCE": "Saavutettavuus",
|
"ACCE": "Saavutettavuus",
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
"Attractions": "Nähtävyydet",
|
"Attractions": "Nähtävyydet",
|
||||||
"Back to scandichotels.com": "Takaisin scandichotels.com",
|
"Back to scandichotels.com": "Takaisin scandichotels.com",
|
||||||
"Bar": "Bar",
|
"Bar": "Bar",
|
||||||
|
"Based on availability": "Saatavuuden mukaan",
|
||||||
"Bed type": "Vuodetyyppi",
|
"Bed type": "Vuodetyyppi",
|
||||||
"Birth date": "Syntymäaika",
|
"Birth date": "Syntymäaika",
|
||||||
"Book": "Varaa",
|
"Book": "Varaa",
|
||||||
@@ -242,12 +245,14 @@
|
|||||||
"Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet",
|
"Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet",
|
||||||
"Previous": "Aikaisempi",
|
"Previous": "Aikaisempi",
|
||||||
"Previous victories": "Edelliset voitot",
|
"Previous victories": "Edelliset voitot",
|
||||||
|
"Price details": "Hintatiedot",
|
||||||
"Proceed to login": "Jatka kirjautumiseen",
|
"Proceed to login": "Jatka kirjautumiseen",
|
||||||
"Proceed to payment method": "Siirry maksutavalle",
|
"Proceed to payment method": "Siirry maksutavalle",
|
||||||
"Provide a payment card in the next step": "Anna maksukortin tiedot seuraavassa vaiheessa",
|
"Provide a payment card in the next step": "Anna maksukortin tiedot seuraavassa vaiheessa",
|
||||||
"Public price from": "Julkinen hinta alkaen",
|
"Public price from": "Julkinen hinta alkaen",
|
||||||
"Public transport": "Julkinen liikenne",
|
"Public transport": "Julkinen liikenne",
|
||||||
"Queen bed": "Queen-vuode",
|
"Queen bed": "Queen-vuode",
|
||||||
|
"Rate details": "Hintatiedot",
|
||||||
"Read more": "Lue lisää",
|
"Read more": "Lue lisää",
|
||||||
"Read more & book a table": "Read more & book a table",
|
"Read more & book a table": "Read more & book a table",
|
||||||
"Read more about the hotel": "Lue lisää hotellista",
|
"Read more about the hotel": "Lue lisää hotellista",
|
||||||
@@ -314,8 +319,8 @@
|
|||||||
"Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}",
|
"Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}",
|
||||||
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.",
|
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.",
|
||||||
"Total Points": "Kokonaispisteet",
|
"Total Points": "Kokonaispisteet",
|
||||||
"Total incl VAT": "Yhteensä sis. alv",
|
|
||||||
"Total price": "Kokonaishinta",
|
"Total price": "Kokonaishinta",
|
||||||
|
"Total price (incl VAT)": "Kokonaishinta (sis. ALV)",
|
||||||
"Tourist": "Turisti",
|
"Tourist": "Turisti",
|
||||||
"Transaction date": "Tapahtuman päivämäärä",
|
"Transaction date": "Tapahtuman päivämäärä",
|
||||||
"Transactions": "Tapahtumat",
|
"Transactions": "Tapahtumat",
|
||||||
@@ -404,6 +409,5 @@
|
|||||||
"uppercase letter": "iso kirjain",
|
"uppercase letter": "iso kirjain",
|
||||||
"{amount} out of {total}": "{amount}/{total}",
|
"{amount} out of {total}": "{amount}/{total}",
|
||||||
"{amount} {currency}": "{amount} {currency}",
|
"{amount} {currency}": "{amount} {currency}",
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
||||||
"{width} cm × {length} cm": "{width} cm × {length} cm"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)",
|
"<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 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",
|
"A photo of the room": "Et bilde av rommet",
|
||||||
"ACCE": "Tilgjengelighet",
|
"ACCE": "Tilgjengelighet",
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
"Attractions": "Attraksjoner",
|
"Attractions": "Attraksjoner",
|
||||||
"Back to scandichotels.com": "Tilbake til scandichotels.com",
|
"Back to scandichotels.com": "Tilbake til scandichotels.com",
|
||||||
"Bar": "Bar",
|
"Bar": "Bar",
|
||||||
|
"Based on availability": "Basert på tilgjengelighet",
|
||||||
"Bed type": "Seng type",
|
"Bed type": "Seng type",
|
||||||
"Birth date": "Fødselsdato",
|
"Birth date": "Fødselsdato",
|
||||||
"Book": "Bestill",
|
"Book": "Bestill",
|
||||||
@@ -240,12 +243,14 @@
|
|||||||
"Points needed to stay on level": "Poeng som trengs for å holde seg på nivå",
|
"Points needed to stay on level": "Poeng som trengs for å holde seg på nivå",
|
||||||
"Previous": "Tidligere",
|
"Previous": "Tidligere",
|
||||||
"Previous victories": "Tidligere seire",
|
"Previous victories": "Tidligere seire",
|
||||||
|
"Price details": "Prisdetaljer",
|
||||||
"Proceed to login": "Fortsett til innlogging",
|
"Proceed to login": "Fortsett til innlogging",
|
||||||
"Proceed to payment method": "Fortsett til betalingsmetode",
|
"Proceed to payment method": "Fortsett til betalingsmetode",
|
||||||
"Provide a payment card in the next step": "Gi oss dine betalingskortdetaljer i neste steg",
|
"Provide a payment card in the next step": "Gi oss dine betalingskortdetaljer i neste steg",
|
||||||
"Public price from": "Offentlig pris fra",
|
"Public price from": "Offentlig pris fra",
|
||||||
"Public transport": "Offentlig transport",
|
"Public transport": "Offentlig transport",
|
||||||
"Queen bed": "Queen-size-seng",
|
"Queen bed": "Queen-size-seng",
|
||||||
|
"Rate details": "Prisdetaljer",
|
||||||
"Read more": "Les mer",
|
"Read more": "Les mer",
|
||||||
"Read more & book a table": "Read more & book a table",
|
"Read more & book a table": "Read more & book a table",
|
||||||
"Read more about the hotel": "Les mer om hotellet",
|
"Read more about the hotel": "Les mer om hotellet",
|
||||||
@@ -402,6 +407,5 @@
|
|||||||
"uppercase letter": "stor bokstav",
|
"uppercase letter": "stor bokstav",
|
||||||
"{amount} out of {total}": "{amount} av {total}",
|
"{amount} out of {total}": "{amount} av {total}",
|
||||||
"{amount} {currency}": "{amount} {currency}",
|
"{amount} {currency}": "{amount} {currency}",
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
||||||
"{width} cm × {length} cm": "{width} cm × {length} cm"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)",
|
"<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 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",
|
"A photo of the room": "Ett foto av rummet",
|
||||||
"ACCE": "Tillgänglighet",
|
"ACCE": "Tillgänglighet",
|
||||||
@@ -35,6 +37,7 @@
|
|||||||
"Attractions": "Sevärdheter",
|
"Attractions": "Sevärdheter",
|
||||||
"Back to scandichotels.com": "Tillbaka till scandichotels.com",
|
"Back to scandichotels.com": "Tillbaka till scandichotels.com",
|
||||||
"Bar": "Bar",
|
"Bar": "Bar",
|
||||||
|
"Based on availability": "Baserat på tillgänglighet",
|
||||||
"Bed type": "Sängtyp",
|
"Bed type": "Sängtyp",
|
||||||
"Birth date": "Födelsedatum",
|
"Birth date": "Födelsedatum",
|
||||||
"Book": "Boka",
|
"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å",
|
"Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå",
|
||||||
"Previous": "Föregående",
|
"Previous": "Föregående",
|
||||||
"Previous victories": "Tidigare segrar",
|
"Previous victories": "Tidigare segrar",
|
||||||
|
"Price details": "Prisdetaljer",
|
||||||
"Proceed to login": "Fortsätt till inloggning",
|
"Proceed to login": "Fortsätt till inloggning",
|
||||||
"Proceed to payment method": "Gå vidare till betalningsmetod",
|
"Proceed to payment method": "Gå vidare till betalningsmetod",
|
||||||
"Provide a payment card in the next step": "Ge oss dina betalkortdetaljer i nästa steg",
|
"Provide a payment card in the next step": "Ge oss dina betalkortdetaljer i nästa steg",
|
||||||
"Public price from": "Offentligt pris från",
|
"Public price from": "Offentligt pris från",
|
||||||
"Public transport": "Kollektivtrafik",
|
"Public transport": "Kollektivtrafik",
|
||||||
"Queen bed": "Queen size-säng",
|
"Queen bed": "Queen size-säng",
|
||||||
|
"Rate details": "Detaljer om rumspriset",
|
||||||
"Read more": "Läs mer",
|
"Read more": "Läs mer",
|
||||||
"Read more & book a table": "Read more & book a table",
|
"Read more & book a table": "Read more & book a table",
|
||||||
"Read more about the hotel": "Läs mer om hotellet",
|
"Read more about the hotel": "Läs mer om hotellet",
|
||||||
@@ -405,6 +410,5 @@
|
|||||||
"paying": "betalar",
|
"paying": "betalar",
|
||||||
"uppercase letter": "stor bokstav",
|
"uppercase letter": "stor bokstav",
|
||||||
"{amount} {currency}": "{amount} {currency}",
|
"{amount} {currency}": "{amount} {currency}",
|
||||||
"{difference}{amount} {currency}": "{difference}{amount} {currency}",
|
"{difference}{amount} {currency}": "{difference}{amount} {currency}"
|
||||||
"{width} cm × {length} cm": "{width} cm × {length} cm"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { cache } from "react"
|
import { cache } from "react"
|
||||||
|
|
||||||
import { Lang } from "@/constants/languages"
|
import { Lang } from "@/constants/languages"
|
||||||
|
import {
|
||||||
|
GetRoomsAvailabilityInput,
|
||||||
|
GetSelectedRoomAvailabilityInput,
|
||||||
|
HotelIncludeEnum,
|
||||||
|
} from "@/server/routers/hotels/input"
|
||||||
|
|
||||||
import { serverClient } from "../server"
|
import { serverClient } from "../server"
|
||||||
|
|
||||||
@@ -50,15 +55,22 @@ export const getUserTracking = cache(async function getMemoizedUserTracking() {
|
|||||||
return serverClient().user.tracking()
|
return serverClient().user.tracking()
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getHotelData = cache(async function getMemoizedHotelData(
|
export const getHotelData = cache(async function getMemoizedHotelData({
|
||||||
hotelId: string,
|
hotelId,
|
||||||
language: string,
|
language,
|
||||||
|
isCardOnlyPayment,
|
||||||
|
include,
|
||||||
|
}: {
|
||||||
|
hotelId: string
|
||||||
|
language: string
|
||||||
isCardOnlyPayment?: boolean
|
isCardOnlyPayment?: boolean
|
||||||
) {
|
include?: HotelIncludeEnum[]
|
||||||
|
}) {
|
||||||
return serverClient().hotel.hotelData.get({
|
return serverClient().hotel.hotelData.get({
|
||||||
hotelId,
|
hotelId,
|
||||||
language,
|
language,
|
||||||
isCardOnlyPayment,
|
isCardOnlyPayment,
|
||||||
|
include,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -71,17 +83,9 @@ export const getRoomAvailability = cache(
|
|||||||
children,
|
children,
|
||||||
promotionCode,
|
promotionCode,
|
||||||
rateCode,
|
rateCode,
|
||||||
}: {
|
}: GetRoomsAvailabilityInput) {
|
||||||
hotelId: string
|
|
||||||
adults: number
|
|
||||||
roomStayStartDate: string
|
|
||||||
roomStayEndDate: string
|
|
||||||
children?: string
|
|
||||||
promotionCode?: string
|
|
||||||
rateCode?: string
|
|
||||||
}) {
|
|
||||||
return serverClient().hotel.availability.rooms({
|
return serverClient().hotel.availability.rooms({
|
||||||
hotelId: parseInt(hotelId),
|
hotelId,
|
||||||
adults,
|
adults,
|
||||||
roomStayStartDate,
|
roomStayStartDate,
|
||||||
roomStayEndDate,
|
roomStayEndDate,
|
||||||
@@ -92,6 +96,14 @@ export const getRoomAvailability = cache(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const getSelectedRoomAvailability = cache(
|
||||||
|
async function getMemoizedRoomAvailability(
|
||||||
|
args: GetSelectedRoomAvailabilityInput
|
||||||
|
) {
|
||||||
|
return serverClient().hotel.availability.room(args)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
export const getFooter = cache(async function getMemoizedFooter() {
|
export const getFooter = cache(async function getMemoizedFooter() {
|
||||||
return serverClient().contentstack.base.footer()
|
return serverClient().contentstack.base.footer()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -87,12 +87,12 @@ const nextConfig = {
|
|||||||
// value: undefined,
|
// value: undefined,
|
||||||
// },
|
// },
|
||||||
// {
|
// {
|
||||||
// key: "fromdate",
|
// key: "fromDate",
|
||||||
// type: "query",
|
// type: "query",
|
||||||
// value: undefined,
|
// value: undefined,
|
||||||
// },
|
// },
|
||||||
// {
|
// {
|
||||||
// key: "todate",
|
// key: "toDate",
|
||||||
// type: "query",
|
// type: "query",
|
||||||
// value: undefined,
|
// value: undefined,
|
||||||
// },
|
// },
|
||||||
|
|||||||
@@ -29,17 +29,43 @@ export const getRoomsAvailabilityInputSchema = z.object({
|
|||||||
rateCode: z.string().optional(),
|
rateCode: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export const getSelectedRoomAvailabilityInputSchema = z.object({
|
||||||
|
hotelId: z.number(),
|
||||||
|
roomStayStartDate: z.string(),
|
||||||
|
roomStayEndDate: z.string(),
|
||||||
|
adults: z.number(),
|
||||||
|
children: z.string().optional(),
|
||||||
|
promotionCode: z.string().optional(),
|
||||||
|
reservationProfileType: z.string().optional().default(""),
|
||||||
|
attachedProfileId: z.string().optional().default(""),
|
||||||
|
rateCode: z.string(),
|
||||||
|
roomTypeCode: z.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type GetSelectedRoomAvailabilityInput = z.input<
|
||||||
|
typeof getSelectedRoomAvailabilityInputSchema
|
||||||
|
>
|
||||||
|
|
||||||
|
export type GetRoomsAvailabilityInput = z.input<
|
||||||
|
typeof getRoomsAvailabilityInputSchema
|
||||||
|
>
|
||||||
|
|
||||||
export const getRatesInputSchema = z.object({
|
export const getRatesInputSchema = z.object({
|
||||||
hotelId: z.string(),
|
hotelId: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getlHotelDataInputSchema = z.object({
|
export enum HotelIncludeEnum {
|
||||||
|
"RoomCategories",
|
||||||
|
"NearbyHotels",
|
||||||
|
"Restaurants",
|
||||||
|
"City",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getHotelDataInputSchema = z.object({
|
||||||
hotelId: z.string(),
|
hotelId: z.string(),
|
||||||
language: z.string(),
|
language: z.string(),
|
||||||
isCardOnlyPayment: z.boolean().optional(),
|
isCardOnlyPayment: z.boolean().optional(),
|
||||||
include: z
|
include: z.array(z.nativeEnum(HotelIncludeEnum)).optional(),
|
||||||
.array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"]))
|
|
||||||
.optional(),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getBreakfastPackageInput = z.object({
|
export const getBreakfastPackageInput = z.object({
|
||||||
|
|||||||
@@ -32,11 +32,12 @@ import {
|
|||||||
} from "./schemas/packages"
|
} from "./schemas/packages"
|
||||||
import {
|
import {
|
||||||
getBreakfastPackageInput,
|
getBreakfastPackageInput,
|
||||||
|
getHotelDataInputSchema,
|
||||||
getHotelInputSchema,
|
getHotelInputSchema,
|
||||||
getHotelsAvailabilityInputSchema,
|
getHotelsAvailabilityInputSchema,
|
||||||
getlHotelDataInputSchema,
|
|
||||||
getRatesInputSchema,
|
getRatesInputSchema,
|
||||||
getRoomsAvailabilityInputSchema,
|
getRoomsAvailabilityInputSchema,
|
||||||
|
getSelectedRoomAvailabilityInputSchema,
|
||||||
} from "./input"
|
} from "./input"
|
||||||
import {
|
import {
|
||||||
breakfastPackagesSchema,
|
breakfastPackagesSchema,
|
||||||
@@ -93,6 +94,16 @@ const roomsAvailabilityFailCounter = meter.createCounter(
|
|||||||
"trpc.hotel.availability.rooms-fail"
|
"trpc.hotel.availability.rooms-fail"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const selectedRoomAvailabilityCounter = meter.createCounter(
|
||||||
|
"trpc.hotel.availability.room"
|
||||||
|
)
|
||||||
|
const selectedRoomAvailabilitySuccessCounter = meter.createCounter(
|
||||||
|
"trpc.hotel.availability.room-success"
|
||||||
|
)
|
||||||
|
const selectedRoomAvailabilityFailCounter = meter.createCounter(
|
||||||
|
"trpc.hotel.availability.room-fail"
|
||||||
|
)
|
||||||
|
|
||||||
const breakfastPackagesCounter = meter.createCounter("trpc.package.breakfast")
|
const breakfastPackagesCounter = meter.createCounter("trpc.package.breakfast")
|
||||||
const breakfastPackagesSuccessCounter = meter.createCounter(
|
const breakfastPackagesSuccessCounter = meter.createCounter(
|
||||||
"trpc.package.breakfast-success"
|
"trpc.package.breakfast-success"
|
||||||
@@ -545,6 +556,161 @@ export const hotelQueryRouter = router({
|
|||||||
|
|
||||||
return validateAvailabilityData.data
|
return validateAvailabilityData.data
|
||||||
}),
|
}),
|
||||||
|
room: serviceProcedure
|
||||||
|
.input(getSelectedRoomAvailabilityInputSchema)
|
||||||
|
.query(async ({ input, ctx }) => {
|
||||||
|
const {
|
||||||
|
hotelId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
promotionCode,
|
||||||
|
reservationProfileType,
|
||||||
|
attachedProfileId,
|
||||||
|
rateCode,
|
||||||
|
roomTypeCode,
|
||||||
|
} = input
|
||||||
|
|
||||||
|
const params: Record<string, string | number | undefined> = {
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
...(children && { children }),
|
||||||
|
promotionCode,
|
||||||
|
reservationProfileType,
|
||||||
|
attachedProfileId,
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedRoomAvailabilityCounter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
promotionCode,
|
||||||
|
reservationProfileType,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.selectedRoomAvailability start",
|
||||||
|
JSON.stringify({ query: { hotelId, params } })
|
||||||
|
)
|
||||||
|
const apiResponseAvailability = await api.get(
|
||||||
|
api.endpoints.v1.Availability.hotel(hotelId.toString()),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponseAvailability.ok) {
|
||||||
|
const text = await apiResponseAvailability.text()
|
||||||
|
selectedRoomAvailabilityFailCounter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
promotionCode,
|
||||||
|
reservationProfileType,
|
||||||
|
error_type: "http_error",
|
||||||
|
error: JSON.stringify({
|
||||||
|
status: apiResponseAvailability.status,
|
||||||
|
statusText: apiResponseAvailability.statusText,
|
||||||
|
text,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotels.selectedRoomAvailability error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params },
|
||||||
|
error: {
|
||||||
|
status: apiResponseAvailability.status,
|
||||||
|
statusText: apiResponseAvailability.statusText,
|
||||||
|
text,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const apiJsonAvailability = await apiResponseAvailability.json()
|
||||||
|
const validateAvailabilityData =
|
||||||
|
getRoomsAvailabilitySchema.safeParse(apiJsonAvailability)
|
||||||
|
if (!validateAvailabilityData.success) {
|
||||||
|
selectedRoomAvailabilityFailCounter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
promotionCode,
|
||||||
|
reservationProfileType,
|
||||||
|
error_type: "validation_error",
|
||||||
|
error: JSON.stringify(validateAvailabilityData.error),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotels.selectedRoomAvailability validation error",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params },
|
||||||
|
error: validateAvailabilityData.error,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
throw badRequestError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedRoom = validateAvailabilityData.data.roomConfigurations
|
||||||
|
.filter((room) => room.status === "Available")
|
||||||
|
.find((room) => room.roomTypeCode === roomTypeCode)
|
||||||
|
|
||||||
|
if (!selectedRoom) {
|
||||||
|
console.error("No matching room found")
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const memberRate = selectedRoom.products.find(
|
||||||
|
(rate) => rate.productType.member?.rateCode === rateCode
|
||||||
|
)?.productType.member
|
||||||
|
|
||||||
|
const publicRate = selectedRoom.products.find(
|
||||||
|
(rate) => rate.productType.public?.rateCode === rateCode
|
||||||
|
)?.productType.public
|
||||||
|
|
||||||
|
const mustBeGuaranteed =
|
||||||
|
validateAvailabilityData.data.rateDefinitions.filter(
|
||||||
|
(rate) => rate.rateCode === rateCode
|
||||||
|
)[0].mustBeGuaranteed
|
||||||
|
|
||||||
|
const cancellationText =
|
||||||
|
validateAvailabilityData.data.rateDefinitions.find(
|
||||||
|
(rate) => rate.rateCode === rateCode
|
||||||
|
)?.cancellationText ?? ""
|
||||||
|
|
||||||
|
selectedRoomAvailabilitySuccessCounter.add(1, {
|
||||||
|
hotelId,
|
||||||
|
roomStayStartDate,
|
||||||
|
roomStayEndDate,
|
||||||
|
adults,
|
||||||
|
children,
|
||||||
|
promotionCode,
|
||||||
|
reservationProfileType,
|
||||||
|
})
|
||||||
|
console.info(
|
||||||
|
"api.hotels.selectedRoomAvailability success",
|
||||||
|
JSON.stringify({
|
||||||
|
query: { hotelId, params: params },
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
selectedRoom,
|
||||||
|
mustBeGuaranteed,
|
||||||
|
cancellationText,
|
||||||
|
memberRate,
|
||||||
|
publicRate,
|
||||||
|
}
|
||||||
|
}),
|
||||||
}),
|
}),
|
||||||
rates: router({
|
rates: router({
|
||||||
get: publicProcedure
|
get: publicProcedure
|
||||||
@@ -584,7 +750,7 @@ export const hotelQueryRouter = router({
|
|||||||
}),
|
}),
|
||||||
hotelData: router({
|
hotelData: router({
|
||||||
get: serviceProcedure
|
get: serviceProcedure
|
||||||
.input(getlHotelDataInputSchema)
|
.input(getHotelDataInputSchema)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const { hotelId, language, include, isCardOnlyPayment } = input
|
const { hotelId, language, include, isCardOnlyPayment } = input
|
||||||
|
|
||||||
|
|||||||
@@ -87,8 +87,11 @@ export const roomSchema = z
|
|||||||
name: data.attributes.name,
|
name: data.attributes.name,
|
||||||
occupancy: data.attributes.occupancy,
|
occupancy: data.attributes.occupancy,
|
||||||
roomSize: data.attributes.roomSize,
|
roomSize: data.attributes.roomSize,
|
||||||
|
roomTypes: data.attributes.roomTypes,
|
||||||
sortOrder: data.attributes.sortOrder,
|
sortOrder: data.attributes.sortOrder,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
roomFacilities: data.attributes.roomFacilities,
|
roomFacilities: data.attributes.roomFacilities,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export type RoomType = Pick<z.output<typeof roomSchema>, "roomTypes" | "name">
|
||||||
|
|||||||
@@ -1,30 +1,33 @@
|
|||||||
import { produce } from "immer"
|
import { produce } from "immer"
|
||||||
|
import { ReadonlyURLSearchParams } from "next/navigation"
|
||||||
import { createContext, useContext } from "react"
|
import { createContext, useContext } from "react"
|
||||||
import { create, useStore } from "zustand"
|
import { create, useStore } from "zustand"
|
||||||
|
|
||||||
import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema"
|
import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema"
|
||||||
import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
|
import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
|
||||||
import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema"
|
import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema"
|
||||||
|
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||||
|
|
||||||
import { BreakfastPackage } from "@/types/components/enterDetails/breakfast"
|
import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData"
|
||||||
import { DetailsSchema } from "@/types/components/enterDetails/details"
|
import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
|
||||||
import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek"
|
import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
|
||||||
import { StepEnum } from "@/types/components/enterDetails/step"
|
import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek"
|
||||||
import { BedTypeEnum } from "@/types/enums/bedType"
|
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
|
||||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||||
|
|
||||||
const SESSION_STORAGE_KEY = "enterDetails"
|
const SESSION_STORAGE_KEY = "enterDetails"
|
||||||
|
|
||||||
interface EnterDetailsState {
|
interface EnterDetailsState {
|
||||||
data: {
|
userData: {
|
||||||
bedType: BedTypeEnum | undefined
|
bedType: string | undefined
|
||||||
breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined
|
breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined
|
||||||
} & DetailsSchema
|
} & DetailsSchema
|
||||||
|
roomData: BookingData
|
||||||
steps: StepEnum[]
|
steps: StepEnum[]
|
||||||
currentStep: StepEnum
|
currentStep: StepEnum
|
||||||
activeSidePeek: SidePeekEnum | null
|
activeSidePeek: SidePeekEnum | null
|
||||||
isValid: Record<StepEnum, boolean>
|
isValid: Record<StepEnum, boolean>
|
||||||
completeStep: (updatedData: Partial<EnterDetailsState["data"]>) => void
|
completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void
|
||||||
navigate: (
|
navigate: (
|
||||||
step: StepEnum,
|
step: StepEnum,
|
||||||
updatedData?: Record<string, string | boolean | BreakfastPackage>
|
updatedData?: Record<string, string | boolean | BreakfastPackage>
|
||||||
@@ -34,13 +37,21 @@ interface EnterDetailsState {
|
|||||||
closeSidePeek: () => void
|
closeSidePeek: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initEditDetailsState(currentStep: StepEnum) {
|
export function initEditDetailsState(
|
||||||
|
currentStep: StepEnum,
|
||||||
|
searchParams: ReadonlyURLSearchParams
|
||||||
|
) {
|
||||||
const isBrowser = typeof window !== "undefined"
|
const isBrowser = typeof window !== "undefined"
|
||||||
const sessionData = isBrowser
|
const sessionData = isBrowser
|
||||||
? sessionStorage.getItem(SESSION_STORAGE_KEY)
|
? sessionStorage.getItem(SESSION_STORAGE_KEY)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
const defaultData: EnterDetailsState["data"] = {
|
let roomData: BookingData
|
||||||
|
if (searchParams?.size) {
|
||||||
|
roomData = getQueryParamsForEnterDetails(searchParams)
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultUserData: EnterDetailsState["userData"] = {
|
||||||
bedType: undefined,
|
bedType: undefined,
|
||||||
breakfast: undefined,
|
breakfast: undefined,
|
||||||
countryCode: "",
|
countryCode: "",
|
||||||
@@ -54,14 +65,14 @@ export function initEditDetailsState(currentStep: StepEnum) {
|
|||||||
termsAccepted: false,
|
termsAccepted: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
let inputData = {}
|
let inputUserData = {}
|
||||||
if (sessionData) {
|
if (sessionData) {
|
||||||
inputData = JSON.parse(sessionData)
|
inputUserData = JSON.parse(sessionData)
|
||||||
}
|
}
|
||||||
|
|
||||||
const validPaths = [StepEnum.selectBed]
|
const validPaths = [StepEnum.selectBed]
|
||||||
|
|
||||||
let initialData: EnterDetailsState["data"] = defaultData
|
let initialData: EnterDetailsState["userData"] = defaultUserData
|
||||||
|
|
||||||
const isValid = {
|
const isValid = {
|
||||||
[StepEnum.selectBed]: false,
|
[StepEnum.selectBed]: false,
|
||||||
@@ -70,19 +81,19 @@ export function initEditDetailsState(currentStep: StepEnum) {
|
|||||||
[StepEnum.payment]: false,
|
[StepEnum.payment]: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const validatedBedType = bedTypeSchema.safeParse(inputData)
|
const validatedBedType = bedTypeSchema.safeParse(inputUserData)
|
||||||
if (validatedBedType.success) {
|
if (validatedBedType.success) {
|
||||||
validPaths.push(StepEnum.breakfast)
|
validPaths.push(StepEnum.breakfast)
|
||||||
initialData = { ...initialData, ...validatedBedType.data }
|
initialData = { ...initialData, ...validatedBedType.data }
|
||||||
isValid[StepEnum.selectBed] = true
|
isValid[StepEnum.selectBed] = true
|
||||||
}
|
}
|
||||||
const validatedBreakfast = breakfastStoreSchema.safeParse(inputData)
|
const validatedBreakfast = breakfastStoreSchema.safeParse(inputUserData)
|
||||||
if (validatedBreakfast.success) {
|
if (validatedBreakfast.success) {
|
||||||
validPaths.push(StepEnum.details)
|
validPaths.push(StepEnum.details)
|
||||||
initialData = { ...initialData, ...validatedBreakfast.data }
|
initialData = { ...initialData, ...validatedBreakfast.data }
|
||||||
isValid[StepEnum.breakfast] = true
|
isValid[StepEnum.breakfast] = true
|
||||||
}
|
}
|
||||||
const validatedDetails = detailsSchema.safeParse(inputData)
|
const validatedDetails = detailsSchema.safeParse(inputUserData)
|
||||||
if (validatedDetails.success) {
|
if (validatedDetails.success) {
|
||||||
validPaths.push(StepEnum.payment)
|
validPaths.push(StepEnum.payment)
|
||||||
initialData = { ...initialData, ...validatedDetails.data }
|
initialData = { ...initialData, ...validatedDetails.data }
|
||||||
@@ -101,7 +112,8 @@ export function initEditDetailsState(currentStep: StepEnum) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return create<EnterDetailsState>()((set, get) => ({
|
return create<EnterDetailsState>()((set, get) => ({
|
||||||
data: initialData,
|
userData: initialData,
|
||||||
|
roomData,
|
||||||
steps: Object.values(StepEnum),
|
steps: Object.values(StepEnum),
|
||||||
setCurrentStep: (step) => set({ currentStep: step }),
|
setCurrentStep: (step) => set({ currentStep: step }),
|
||||||
navigate: (step, updatedData) =>
|
navigate: (step, updatedData) =>
|
||||||
@@ -129,14 +141,17 @@ export function initEditDetailsState(currentStep: StepEnum) {
|
|||||||
isValid,
|
isValid,
|
||||||
completeStep: (updatedData) =>
|
completeStep: (updatedData) =>
|
||||||
set(
|
set(
|
||||||
produce((state) => {
|
produce((state: EnterDetailsState) => {
|
||||||
state.isValid[state.currentStep] = true
|
state.isValid[state.currentStep] = true
|
||||||
|
|
||||||
const nextStep =
|
const nextStep =
|
||||||
state.steps[state.steps.indexOf(state.currentStep) + 1]
|
state.steps[state.steps.indexOf(state.currentStep) + 1]
|
||||||
|
|
||||||
state.data = { ...state.data, ...updatedData }
|
// @ts-expect-error: ts has a hard time understanding that "false | true" equals "boolean"
|
||||||
|
state.userData = {
|
||||||
|
...state.userData,
|
||||||
|
...updatedData,
|
||||||
|
}
|
||||||
state.currentStep = nextStep
|
state.currentStep = nextStep
|
||||||
get().navigate(nextStep, updatedData)
|
get().navigate(nextStep, updatedData)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,4 +2,16 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema"
|
import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema"
|
||||||
|
|
||||||
|
type BedType = {
|
||||||
|
description: string
|
||||||
|
size: {
|
||||||
|
min: number
|
||||||
|
max: number
|
||||||
|
}
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
export type BedTypeProps = {
|
||||||
|
bedTypes: BedType[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface BedTypeSchema extends z.output<typeof bedTypeSchema> {}
|
export interface BedTypeSchema extends z.output<typeof bedTypeSchema> {}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
interface Child {
|
||||||
|
bed: string
|
||||||
|
age: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Room {
|
||||||
|
adults: number
|
||||||
|
roomtype?: string
|
||||||
|
ratecode?: string
|
||||||
|
child?: Child[]
|
||||||
|
}
|
||||||
|
export interface BookingData {
|
||||||
|
hotel: string
|
||||||
|
fromDate: string
|
||||||
|
toDate: string
|
||||||
|
room: Room[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type Price = {
|
||||||
|
price?: string
|
||||||
|
currency?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RoomsData = {
|
||||||
|
roomType: string
|
||||||
|
localPrice: Price
|
||||||
|
euroPrice: Price
|
||||||
|
adults: number
|
||||||
|
children?: Child[]
|
||||||
|
cancellationText: string
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { StepEnum } from "../../enterDetails/step"
|
import { StepEnum } from "../enterDetails/step"
|
||||||
|
|
||||||
export interface SectionAccordionProps {
|
export interface SectionAccordionProps {
|
||||||
header: string
|
header: string
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ export interface Child {
|
|||||||
|
|
||||||
interface Room {
|
interface Room {
|
||||||
adults: number
|
adults: number
|
||||||
roomcode?: string
|
roomtype?: string
|
||||||
ratecode?: string
|
ratecode?: string
|
||||||
|
counterratecode?: string
|
||||||
child?: Child[]
|
child?: Child[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
8
utils/format.ts
Normal file
8
utils/format.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
/// Function to format numbers with space as thousands separator
|
||||||
|
export function formatNumber(num: number) {
|
||||||
|
// sv hardcoded to force space on thousands
|
||||||
|
const formatter = new Intl.NumberFormat(Lang.sv)
|
||||||
|
return formatter.format(num)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user