merge
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import Breadcrumbs from "@/components/Breadcrumbs"
|
||||
import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton"
|
||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import Breadcrumbs from "@/components/Breadcrumbs"
|
||||
import BreadcrumbsSkeleton from "@/components/Breadcrumbs/BreadcrumbsSkeleton"
|
||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||
import { setLang } from "@/i18n/serverContext"
|
||||
|
||||
import { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
.layout {
|
||||
background-color: var(--Base-Background-Primary-Normal);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import styles from "./layout.module.css"
|
||||
|
||||
import { LangParams, LayoutArgs } from "@/types/params"
|
||||
|
||||
export default function PaymentCallbackLayout({
|
||||
children,
|
||||
}: React.PropsWithChildren<LayoutArgs<LangParams>>) {
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
return notFound()
|
||||
}
|
||||
return <div className={styles.layout}>{children}</div>
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { redirect } from "next/navigation"
|
||||
|
||||
import {
|
||||
BOOKING_CONFIRMATION_NUMBER,
|
||||
PaymentErrorCodeEnum,
|
||||
} from "@/constants/booking"
|
||||
import { Lang } from "@/constants/languages"
|
||||
import {
|
||||
bookingConfirmation,
|
||||
payment,
|
||||
} from "@/constants/routes/hotelReservation"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import PaymentCallback from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback"
|
||||
|
||||
import { LangParams, PageArgs } from "@/types/params"
|
||||
|
||||
export default async function PaymentCallbackPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageArgs<
|
||||
LangParams,
|
||||
{ status: "error" | "success" | "cancel"; confirmationNumber?: string }
|
||||
>) {
|
||||
console.log(`[payment-callback] callback started`)
|
||||
const lang = params.lang
|
||||
const status = searchParams.status
|
||||
const confirmationNumber = searchParams.confirmationNumber
|
||||
|
||||
if (status === "success" && confirmationNumber) {
|
||||
const confirmationUrl = `${bookingConfirmation(lang)}?${BOOKING_CONFIRMATION_NUMBER}=${confirmationNumber}`
|
||||
|
||||
console.log(`[payment-callback] redirecting to: ${confirmationUrl}`)
|
||||
redirect(confirmationUrl)
|
||||
}
|
||||
|
||||
const returnUrl = payment(lang)
|
||||
const searchObject = new URLSearchParams()
|
||||
|
||||
if (confirmationNumber) {
|
||||
try {
|
||||
const bookingStatus = await serverClient().booking.status({
|
||||
confirmationNumber,
|
||||
})
|
||||
if (bookingStatus.metadata) {
|
||||
searchObject.set(
|
||||
"errorCode",
|
||||
bookingStatus.metadata.errorCode?.toString() ?? ""
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||
)
|
||||
if (status === "cancel") {
|
||||
searchObject.set("errorCode", PaymentErrorCodeEnum.Cancelled.toString())
|
||||
}
|
||||
if (status === "error") {
|
||||
searchObject.set("errorCode", PaymentErrorCodeEnum.Failed.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PaymentCallback
|
||||
returnUrl={returnUrl.toString()}
|
||||
searchObject={searchObject}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,10 @@
|
||||
padding: var(--Spacing-x3) var(--Spacing-x2) 0 var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.header nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cityInformation {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -65,13 +69,19 @@
|
||||
var(--Spacing-x5);
|
||||
}
|
||||
|
||||
.header nav {
|
||||
display: block;
|
||||
max-width: var(--max-width-navigation);
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.sorter {
|
||||
display: block;
|
||||
width: 339px;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0 auto;
|
||||
margin: var(--Spacing-x3) auto 0;
|
||||
display: flex;
|
||||
max-width: var(--max-width-navigation);
|
||||
align-items: center;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { notFound } from "next/navigation"
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { selectHotelMap } from "@/constants/routes/hotelReservation"
|
||||
import {
|
||||
selectHotel,
|
||||
selectHotelMap,
|
||||
} from "@/constants/routes/hotelReservation"
|
||||
import { getLocations } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import {
|
||||
@@ -19,6 +23,8 @@ import {
|
||||
import { ChevronRightIcon } from "@/components/Icons"
|
||||
import StaticMap from "@/components/Maps/StaticMap"
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
import Breadcrumbs from "@/components/TempDesignSystem/Breadcrumbs"
|
||||
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
@@ -65,12 +71,36 @@ export default async function SelectHotelPage({
|
||||
})
|
||||
|
||||
const filterList = getFiltersFromHotels(hotels)
|
||||
const breadcrumbs = [
|
||||
{
|
||||
title: intl.formatMessage({ id: "Home" }),
|
||||
href: `/${params.lang}`,
|
||||
uid: "home-page",
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: "Hotel reservation" }),
|
||||
href: `/${params.lang}/hotelreservation`,
|
||||
uid: "hotel-reservation",
|
||||
},
|
||||
{
|
||||
title: intl.formatMessage({ id: "Select hotel" }),
|
||||
href: `${selectHotel(params.lang)}/?${selectHotelParams}`,
|
||||
uid: "select-hotel",
|
||||
},
|
||||
{
|
||||
title: city.name,
|
||||
uid: city.id,
|
||||
},
|
||||
]
|
||||
|
||||
const isAllUnavailable = hotels.every((hotel) => hotel.price === undefined)
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className={styles.header}>
|
||||
<Suspense fallback={<BreadcrumbsSkeleton />}>
|
||||
<Breadcrumbs breadcrumbs={breadcrumbs} />
|
||||
</Suspense>
|
||||
<div className={styles.title}>
|
||||
<div className={styles.cityInformation}>
|
||||
<Subtitle>{city.name}</Subtitle>
|
||||
|
||||
@@ -64,32 +64,34 @@ export default async function SummaryPage({
|
||||
redirect(selectRate(params.lang))
|
||||
}
|
||||
|
||||
const prices =
|
||||
user && availability.memberRate
|
||||
const prices = {
|
||||
public: {
|
||||
local: {
|
||||
amount: availability.publicRate.localPrice.pricePerStay,
|
||||
currency: availability.publicRate.localPrice.currency,
|
||||
},
|
||||
euro: availability.publicRate?.requestedPrice
|
||||
? {
|
||||
amount: availability.publicRate?.requestedPrice.pricePerStay,
|
||||
currency: availability.publicRate?.requestedPrice.currency,
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
member: availability.memberRate
|
||||
? {
|
||||
local: {
|
||||
price: availability.memberRate.localPrice.pricePerStay,
|
||||
amount: availability.memberRate.localPrice.pricePerStay,
|
||||
currency: availability.memberRate.localPrice.currency,
|
||||
},
|
||||
euro: availability.memberRate.requestedPrice
|
||||
? {
|
||||
price: availability.memberRate.requestedPrice.pricePerStay,
|
||||
amount: availability.memberRate.requestedPrice.pricePerStay,
|
||||
currency: availability.memberRate.requestedPrice.currency,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: {
|
||||
local: {
|
||||
price: availability.publicRate.localPrice.pricePerStay,
|
||||
currency: availability.publicRate.localPrice.currency,
|
||||
},
|
||||
euro: availability.publicRate?.requestedPrice
|
||||
? {
|
||||
price: availability.publicRate?.requestedPrice.pricePerStay,
|
||||
currency: availability.publicRate?.requestedPrice.currency,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
: undefined,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -100,8 +102,7 @@ export default async function SummaryPage({
|
||||
showMemberPrice={!!(user && availability.memberRate)}
|
||||
room={{
|
||||
roomType: availability.selectedRoom.roomType,
|
||||
localPrice: prices.local,
|
||||
euroPrice: prices.euro,
|
||||
prices,
|
||||
adults,
|
||||
children,
|
||||
rateDetails: availability.rateDetails,
|
||||
@@ -119,8 +120,7 @@ export default async function SummaryPage({
|
||||
showMemberPrice={!!(user && availability.memberRate)}
|
||||
room={{
|
||||
roomType: availability.selectedRoom.roomType,
|
||||
localPrice: prices.local,
|
||||
euroPrice: prices.euro,
|
||||
prices,
|
||||
adults,
|
||||
children,
|
||||
rateDetails: availability.rateDetails,
|
||||
|
||||
@@ -171,6 +171,7 @@ export default async function StepPage({
|
||||
label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod}
|
||||
>
|
||||
<Payment
|
||||
user={user}
|
||||
roomPrice={roomPrice}
|
||||
otherPaymentOptions={
|
||||
hotelData.data.attributes.merchantInformationData
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
|
||||
import {
|
||||
BOOKING_CONFIRMATION_NUMBER,
|
||||
PaymentErrorCodeEnum,
|
||||
} from "@/constants/booking"
|
||||
import { Lang } from "@/constants/languages"
|
||||
import {
|
||||
bookingConfirmation,
|
||||
payment,
|
||||
} from "@/constants/routes/hotelReservation"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
import { getPublicURL } from "@/server/utils"
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
{ params }: { params: { lang: string; status: string } }
|
||||
): Promise<NextResponse> {
|
||||
const publicURL = getPublicURL(request)
|
||||
|
||||
console.log(`[payment-callback] callback started`)
|
||||
const lang = params.lang as Lang
|
||||
const status = params.status
|
||||
|
||||
const queryParams = request.nextUrl.searchParams
|
||||
const confirmationNumber = queryParams.get(BOOKING_CONFIRMATION_NUMBER)
|
||||
|
||||
if (status === "success" && confirmationNumber) {
|
||||
const confirmationUrl = new URL(`${publicURL}/${bookingConfirmation(lang)}`)
|
||||
confirmationUrl.searchParams.set(
|
||||
BOOKING_CONFIRMATION_NUMBER,
|
||||
confirmationNumber
|
||||
)
|
||||
|
||||
console.log(`[payment-callback] redirecting to: ${confirmationUrl}`)
|
||||
return NextResponse.redirect(confirmationUrl)
|
||||
}
|
||||
|
||||
const returnUrl = new URL(`${publicURL}/${payment(lang)}`)
|
||||
returnUrl.search = queryParams.toString()
|
||||
|
||||
if (confirmationNumber) {
|
||||
try {
|
||||
const bookingStatus = await serverClient().booking.status({
|
||||
confirmationNumber,
|
||||
})
|
||||
if (bookingStatus.metadata) {
|
||||
returnUrl.searchParams.set(
|
||||
"errorCode",
|
||||
bookingStatus.metadata.errorCode?.toString() ?? ""
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[payment-callback] failed to get booking status for ${confirmationNumber}, status: ${status}`
|
||||
)
|
||||
|
||||
if (status === "cancel") {
|
||||
returnUrl.searchParams.set(
|
||||
"errorCode",
|
||||
PaymentErrorCodeEnum.Cancelled.toString()
|
||||
)
|
||||
}
|
||||
if (status === "error") {
|
||||
returnUrl.searchParams.set(
|
||||
"errorCode",
|
||||
PaymentErrorCodeEnum.Failed.toString()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[payment-callback] redirecting to: ${returnUrl}`)
|
||||
return NextResponse.redirect(returnUrl)
|
||||
}
|
||||
@@ -1,60 +1,13 @@
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import { ChevronRightSmallIcon,HouseIcon } from "@/components/Icons"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
|
||||
import styles from "./breadcrumbs.module.css"
|
||||
import BreadcrumbsComp from "@/components/TempDesignSystem/Breadcrumbs"
|
||||
|
||||
export default async function Breadcrumbs() {
|
||||
const breadcrumbs = await serverClient().contentstack.breadcrumbs.get()
|
||||
|
||||
if (!breadcrumbs?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const homeBreadcrumb = breadcrumbs.shift()
|
||||
return (
|
||||
<nav className={styles.breadcrumbs}>
|
||||
<ul className={styles.list}>
|
||||
{homeBreadcrumb ? (
|
||||
<li className={styles.listItem}>
|
||||
<Link
|
||||
className={styles.homeLink}
|
||||
color="peach80"
|
||||
href={homeBreadcrumb.href!}
|
||||
variant="breadcrumb"
|
||||
>
|
||||
<HouseIcon width={16} height={16} color="peach80" />
|
||||
</Link>
|
||||
<ChevronRightSmallIcon aria-hidden="true" color="peach80" />
|
||||
</li>
|
||||
) : null}
|
||||
|
||||
{breadcrumbs.map((breadcrumb) => {
|
||||
if (breadcrumb.href) {
|
||||
return (
|
||||
<li key={breadcrumb.uid} className={styles.listItem}>
|
||||
<Link
|
||||
color="peach80"
|
||||
href={breadcrumb.href}
|
||||
variant="breadcrumb"
|
||||
>
|
||||
{breadcrumb.title}
|
||||
</Link>
|
||||
<ChevronRightSmallIcon aria-hidden="true" color="peach80" />
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={breadcrumb.uid} className={styles.listItem}>
|
||||
<Footnote color="burgundy" type="bold">
|
||||
{breadcrumb.title}
|
||||
</Footnote>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
return <BreadcrumbsComp breadcrumbs={breadcrumbs} />
|
||||
}
|
||||
|
||||
@@ -55,4 +55,8 @@ export const signedInDetailsSchema = z.object({
|
||||
firstName: z.string().optional(),
|
||||
lastName: z.string().optional(),
|
||||
phoneNumber: z.string().optional(),
|
||||
join: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.transform((_) => false),
|
||||
})
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect } from "react"
|
||||
|
||||
import { detailsStorageName } from "@/stores/details"
|
||||
|
||||
import { createQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
|
||||
import { DetailsState } from "@/types/stores/details"
|
||||
|
||||
export default function PaymentCallback({
|
||||
returnUrl,
|
||||
searchObject,
|
||||
}: {
|
||||
returnUrl: string
|
||||
searchObject: URLSearchParams
|
||||
}) {
|
||||
const router = useRouter()
|
||||
|
||||
useEffect(() => {
|
||||
const bookingData = window.sessionStorage.getItem(detailsStorageName)
|
||||
|
||||
if (bookingData) {
|
||||
const detailsStorage: Record<
|
||||
"state",
|
||||
Pick<DetailsState, "data">
|
||||
> = JSON.parse(bookingData)
|
||||
const searchParams = createQueryParamsForEnterDetails(
|
||||
detailsStorage.state.data.booking,
|
||||
searchObject
|
||||
)
|
||||
|
||||
if (searchParams.size > 0) {
|
||||
router.replace(`${returnUrl}?${searchParams.toString()}`)
|
||||
}
|
||||
}
|
||||
}, [returnUrl, router, searchObject])
|
||||
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
@@ -51,6 +51,7 @@ function isPaymentMethodEnum(value: string): value is PaymentMethodEnum {
|
||||
}
|
||||
|
||||
export default function Payment({
|
||||
user,
|
||||
roomPrice,
|
||||
otherPaymentOptions,
|
||||
savedCreditCards,
|
||||
@@ -59,7 +60,6 @@ export default function Payment({
|
||||
const router = useRouter()
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
const queryParams = useSearchParams()
|
||||
const { booking, ...userData } = useDetailsStore((state) => state.data)
|
||||
const setIsSubmittingDisabled = useDetailsStore(
|
||||
(state) => state.actions.setIsSubmittingDisabled
|
||||
@@ -163,9 +163,6 @@ export default function Payment({
|
||||
])
|
||||
|
||||
function handleSubmit(data: PaymentFormData) {
|
||||
const allQueryParams =
|
||||
queryParams.size > 0 ? `?${queryParams.toString()}` : ""
|
||||
|
||||
// set payment method to card if saved card is submitted
|
||||
const paymentMethod = isPaymentMethodEnum(data.paymentMethod)
|
||||
? data.paymentMethod
|
||||
@@ -175,6 +172,8 @@ export default function Payment({
|
||||
(card) => card.id === data.paymentMethod
|
||||
)
|
||||
|
||||
const paymentRedirectUrl = `${env.NEXT_PUBLIC_NODE_ENV === "development" ? `http://localhost:${env.NEXT_PUBLIC_PORT}` : ""}/${lang}/hotelreservation/payment-callback`
|
||||
|
||||
initiateBooking.mutate({
|
||||
hotelId: hotel,
|
||||
checkInDate: fromDate,
|
||||
@@ -185,7 +184,8 @@ export default function Payment({
|
||||
age: child.age,
|
||||
bedType: bedTypeMap[parseInt(child.bed.toString())],
|
||||
})),
|
||||
rateCode: room.rateCode,
|
||||
rateCode:
|
||||
user || join || membershipNo ? room.counterRateCode : room.rateCode,
|
||||
roomTypeCode: bedType!.roomTypeCode, // A selection has been made in order to get to this step.
|
||||
guest: {
|
||||
title: "",
|
||||
@@ -222,9 +222,9 @@ export default function Payment({
|
||||
}
|
||||
: undefined,
|
||||
|
||||
success: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/success`,
|
||||
error: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/error${allQueryParams}`,
|
||||
cancel: `${env.NEXT_PUBLIC_PAYMENT_CALLBACK_URL}/${lang}/cancel${allQueryParams}`,
|
||||
success: `${paymentRedirectUrl}/success`,
|
||||
error: `${paymentRedirectUrl}/error`,
|
||||
cancel: `${paymentRedirectUrl}/cancel`,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export function SummaryBottomSheet({ children }: PropsWithChildren) {
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(totalPrice.local.price),
|
||||
amount: intl.formatNumber(totalPrice.local.amount),
|
||||
currency: totalPrice.local.currency,
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { ChevronDown } from "react-feather"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
@@ -33,6 +33,8 @@ function storeSelector(state: DetailsState) {
|
||||
toggleSummaryOpen: state.actions.toggleSummaryOpen,
|
||||
setTotalPrice: state.actions.setTotalPrice,
|
||||
totalPrice: state.totalPrice,
|
||||
join: state.data.join,
|
||||
membershipNo: state.data.membershipNo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +53,8 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
toDate,
|
||||
toggleSummaryOpen,
|
||||
totalPrice,
|
||||
join,
|
||||
membershipNo,
|
||||
} = useDetailsStore(storeSelector)
|
||||
|
||||
const diff = dt(toDate).diff(fromDate, "days")
|
||||
@@ -60,10 +64,8 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
{ totalNights: diff }
|
||||
)
|
||||
|
||||
let color: "uiTextHighContrast" | "red" = "uiTextHighContrast"
|
||||
if (showMemberPrice) {
|
||||
color = "red"
|
||||
}
|
||||
const color = useRef<"uiTextHighContrast" | "red">("uiTextHighContrast")
|
||||
const [price, setPrice] = useState(room.prices.public)
|
||||
|
||||
const additionalPackageCost = room.packages?.reduce(
|
||||
(acc, curr) => {
|
||||
@@ -74,11 +76,23 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
{ local: 0, euro: 0 }
|
||||
) || { local: 0, euro: 0 }
|
||||
|
||||
const roomsPriceLocal = room.localPrice.price + additionalPackageCost.local
|
||||
const roomsPriceEuro = room.euroPrice
|
||||
? room.euroPrice.price + additionalPackageCost.euro
|
||||
const roomsPriceLocal = price.local.amount + additionalPackageCost.local
|
||||
const roomsPriceEuro = price.euro
|
||||
? price.euro.amount + additionalPackageCost.euro
|
||||
: undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (showMemberPrice || join || membershipNo) {
|
||||
color.current = "red"
|
||||
if (room.prices.member) {
|
||||
setPrice(room.prices.member)
|
||||
}
|
||||
} else {
|
||||
color.current = "uiTextHighContrast"
|
||||
setPrice(room.prices.public)
|
||||
}
|
||||
}, [showMemberPrice, join, membershipNo, room.prices])
|
||||
|
||||
useEffect(() => {
|
||||
setChosenBed(bedType)
|
||||
|
||||
@@ -87,30 +101,30 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
if (breakfast === false) {
|
||||
setTotalPrice({
|
||||
local: {
|
||||
price: roomsPriceLocal,
|
||||
currency: room.localPrice.currency,
|
||||
amount: roomsPriceLocal,
|
||||
currency: price.local.currency,
|
||||
},
|
||||
euro:
|
||||
room.euroPrice && roomsPriceEuro
|
||||
price.euro && roomsPriceEuro
|
||||
? {
|
||||
price: roomsPriceEuro,
|
||||
currency: room.euroPrice.currency,
|
||||
amount: roomsPriceEuro,
|
||||
currency: price.euro.currency,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
} else {
|
||||
setTotalPrice({
|
||||
local: {
|
||||
price: roomsPriceLocal + parseInt(breakfast.localPrice.totalPrice),
|
||||
currency: room.localPrice.currency,
|
||||
amount: roomsPriceLocal + parseInt(breakfast.localPrice.totalPrice),
|
||||
currency: price.local.currency,
|
||||
},
|
||||
euro:
|
||||
room.euroPrice && roomsPriceEuro
|
||||
price.euro && roomsPriceEuro
|
||||
? {
|
||||
price:
|
||||
amount:
|
||||
roomsPriceEuro +
|
||||
parseInt(breakfast.requestedPrice.totalPrice),
|
||||
currency: room.euroPrice.currency,
|
||||
currency: price.euro.currency,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
@@ -120,8 +134,8 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
bedType,
|
||||
breakfast,
|
||||
roomsPriceLocal,
|
||||
room.localPrice.currency,
|
||||
room.euroPrice,
|
||||
price.local.currency,
|
||||
price.euro,
|
||||
roomsPriceEuro,
|
||||
setTotalPrice,
|
||||
])
|
||||
@@ -151,12 +165,12 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
<div>
|
||||
<div className={styles.entry}>
|
||||
<Body color="uiTextHighContrast">{room.roomType}</Body>
|
||||
<Caption color={color}>
|
||||
<Caption color={color.current}>
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(room.localPrice.price),
|
||||
currency: room.localPrice.currency,
|
||||
amount: intl.formatNumber(price.local.amount),
|
||||
currency: price.local.currency,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
@@ -229,7 +243,7 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
<Caption color="uiTextHighContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{ amount: "0", currency: room.localPrice.currency }
|
||||
{ amount: "0", currency: price.local.currency }
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
@@ -243,7 +257,7 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{ amount: "0", currency: room.localPrice.currency }
|
||||
{ amount: "0", currency: price.local.currency }
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
@@ -279,22 +293,24 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(totalPrice.local.price),
|
||||
currency: totalPrice.local.currency,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
{totalPrice.euro && (
|
||||
{totalPrice.local.amount > 0 && (
|
||||
<Body textTransform="bold">
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(totalPrice.local.amount),
|
||||
currency: totalPrice.local.currency,
|
||||
}
|
||||
)}
|
||||
</Body>
|
||||
)}
|
||||
{totalPrice.euro && totalPrice.euro.amount > 0 && (
|
||||
<Caption color="uiTextMediumContrast">
|
||||
{intl.formatMessage({ id: "Approx." })}{" "}
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} {currency}" },
|
||||
{
|
||||
amount: intl.formatNumber(totalPrice.euro.price),
|
||||
amount: intl.formatNumber(totalPrice.euro.amount),
|
||||
currency: totalPrice.euro.currency,
|
||||
}
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
"use client"
|
||||
import { useSearchParams } from "next/navigation"
|
||||
import { useEffect, useMemo, useState } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { useHotelFilterStore } from "@/stores/hotel-filters"
|
||||
|
||||
import Alert from "@/components/TempDesignSystem/Alert"
|
||||
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
|
||||
|
||||
import HotelCard from "../HotelCard"
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
type HotelData,
|
||||
} from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps"
|
||||
import { SortOrder } from "@/types/components/hotelReservation/selectHotel/hotelSorter"
|
||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||
|
||||
export default function HotelCardListing({
|
||||
hotelData,
|
||||
@@ -28,6 +31,7 @@ export default function HotelCardListing({
|
||||
const activeFilters = useHotelFilterStore((state) => state.activeFilters)
|
||||
const setResultCount = useHotelFilterStore((state) => state.setResultCount)
|
||||
const [showBackToTop, setShowBackToTop] = useState<boolean>(false)
|
||||
const intl = useIntl()
|
||||
|
||||
const sortBy = useMemo(
|
||||
() => searchParams.get("sort") ?? DEFAULT_SORT,
|
||||
@@ -69,7 +73,6 @@ export default function HotelCardListing({
|
||||
|
||||
const hotels = useMemo(() => {
|
||||
if (activeFilters.length === 0) {
|
||||
setResultCount(sortedHotels.length)
|
||||
return sortedHotels
|
||||
}
|
||||
|
||||
@@ -81,9 +84,8 @@ export default function HotelCardListing({
|
||||
)
|
||||
)
|
||||
|
||||
setResultCount(filteredHotels.length)
|
||||
return filteredHotels
|
||||
}, [activeFilters, sortedHotels, setResultCount])
|
||||
}, [activeFilters, sortedHotels])
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
@@ -95,23 +97,33 @@ export default function HotelCardListing({
|
||||
return () => window.removeEventListener("scroll", handleScroll)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setResultCount(hotels ? hotels.length : 0)
|
||||
}, [hotels, setResultCount])
|
||||
|
||||
function scrollToTop() {
|
||||
window.scrollTo({ top: 0, behavior: "smooth" })
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.hotelCards}>
|
||||
{hotels?.length
|
||||
? hotels.map((hotel) => (
|
||||
<HotelCard
|
||||
key={hotel.hotelData.operaId}
|
||||
hotel={hotel}
|
||||
type={type}
|
||||
state={hotel.hotelData.name === activeCard ? "active" : "default"}
|
||||
onHotelCardHover={onHotelCardHover}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
{hotels?.length ? (
|
||||
hotels.map((hotel) => (
|
||||
<HotelCard
|
||||
key={hotel.hotelData.operaId}
|
||||
hotel={hotel}
|
||||
type={type}
|
||||
state={hotel.hotelData.name === activeCard ? "active" : "default"}
|
||||
onHotelCardHover={onHotelCardHover}
|
||||
/>
|
||||
))
|
||||
) : activeFilters ? (
|
||||
<Alert
|
||||
type={AlertTypeEnum.Info}
|
||||
heading={intl.formatMessage({ id: "filters.nohotel.heading" })}
|
||||
text={intl.formatMessage({ id: "filters.nohotel.text" })}
|
||||
/>
|
||||
) : null}
|
||||
{showBackToTop && <BackToTopButton onClick={scrollToTop} />}
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -99,11 +99,13 @@ export default function RoomFilter({
|
||||
<form onSubmit={handleSubmit(submitFilter)}>
|
||||
<div className={styles.roomsFilter}>
|
||||
{filterOptions.map((option) => {
|
||||
const { code, description } = option
|
||||
const { code, description, itemCode } = option
|
||||
const isPetRoom = code === RoomPackageCodeEnum.PET_ROOM
|
||||
const isAllergyRoom = code === RoomPackageCodeEnum.ALLERGY_ROOM
|
||||
const isDisabled =
|
||||
(isAllergyRoom && petFriendly) || (isPetRoom && allergyFriendly)
|
||||
(isAllergyRoom && petFriendly) ||
|
||||
(isPetRoom && allergyFriendly) ||
|
||||
!itemCode
|
||||
|
||||
const checkboxChip = (
|
||||
<CheckboxChip
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function FlexibilityOption({
|
||||
</div>
|
||||
<Label size="regular" className={styles.noPricesLabel}>
|
||||
<Caption color="uiTextHighContrast" type="bold">
|
||||
{intl.formatMessage({ id: "No Prices available" })}
|
||||
{intl.formatMessage({ id: "No prices available" })}
|
||||
</Caption>
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function RoomSelection({
|
||||
roomsAvailability,
|
||||
roomCategories,
|
||||
user,
|
||||
packages,
|
||||
availablePackages,
|
||||
selectedPackages,
|
||||
setRateCode,
|
||||
rateSummary,
|
||||
@@ -72,7 +72,7 @@ export default function RoomSelection({
|
||||
roomCategories={roomCategories}
|
||||
handleSelectRate={setRateCode}
|
||||
selectedPackages={selectedPackages}
|
||||
packages={packages}
|
||||
packages={availablePackages}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
@@ -81,7 +81,7 @@ export default function RoomSelection({
|
||||
<RateSummary
|
||||
rateSummary={rateSummary}
|
||||
isUserLoggedIn={isUserLoggedIn}
|
||||
packages={packages}
|
||||
packages={availablePackages}
|
||||
roomsAvailability={roomsAvailability}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -50,6 +50,54 @@ export function getQueryParamsForEnterDetails(
|
||||
roomTypeCode: room.roomtype,
|
||||
rateCode: room.ratecode,
|
||||
packages: room.packages?.split(",") as RoomPackageCodeEnum[],
|
||||
counterRateCode: room.counterratecode,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
export function createQueryParamsForEnterDetails(
|
||||
bookingData: BookingData,
|
||||
intitalSearchParams: URLSearchParams
|
||||
) {
|
||||
const { hotel, fromDate, toDate, rooms } = bookingData
|
||||
|
||||
const bookingSearchParams = new URLSearchParams({ hotel, fromDate, toDate })
|
||||
const searchParams = new URLSearchParams([
|
||||
...intitalSearchParams,
|
||||
...bookingSearchParams,
|
||||
])
|
||||
|
||||
rooms.forEach((item, index) => {
|
||||
if (item?.adults) {
|
||||
searchParams.set(`room[${index}].adults`, item.adults.toString())
|
||||
}
|
||||
if (item?.children) {
|
||||
item.children.forEach((child, childIndex) => {
|
||||
searchParams.set(
|
||||
`room[${index}].child[${childIndex}].age`,
|
||||
child.age.toString()
|
||||
)
|
||||
searchParams.set(
|
||||
`room[${index}].child[${childIndex}].bed`,
|
||||
child.bed.toString()
|
||||
)
|
||||
})
|
||||
}
|
||||
if (item?.roomTypeCode) {
|
||||
searchParams.set(`room[${index}].roomtype`, item.roomTypeCode)
|
||||
}
|
||||
if (item?.rateCode) {
|
||||
searchParams.set(`room[${index}].ratecode`, item.rateCode)
|
||||
}
|
||||
|
||||
if (item?.counterRateCode) {
|
||||
searchParams.set(`room[${index}].counterratecode`, item.counterRateCode)
|
||||
}
|
||||
|
||||
if (item.packages && item.packages.length > 0) {
|
||||
searchParams.set(`room[${index}].packages`, item.packages.join(","))
|
||||
}
|
||||
})
|
||||
|
||||
return searchParams
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ export async function RoomsContainer({
|
||||
return (
|
||||
<Rooms
|
||||
user={user}
|
||||
packages={packages ?? []}
|
||||
availablePackages={packages ?? []}
|
||||
roomsAvailability={roomsAvailability}
|
||||
roomCategories={hotelData?.included ?? []}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,7 @@ import { filterDuplicateRoomTypesByLowestPrice } from "./utils"
|
||||
import styles from "./rooms.module.css"
|
||||
|
||||
import {
|
||||
DefaultFilterOptions,
|
||||
RoomPackageCodeEnum,
|
||||
type RoomPackageCodes,
|
||||
} from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
@@ -20,17 +21,39 @@ export default function Rooms({
|
||||
roomsAvailability,
|
||||
roomCategories = [],
|
||||
user,
|
||||
packages,
|
||||
availablePackages,
|
||||
}: SelectRateProps) {
|
||||
const visibleRooms: RoomConfiguration[] =
|
||||
filterDuplicateRoomTypesByLowestPrice(roomsAvailability.roomConfigurations)
|
||||
// const [internalRateSummary, setRateSummary] = useState<Rate | null>(null)
|
||||
const [selectedRate, setSelectedRate] = useState<
|
||||
{ publicRateCode: string; roomTypeCode: string } | undefined
|
||||
>(undefined)
|
||||
const [selectedPackages, setSelectedPackages] = useState<RoomPackageCodes[]>(
|
||||
[]
|
||||
)
|
||||
const defaultPackages: DefaultFilterOptions[] = [
|
||||
{
|
||||
code: RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
|
||||
description: "Accessible Room",
|
||||
itemCode: availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.ACCESSIBILITY_ROOM
|
||||
)?.itemCode,
|
||||
},
|
||||
{
|
||||
code: RoomPackageCodeEnum.ALLERGY_ROOM,
|
||||
description: "Allergy Room",
|
||||
itemCode: availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.ALLERGY_ROOM
|
||||
)?.itemCode,
|
||||
},
|
||||
{
|
||||
code: RoomPackageCodeEnum.PET_ROOM,
|
||||
description: "Pet Room",
|
||||
itemCode: availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)?.itemCode,
|
||||
},
|
||||
]
|
||||
|
||||
const handleFilter = useCallback(
|
||||
(filter: Record<RoomPackageCodeEnum, boolean | undefined>) => {
|
||||
@@ -39,7 +62,6 @@ export default function Rooms({
|
||||
) as RoomPackageCodeEnum[]
|
||||
|
||||
setSelectedPackages(filteredPackages)
|
||||
// setRateSummary(null)
|
||||
},
|
||||
[]
|
||||
)
|
||||
@@ -94,7 +116,9 @@ export default function Rooms({
|
||||
|
||||
const petRoomPackage =
|
||||
(selectedPackages.includes(RoomPackageCodeEnum.PET_ROOM) &&
|
||||
packages.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) ||
|
||||
availablePackages.find(
|
||||
(pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM
|
||||
)) ||
|
||||
undefined
|
||||
|
||||
const features = filteredRooms.find((room) =>
|
||||
@@ -113,7 +137,7 @@ export default function Rooms({
|
||||
}
|
||||
|
||||
return rateSummary
|
||||
}, [filteredRooms, packages, selectedPackages, selectedRate])
|
||||
}, [filteredRooms, availablePackages, selectedPackages, selectedRate])
|
||||
|
||||
useEffect(() => {
|
||||
if (rateSummary) return
|
||||
@@ -127,13 +151,13 @@ export default function Rooms({
|
||||
<RoomFilter
|
||||
numberOfRooms={rooms.roomConfigurations.length}
|
||||
onFilter={handleFilter}
|
||||
filterOptions={packages}
|
||||
filterOptions={defaultPackages}
|
||||
/>
|
||||
<RoomSelection
|
||||
roomsAvailability={rooms}
|
||||
roomCategories={roomCategories}
|
||||
user={user}
|
||||
packages={packages}
|
||||
availablePackages={availablePackages}
|
||||
selectedPackages={selectedPackages}
|
||||
setRateCode={setSelectedRate}
|
||||
rateSummary={rateSummary}
|
||||
|
||||
@@ -227,7 +227,7 @@
|
||||
|
||||
.galleryContent {
|
||||
width: 1090px;
|
||||
height: 725px;
|
||||
height: min(725px, 85dvh);
|
||||
}
|
||||
|
||||
.fullViewContent {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { ChevronRightIcon, HouseIcon } from "@/components/Icons"
|
||||
import styles from "@/components/TempDesignSystem/Breadcrumbs/breadcrumbs.module.css"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
|
||||
import styles from "./breadcrumbs.module.css"
|
||||
|
||||
export default function BreadcrumbsSkeleton() {
|
||||
return (
|
||||
<nav className={styles.breadcrumbs}>
|
||||
@@ -1,8 +1,6 @@
|
||||
.breadcrumbs {
|
||||
display: block;
|
||||
padding-left: var(--Spacing-x2);
|
||||
padding-right: var(--Spacing-x2);
|
||||
padding-top: var(--Spacing-x2);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x2) 0;
|
||||
max-width: var(--max-width);
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
9
components/TempDesignSystem/Breadcrumbs/breadcrumbs.ts
Normal file
9
components/TempDesignSystem/Breadcrumbs/breadcrumbs.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
type Breadcrumb = {
|
||||
title: string
|
||||
uid: string
|
||||
href?: string
|
||||
}
|
||||
|
||||
export interface BreadcrumbsProps {
|
||||
breadcrumbs: Breadcrumb[]
|
||||
}
|
||||
61
components/TempDesignSystem/Breadcrumbs/index.tsx
Normal file
61
components/TempDesignSystem/Breadcrumbs/index.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { HouseIcon } from "@/components/Icons"
|
||||
import ChevronRightSmallIcon from "@/components/Icons/ChevronRightSmall"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
|
||||
|
||||
import styles from "./breadcrumbs.module.css"
|
||||
|
||||
import type { BreadcrumbsProps } from "@/components/TempDesignSystem/Breadcrumbs/breadcrumbs"
|
||||
|
||||
export default function Breadcrumbs({ breadcrumbs }: BreadcrumbsProps) {
|
||||
if (!breadcrumbs?.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const homeBreadcrumb = breadcrumbs.shift()
|
||||
return (
|
||||
<nav className={styles.breadcrumbs}>
|
||||
<ul className={styles.list}>
|
||||
{homeBreadcrumb ? (
|
||||
<li className={styles.listItem}>
|
||||
<Link
|
||||
className={styles.homeLink}
|
||||
color="peach80"
|
||||
href={homeBreadcrumb.href!}
|
||||
variant="breadcrumb"
|
||||
aria-label={homeBreadcrumb.title}
|
||||
>
|
||||
<HouseIcon width={16} height={16} color="peach80" />
|
||||
</Link>
|
||||
<ChevronRightSmallIcon aria-hidden="true" color="peach80" />
|
||||
</li>
|
||||
) : null}
|
||||
|
||||
{breadcrumbs.map((breadcrumb) => {
|
||||
if (breadcrumb.href) {
|
||||
return (
|
||||
<li key={breadcrumb.uid} className={styles.listItem}>
|
||||
<Link
|
||||
color="peach80"
|
||||
href={breadcrumb.href}
|
||||
variant="breadcrumb"
|
||||
>
|
||||
{breadcrumb.title}
|
||||
</Link>
|
||||
<ChevronRightSmallIcon aria-hidden="true" color="peach80" />
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={breadcrumb.uid} className={styles.listItem}>
|
||||
<Footnote color="burgundy" type="bold">
|
||||
{breadcrumb.title}
|
||||
</Footnote>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
@@ -2,11 +2,13 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x-half);
|
||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
||||
padding: calc(var(--Spacing-x1) - 2px) var(--Spacing-x-one-and-half);
|
||||
border: 1px solid var(--Base-Border-Subtle);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
cursor: pointer;
|
||||
height: 32px;
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
}
|
||||
|
||||
.label[data-selected="true"],
|
||||
@@ -21,8 +23,9 @@
|
||||
}
|
||||
|
||||
.label[data-disabled="true"] {
|
||||
background-color: var(--Base-Button-Primary-Fill-Disabled);
|
||||
border-color: var(--Base-Button-Primary-Fill-Disabled);
|
||||
background-color: var(--UI-Input-Controls-Surface-Disabled);
|
||||
border-color: var(--UI-Input-Controls-Border-Disabled);
|
||||
color: var(--Base-Text-Disabled);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
|
||||
4
env/client.ts
vendored
4
env/client.ts
vendored
@@ -5,14 +5,10 @@ export const env = createEnv({
|
||||
client: {
|
||||
NEXT_PUBLIC_NODE_ENV: z.enum(["development", "test", "production"]),
|
||||
NEXT_PUBLIC_PORT: z.string().default("3000"),
|
||||
NEXT_PUBLIC_PAYMENT_CALLBACK_URL: z
|
||||
.string()
|
||||
.default("/api/web/payment-callback"),
|
||||
},
|
||||
emptyStringAsUndefined: true,
|
||||
runtimeEnv: {
|
||||
NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,
|
||||
NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT,
|
||||
NEXT_PUBLIC_PAYMENT_CALLBACK_URL: `${process.env.NODE_ENV === "development" ? `http://localhost:${process.env.NEXT_PUBLIC_PORT}` : ""}/api/web/payment-callback`,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -150,9 +150,11 @@
|
||||
"Gym": "Fitnesscenter",
|
||||
"Hi": "Hei",
|
||||
"Highest level": "Højeste niveau",
|
||||
"Home": "Hjem",
|
||||
"Hospital": "Hospital",
|
||||
"Hotel": "Hotel",
|
||||
"Hotel facilities": "Hotel faciliteter",
|
||||
"Hotel reservation": "Hotel reservation",
|
||||
"Hotel surroundings": "Hotel omgivelser",
|
||||
"Hotel(s)": "{amount} {amount, plural, one {hotel} other {hoteller}}",
|
||||
"Hotels": "Hoteller",
|
||||
@@ -227,6 +229,7 @@
|
||||
"No breakfast": "Ingen morgenmad",
|
||||
"No content published": "Intet indhold offentliggjort",
|
||||
"No matching location found": "Der blev ikke fundet nogen matchende placering",
|
||||
"No prices available": "Ingen tilgængelige priser",
|
||||
"No results": "Ingen resultater",
|
||||
"No transactions available": "Ingen tilgængelige transaktioner",
|
||||
"No, keep card": "Nej, behold kortet",
|
||||
@@ -324,6 +327,7 @@
|
||||
"Select country of residence": "Vælg bopælsland",
|
||||
"Select date of birth": "Vælg fødselsdato",
|
||||
"Select dates": "Vælg datoer",
|
||||
"Select hotel": "Vælg hotel",
|
||||
"Select language": "Vælg sprog",
|
||||
"Select payment method": "Vælg betalingsmetode",
|
||||
"Select your language": "Vælg dit sprog",
|
||||
|
||||
@@ -150,9 +150,11 @@
|
||||
"Gym": "Fitnessstudio",
|
||||
"Hi": "Hallo",
|
||||
"Highest level": "Höchstes Level",
|
||||
"Home": "Heim",
|
||||
"Hospital": "Krankenhaus",
|
||||
"Hotel": "Hotel",
|
||||
"Hotel facilities": "Hotel-Infos",
|
||||
"Hotel reservation": "Hotelreservierung",
|
||||
"Hotel surroundings": "Umgebung des Hotels",
|
||||
"Hotel(s)": "{amount} {amount, plural, one {hotel} other {hotels}}",
|
||||
"Hotels": "Hotels",
|
||||
@@ -225,6 +227,7 @@
|
||||
"No breakfast": "Kein Frühstück",
|
||||
"No content published": "Kein Inhalt veröffentlicht",
|
||||
"No matching location found": "Kein passender Standort gefunden",
|
||||
"No prices available": "Keine Preise verfügbar",
|
||||
"No results": "Keine Ergebnisse",
|
||||
"No transactions available": "Keine Transaktionen verfügbar",
|
||||
"No, keep card": "Nein, Karte behalten",
|
||||
@@ -323,6 +326,7 @@
|
||||
"Select country of residence": "Wählen Sie das Land Ihres Wohnsitzes aus",
|
||||
"Select date of birth": "Geburtsdatum auswählen",
|
||||
"Select dates": "Datum auswählen",
|
||||
"Select hotel": "Hotel auswählen",
|
||||
"Select language": "Sprache auswählen",
|
||||
"Select payment method": "Zahlungsart auswählen",
|
||||
"Select your language": "Wählen Sie Ihre Sprache",
|
||||
|
||||
@@ -162,9 +162,11 @@
|
||||
"Gym": "Gym",
|
||||
"Hi": "Hi",
|
||||
"Highest level": "Highest level",
|
||||
"Home": "Home",
|
||||
"Hospital": "Hospital",
|
||||
"Hotel": "Hotel",
|
||||
"Hotel facilities": "Hotel facilities",
|
||||
"Hotel reservation": "Hotel reservation",
|
||||
"Hotel surroundings": "Hotel surroundings",
|
||||
"Hotel(s)": "{amount} {amount, plural, one {hotel} other {hotels}}",
|
||||
"Hotels": "Hotels",
|
||||
@@ -244,6 +246,7 @@
|
||||
"No breakfast": "No breakfast",
|
||||
"No content published": "No content published",
|
||||
"No matching location found": "No matching location found",
|
||||
"No prices available": "No prices available",
|
||||
"No results": "No results",
|
||||
"No transactions available": "No transactions available",
|
||||
"No, keep card": "No, keep card",
|
||||
@@ -353,6 +356,7 @@
|
||||
"Select country of residence": "Select country of residence",
|
||||
"Select date of birth": "Select date of birth",
|
||||
"Select dates": "Select dates",
|
||||
"Select hotel": "Select hotel",
|
||||
"Select language": "Select language",
|
||||
"Select payment method": "Select payment method",
|
||||
"Select your language": "Select your language",
|
||||
@@ -469,6 +473,8 @@
|
||||
"breakfast.price.free": "<strikethrough>{amount} {currency}</strikethrough> <free>0 {currency}</free>/night",
|
||||
"by": "by",
|
||||
"characters": "characters",
|
||||
"filters.nohotel.heading": "No hotels match your filters",
|
||||
"filters.nohotel.text": "It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.",
|
||||
"from": "from",
|
||||
"guaranteeing": "guaranteeing",
|
||||
"guest": "guest",
|
||||
|
||||
@@ -150,9 +150,11 @@
|
||||
"Gym": "Kuntosali",
|
||||
"Hi": "Hi",
|
||||
"Highest level": "Korkein taso",
|
||||
"Home": "Kotiin",
|
||||
"Hospital": "Sairaala",
|
||||
"Hotel": "Hotelli",
|
||||
"Hotel facilities": "Hotellin palvelut",
|
||||
"Hotel reservation": "Hotellivaraukset",
|
||||
"Hotel surroundings": "Hotellin ympäristö",
|
||||
"Hotel(s)": "{amount} {amount, plural, one {hotelli} other {hotellit}}",
|
||||
"Hotels": "Hotellit",
|
||||
@@ -227,6 +229,7 @@
|
||||
"No breakfast": "Ei aamiaista",
|
||||
"No content published": "Ei julkaistua sisältöä",
|
||||
"No matching location found": "Vastaavaa sijaintia ei löytynyt",
|
||||
"No prices available": "Hintoja ei ole saatavilla",
|
||||
"No results": "Ei tuloksia",
|
||||
"No transactions available": "Ei tapahtumia saatavilla",
|
||||
"No, keep card": "Ei, pidä kortti",
|
||||
@@ -325,6 +328,7 @@
|
||||
"Select country of residence": "Valitse asuinmaa",
|
||||
"Select date of birth": "Valitse syntymäaika",
|
||||
"Select dates": "Valitse päivämäärät",
|
||||
"Select hotel": "Valitse hotelli",
|
||||
"Select language": "Valitse kieli",
|
||||
"Select payment method": "Valitse maksutapa",
|
||||
"Select your language": "Valitse kieli",
|
||||
|
||||
@@ -149,9 +149,11 @@
|
||||
"Gym": "Treningsstudio",
|
||||
"Hi": "Hei",
|
||||
"Highest level": "Høyeste nivå",
|
||||
"Home": "Hjem",
|
||||
"Hospital": "Sykehus",
|
||||
"Hotel": "Hotel",
|
||||
"Hotel facilities": "Hotelfaciliteter",
|
||||
"Hotel reservation": "Hotellreservasjon",
|
||||
"Hotel surroundings": "Hotellomgivelser",
|
||||
"Hotel(s)": "{amount} {amount, plural, one {hotell} other {hoteller}}",
|
||||
"Hotels": "Hoteller",
|
||||
@@ -225,6 +227,7 @@
|
||||
"No breakfast": "Ingen frokost",
|
||||
"No content published": "Ingen innhold publisert",
|
||||
"No matching location found": "Fant ingen samsvarende plassering",
|
||||
"No prices available": "Ingen priser tilgjengelig",
|
||||
"No results": "Ingen resultater",
|
||||
"No transactions available": "Ingen transaksjoner tilgjengelig",
|
||||
"No, keep card": "Nei, behold kortet",
|
||||
@@ -322,6 +325,7 @@
|
||||
"Select country of residence": "Velg bostedsland",
|
||||
"Select date of birth": "Velg fødselsdato",
|
||||
"Select dates": "Velg datoer",
|
||||
"Select hotel": "Velg hotell",
|
||||
"Select language": "Velg språk",
|
||||
"Select payment method": "Velg betalingsmetode",
|
||||
"Select your language": "Velg språk",
|
||||
|
||||
@@ -149,9 +149,11 @@
|
||||
"Gym": "Gym",
|
||||
"Hi": "Hej",
|
||||
"Highest level": "Högsta nivå",
|
||||
"Home": "Hem",
|
||||
"Hospital": "Sjukhus",
|
||||
"Hotel": "Hotell",
|
||||
"Hotel facilities": "Hotellfaciliteter",
|
||||
"Hotel reservation": "Hotellbokning",
|
||||
"Hotel surroundings": "Hotellomgivning",
|
||||
"Hotel(s)": "{amount} hotell",
|
||||
"Hotels": "Hotell",
|
||||
@@ -225,6 +227,7 @@
|
||||
"No breakfast": "Ingen frukost",
|
||||
"No content published": "Inget innehåll publicerat",
|
||||
"No matching location found": "Ingen matchande plats hittades",
|
||||
"No prices available": "Inga priser tillgängliga",
|
||||
"No results": "Inga resultat",
|
||||
"No transactions available": "Inga transaktioner tillgängliga",
|
||||
"No, keep card": "Nej, behåll kortet",
|
||||
@@ -322,6 +325,7 @@
|
||||
"Select country of residence": "Välj bosättningsland",
|
||||
"Select date of birth": "Välj födelsedatum",
|
||||
"Select dates": "Välj datum",
|
||||
"Select hotel": "Välj hotell",
|
||||
"Select language": "Välj språk",
|
||||
"Select payment method": "Välj betalningsmetod",
|
||||
"Select your language": "Välj ditt språk",
|
||||
|
||||
@@ -2,30 +2,14 @@ import "server-only"
|
||||
|
||||
import deepmerge from "deepmerge"
|
||||
|
||||
import { arrayMerge } from "@/utils/merge"
|
||||
|
||||
import { request } from "./request"
|
||||
|
||||
import type { BatchRequestDocument } from "graphql-request"
|
||||
|
||||
import type { Data } from "@/types/request"
|
||||
|
||||
function arrayMerge(
|
||||
target: any[],
|
||||
source: any[],
|
||||
options: deepmerge.ArrayMergeOptions | undefined
|
||||
) {
|
||||
const destination = target.slice()
|
||||
source.forEach((item, index) => {
|
||||
if (typeof destination[index] === "undefined") {
|
||||
destination[index] = options?.cloneUnlessOtherwiseSpecified(item, options)
|
||||
} else if (options?.isMergeableObject(item)) {
|
||||
destination[index] = deepmerge(target[index], item, options)
|
||||
} else if (target.indexOf(item) === -1) {
|
||||
destination.push(item)
|
||||
}
|
||||
})
|
||||
return destination
|
||||
}
|
||||
|
||||
export async function batchRequest<T>(
|
||||
queries: (BatchRequestDocument & { options?: RequestInit })[]
|
||||
): Promise<Data<T>> {
|
||||
|
||||
@@ -282,6 +282,11 @@ const nextConfig = {
|
||||
"/:lang/hotelreservation/:step(breakfast|details|payment|select-bed)",
|
||||
destination: "/:lang/hotelreservation/step?step=:step",
|
||||
},
|
||||
{
|
||||
source: "/:lang/hotelreservation/payment-callback/:status",
|
||||
destination:
|
||||
"/:lang/hotelreservation/payment-callback?status=:status",
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
||||
@@ -17,13 +17,14 @@ export const createBookingSchema = z
|
||||
paymentUrl: z.string().nullable(),
|
||||
metadata: z
|
||||
.object({
|
||||
errorCode: z.number().optional(),
|
||||
errorMessage: z.string().optional(),
|
||||
errorCode: z.number().nullable().optional(),
|
||||
errorMessage: z.string().nullable().optional(),
|
||||
priceChangedMetadata: z
|
||||
.object({
|
||||
roomPrice: z.number().optional(),
|
||||
totalPrice: z.number().optional(),
|
||||
roomPrice: z.number().nullable().optional(),
|
||||
totalPrice: z.number().nullable().optional(),
|
||||
})
|
||||
.nullable()
|
||||
.optional(),
|
||||
})
|
||||
.nullable(),
|
||||
|
||||
@@ -112,10 +112,10 @@ const hotelContentSchema = z.object({
|
||||
}),
|
||||
}),
|
||||
restaurantsOverviewPage: z.object({
|
||||
restaurantsOverviewPageLinkText: z.string(),
|
||||
restaurantsOverviewPageLink: z.string(),
|
||||
restaurantsContentDescriptionShort: z.string(),
|
||||
restaurantsContentDescriptionMedium: z.string(),
|
||||
restaurantsOverviewPageLinkText: z.string().optional(),
|
||||
restaurantsOverviewPageLink: z.string().optional(),
|
||||
restaurantsContentDescriptionShort: z.string().optional(),
|
||||
restaurantsContentDescriptionMedium: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -864,22 +864,24 @@ export const packagesSchema = z.object({
|
||||
|
||||
export const getRoomPackagesSchema = z
|
||||
.object({
|
||||
data: z.object({
|
||||
attributes: z.object({
|
||||
hotelId: z.number(),
|
||||
packages: z.array(packagesSchema).optional().default([]),
|
||||
}),
|
||||
relationships: z
|
||||
.object({
|
||||
links: z.array(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
type: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
.optional(),
|
||||
type: z.string(),
|
||||
}),
|
||||
data: z
|
||||
.object({
|
||||
attributes: z.object({
|
||||
hotelId: z.number(),
|
||||
packages: z.array(packagesSchema).optional().default([]),
|
||||
}),
|
||||
relationships: z
|
||||
.object({
|
||||
links: z.array(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
type: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
.optional(),
|
||||
type: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.transform((data) => data.data.attributes.packages)
|
||||
.transform((data) => data.data?.attributes?.packages ?? [])
|
||||
|
||||
@@ -939,12 +939,10 @@ export const hotelQueryRouter = router({
|
||||
"api.hotels.packages error",
|
||||
JSON.stringify({ query: { hotelId, params } })
|
||||
)
|
||||
throw serverErrorByStatus(apiResponse.status, apiResponse)
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const validatedPackagesData = getRoomPackagesSchema.safeParse(apiJson)
|
||||
|
||||
if (!validatedPackagesData.success) {
|
||||
getHotelFailCounter.add(1, {
|
||||
hotelId,
|
||||
|
||||
@@ -11,11 +11,12 @@ import {
|
||||
signedInDetailsSchema,
|
||||
} from "@/components/HotelReservation/EnterDetails/Details/schema"
|
||||
import { DetailsContext } from "@/contexts/Details"
|
||||
import { arrayMerge } from "@/utils/merge"
|
||||
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { DetailsState, InitialState } from "@/types/stores/details"
|
||||
|
||||
export const storageName = "details-storage"
|
||||
export const detailsStorageName = "details-storage"
|
||||
export function createDetailsStore(
|
||||
initialState: InitialState,
|
||||
isMember: boolean
|
||||
@@ -27,13 +28,15 @@ export function createDetailsStore(
|
||||
* we cannot use the data as `defaultValues` for our forms.
|
||||
* RHF caches defaultValues on mount.
|
||||
*/
|
||||
const detailsStorageUnparsed = sessionStorage.getItem(storageName)
|
||||
const detailsStorageUnparsed = sessionStorage.getItem(detailsStorageName)
|
||||
if (detailsStorageUnparsed) {
|
||||
const detailsStorage: Record<
|
||||
"state",
|
||||
Pick<DetailsState, "data">
|
||||
> = JSON.parse(detailsStorageUnparsed)
|
||||
initialState = merge(initialState, detailsStorage.state.data)
|
||||
initialState = merge(detailsStorage.state.data, initialState, {
|
||||
arrayMerge,
|
||||
})
|
||||
}
|
||||
}
|
||||
return create<DetailsState>()(
|
||||
@@ -135,40 +138,39 @@ export function createDetailsStore(
|
||||
},
|
||||
|
||||
totalPrice: {
|
||||
euro: { currency: "", price: 0 },
|
||||
local: { currency: "", price: 0 },
|
||||
euro: { currency: "", amount: 0 },
|
||||
local: { currency: "", amount: 0 },
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: storageName,
|
||||
onRehydrateStorage() {
|
||||
name: detailsStorageName,
|
||||
onRehydrateStorage(prevState) {
|
||||
return function (state) {
|
||||
if (state) {
|
||||
const validatedBedType = bedTypeSchema.safeParse(state.data)
|
||||
if (validatedBedType.success) {
|
||||
state.actions.updateValidity(StepEnum.selectBed, true)
|
||||
} else {
|
||||
state.actions.updateValidity(StepEnum.selectBed, false)
|
||||
if (validatedBedType.success !== state.isValid["select-bed"]) {
|
||||
state.isValid["select-bed"] = validatedBedType.success
|
||||
}
|
||||
|
||||
const validatedBreakfast = breakfastStoreSchema.safeParse(
|
||||
state.data
|
||||
)
|
||||
if (validatedBreakfast.success) {
|
||||
state.actions.updateValidity(StepEnum.breakfast, true)
|
||||
} else {
|
||||
state.actions.updateValidity(StepEnum.breakfast, false)
|
||||
if (validatedBreakfast.success !== state.isValid.breakfast) {
|
||||
state.isValid.breakfast = validatedBreakfast.success
|
||||
}
|
||||
|
||||
const detailsSchema = isMember
|
||||
? signedInDetailsSchema
|
||||
: guestDetailsSchema
|
||||
const validatedDetails = detailsSchema.safeParse(state.data)
|
||||
if (validatedDetails.success) {
|
||||
state.actions.updateValidity(StepEnum.details, true)
|
||||
} else {
|
||||
state.actions.updateValidity(StepEnum.details, false)
|
||||
if (validatedDetails.success !== state.isValid.details) {
|
||||
state.isValid.details = validatedDetails.success
|
||||
}
|
||||
|
||||
const mergedState = merge(state.data, prevState.data, {
|
||||
arrayMerge,
|
||||
})
|
||||
state.data = mergedState
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "@/components/HotelReservation/EnterDetails/Details/schema"
|
||||
import { StepsContext } from "@/contexts/Steps"
|
||||
|
||||
import { storageName as detailsStorageName } from "./details"
|
||||
import { detailsStorageName as detailsStorageName } from "./details"
|
||||
|
||||
import { StepEnum } from "@/types/enums/step"
|
||||
import type { DetailsState } from "@/types/stores/details"
|
||||
|
||||
@@ -7,6 +7,7 @@ interface Room {
|
||||
adults: number
|
||||
roomTypeCode: string
|
||||
rateCode: string
|
||||
counterRateCode: string
|
||||
children?: Child[]
|
||||
packages?: RoomPackageCodeEnum[]
|
||||
}
|
||||
@@ -18,14 +19,24 @@ export interface BookingData {
|
||||
}
|
||||
|
||||
type Price = {
|
||||
price: number
|
||||
amount: number
|
||||
currency: string
|
||||
}
|
||||
|
||||
export type RoomsData = {
|
||||
roomType: string
|
||||
localPrice: Price
|
||||
euroPrice: Price | undefined
|
||||
prices: {
|
||||
public: {
|
||||
local: Price
|
||||
euro: Price | undefined
|
||||
}
|
||||
member:
|
||||
| {
|
||||
local: Price
|
||||
euro: Price | undefined
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
adults: number
|
||||
children?: Child[]
|
||||
rateDetails?: string[]
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
getRoomPackagesSchema,
|
||||
packagesSchema,
|
||||
} from "@/server/routers/hotels/output"
|
||||
import { packagesSchema } from "@/server/routers/hotels/output"
|
||||
|
||||
export enum RoomPackageCodeEnum {
|
||||
PET_ROOM = "PETR",
|
||||
ALLERGY_ROOM = "ALLG",
|
||||
ACCESSIBILITY_ROOM = "ACCE",
|
||||
}
|
||||
|
||||
export interface DefaultFilterOptions {
|
||||
code: RoomPackageCodeEnum
|
||||
description: string
|
||||
itemCode: string | undefined
|
||||
}
|
||||
export interface RoomFilterProps {
|
||||
numberOfRooms: number
|
||||
onFilter: (filter: Record<string, boolean | undefined>) => void
|
||||
filterOptions: RoomPackageData
|
||||
filterOptions: DefaultFilterOptions[]
|
||||
}
|
||||
|
||||
export type RoomPackage = z.output<typeof packagesSchema>
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface RoomSelectionProps {
|
||||
roomsAvailability: RoomsAvailability
|
||||
roomCategories: RoomData[]
|
||||
user: SafeUser
|
||||
packages: RoomPackageData | undefined
|
||||
availablePackages: RoomPackageData | undefined
|
||||
selectedPackages: RoomPackageCodes[]
|
||||
setRateCode: (rateCode: {
|
||||
publicRateCode: string
|
||||
@@ -21,5 +21,5 @@ export interface SelectRateProps {
|
||||
roomsAvailability: RoomsAvailability
|
||||
roomCategories: RoomData[]
|
||||
user: SafeUser
|
||||
packages: RoomPackageData
|
||||
availablePackages: RoomPackageData
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CreditCard } from "@/types/user"
|
||||
import { CreditCard, SafeUser } from "@/types/user"
|
||||
|
||||
export interface SectionProps {
|
||||
nextPath: string
|
||||
@@ -28,6 +28,7 @@ export interface BreakfastSelectionProps extends SectionProps {
|
||||
export interface DetailsProps extends SectionProps {}
|
||||
|
||||
export interface PaymentProps {
|
||||
user: SafeUser
|
||||
roomPrice: { publicPrice: number; memberPrice: number | undefined }
|
||||
otherPaymentOptions: string[]
|
||||
savedCreditCards: CreditCard[] | null
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Room {
|
||||
adults: number
|
||||
roomtype: string
|
||||
ratecode: string
|
||||
counterratecode?: string
|
||||
counterratecode: string
|
||||
child?: Child[]
|
||||
packages?: string
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ export interface DetailsState {
|
||||
actions: {
|
||||
setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void
|
||||
setTotalPrice: (totalPrice: TotalPrice) => void
|
||||
toggleSummaryOpen: () => void,
|
||||
toggleSummaryOpen: () => void
|
||||
updateBedType: (data: BedTypeSchema) => void
|
||||
updateBreakfast: (data: BreakfastPackage | false) => void
|
||||
updateDetails: (data: DetailsSchema) => void
|
||||
@@ -31,10 +31,10 @@ export interface InitialState extends Partial<DetailsState> {
|
||||
|
||||
interface Price {
|
||||
currency: string
|
||||
price: number
|
||||
amount: number
|
||||
}
|
||||
|
||||
export interface TotalPrice {
|
||||
euro: Price | undefined
|
||||
local: Price
|
||||
}
|
||||
}
|
||||
|
||||
19
utils/merge.ts
Normal file
19
utils/merge.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import merge from "deepmerge"
|
||||
|
||||
export function arrayMerge(
|
||||
target: any[],
|
||||
source: any[],
|
||||
options: merge.ArrayMergeOptions
|
||||
) {
|
||||
const destination = target.slice()
|
||||
source.forEach((item, index) => {
|
||||
if (typeof destination[index] === "undefined") {
|
||||
destination[index] = options.cloneUnlessOtherwiseSpecified(item, options)
|
||||
} else if (options?.isMergeableObject(item)) {
|
||||
destination[index] = merge(target[index], item, options)
|
||||
} else if (target.indexOf(item) === -1) {
|
||||
destination.push(item)
|
||||
}
|
||||
})
|
||||
return destination
|
||||
}
|
||||
Reference in New Issue
Block a user