diff --git a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx index 6775fd188..048cf9e5f 100644 --- a/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/@breadcrumbs/[...path]/page.tsx @@ -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" diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx index ec8f14553..f8024a816 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/@breadcrumbs/page.tsx @@ -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" diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.module.css b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.module.css new file mode 100644 index 000000000..1730ffa68 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.module.css @@ -0,0 +1,3 @@ +.layout { + background-color: var(--Base-Background-Primary-Normal); +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx new file mode 100644 index 000000000..b9ad3b13c --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/layout.tsx @@ -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>) { + if (env.HIDE_FOR_NEXT_RELEASE) { + return notFound() + } + return
{children}
+} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx new file mode 100644 index 000000000..0e4e716f2 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(payment-callback)/payment-callback/page.tsx @@ -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 ( + + ) +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css index e42544196..cb7245753 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.module.css @@ -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; diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx index 62cab6b85..49893c54d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -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 ( <>
+ }> + +
{city.name} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx index 0444913f1..0fb655de6 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx @@ -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, diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index d175fc25f..4fbaae7ff 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -171,6 +171,7 @@ export default async function StepPage({ label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod} > { - 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) -} diff --git a/components/Breadcrumbs/index.tsx b/components/Breadcrumbs/index.tsx index a3244fab8..31a4d445a 100644 --- a/components/Breadcrumbs/index.tsx +++ b/components/Breadcrumbs/index.tsx @@ -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 ( - - ) + return } diff --git a/components/HotelReservation/EnterDetails/Details/schema.ts b/components/HotelReservation/EnterDetails/Details/schema.ts index abb29ac2b..b3e161dc9 100644 --- a/components/HotelReservation/EnterDetails/Details/schema.ts +++ b/components/HotelReservation/EnterDetails/Details/schema.ts @@ -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), }) diff --git a/components/HotelReservation/EnterDetails/Payment/PaymentCallback/index.tsx b/components/HotelReservation/EnterDetails/Payment/PaymentCallback/index.tsx new file mode 100644 index 000000000..1c303dfbe --- /dev/null +++ b/components/HotelReservation/EnterDetails/Payment/PaymentCallback/index.tsx @@ -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 + > = JSON.parse(bookingData) + const searchParams = createQueryParamsForEnterDetails( + detailsStorage.state.data.booking, + searchObject + ) + + if (searchParams.size > 0) { + router.replace(`${returnUrl}?${searchParams.toString()}`) + } + } + }, [returnUrl, router, searchObject]) + + return +} diff --git a/components/HotelReservation/EnterDetails/Payment/index.tsx b/components/HotelReservation/EnterDetails/Payment/index.tsx index a912f74b9..ab1f78807 100644 --- a/components/HotelReservation/EnterDetails/Payment/index.tsx +++ b/components/HotelReservation/EnterDetails/Payment/index.tsx @@ -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`, }, }) } diff --git a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx b/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx index 9f99a56c0..b6ea4b3c6 100644 --- a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx @@ -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, } )} diff --git a/components/HotelReservation/EnterDetails/Summary/index.tsx b/components/HotelReservation/EnterDetails/Summary/index.tsx index 5b0a6a420..5e2d14cb3 100644 --- a/components/HotelReservation/EnterDetails/Summary/index.tsx +++ b/components/HotelReservation/EnterDetails/Summary/index.tsx @@ -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) {
{room.roomType} - + {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, } )} @@ -229,7 +243,7 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: "0", currency: room.localPrice.currency } + { amount: "0", currency: price.local.currency } )}
@@ -243,7 +257,7 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) { {intl.formatMessage( { id: "{amount} {currency}" }, - { amount: "0", currency: room.localPrice.currency } + { amount: "0", currency: price.local.currency } )}
@@ -279,22 +293,24 @@ export default function Summary({ showMemberPrice, room }: SummaryProps) {
- - {intl.formatMessage( - { id: "{amount} {currency}" }, - { - amount: intl.formatNumber(totalPrice.local.price), - currency: totalPrice.local.currency, - } - )} - - {totalPrice.euro && ( + {totalPrice.local.amount > 0 && ( + + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: intl.formatNumber(totalPrice.local.amount), + currency: totalPrice.local.currency, + } + )} + + )} + {totalPrice.euro && totalPrice.euro.amount > 0 && ( {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, } )} diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx index 6ceb9aa85..f666c5ba2 100644 --- a/components/HotelReservation/HotelCardListing/index.tsx +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -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(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 (
- {hotels?.length - ? hotels.map((hotel) => ( - - )) - : null} + {hotels?.length ? ( + hotels.map((hotel) => ( + + )) + ) : activeFilters ? ( + + ) : null} {showBackToTop && }
) diff --git a/components/HotelReservation/SelectRate/RoomFilter/index.tsx b/components/HotelReservation/SelectRate/RoomFilter/index.tsx index a07e8353f..48c73cac2 100644 --- a/components/HotelReservation/SelectRate/RoomFilter/index.tsx +++ b/components/HotelReservation/SelectRate/RoomFilter/index.tsx @@ -99,11 +99,13 @@ export default function RoomFilter({
{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 = (
diff --git a/components/HotelReservation/SelectRate/RoomSelection/index.tsx b/components/HotelReservation/SelectRate/RoomSelection/index.tsx index f7c530521..18e2389c4 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/index.tsx +++ b/components/HotelReservation/SelectRate/RoomSelection/index.tsx @@ -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} /> ))} @@ -81,7 +81,7 @@ export default function RoomSelection({ )} diff --git a/components/HotelReservation/SelectRate/RoomSelection/utils.ts b/components/HotelReservation/SelectRate/RoomSelection/utils.ts index 43af470e3..c47841708 100644 --- a/components/HotelReservation/SelectRate/RoomSelection/utils.ts +++ b/components/HotelReservation/SelectRate/RoomSelection/utils.ts @@ -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 +} diff --git a/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx b/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx index 6d963e5b0..94d8031f1 100644 --- a/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx +++ b/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx @@ -88,7 +88,7 @@ export async function RoomsContainer({ return ( diff --git a/components/HotelReservation/SelectRate/Rooms/index.tsx b/components/HotelReservation/SelectRate/Rooms/index.tsx index 7a661e385..08f6b00c5 100644 --- a/components/HotelReservation/SelectRate/Rooms/index.tsx +++ b/components/HotelReservation/SelectRate/Rooms/index.tsx @@ -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(null) const [selectedRate, setSelectedRate] = useState< { publicRateCode: string; roomTypeCode: string } | undefined >(undefined) const [selectedPackages, setSelectedPackages] = useState( [] ) + 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) => { @@ -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({ diff --git a/components/Breadcrumbs/breadcrumbs.module.css b/components/TempDesignSystem/Breadcrumbs/breadcrumbs.module.css similarity index 82% rename from components/Breadcrumbs/breadcrumbs.module.css rename to components/TempDesignSystem/Breadcrumbs/breadcrumbs.module.css index 93bedd5a6..8a535c4f8 100644 --- a/components/Breadcrumbs/breadcrumbs.module.css +++ b/components/TempDesignSystem/Breadcrumbs/breadcrumbs.module.css @@ -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%; diff --git a/components/TempDesignSystem/Breadcrumbs/breadcrumbs.ts b/components/TempDesignSystem/Breadcrumbs/breadcrumbs.ts new file mode 100644 index 000000000..6a5a7468b --- /dev/null +++ b/components/TempDesignSystem/Breadcrumbs/breadcrumbs.ts @@ -0,0 +1,9 @@ +type Breadcrumb = { + title: string + uid: string + href?: string +} + +export interface BreadcrumbsProps { + breadcrumbs: Breadcrumb[] +} diff --git a/components/TempDesignSystem/Breadcrumbs/index.tsx b/components/TempDesignSystem/Breadcrumbs/index.tsx new file mode 100644 index 000000000..ed40cd519 --- /dev/null +++ b/components/TempDesignSystem/Breadcrumbs/index.tsx @@ -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 ( + + ) +} diff --git a/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css b/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css index 44fa78a14..fa029bb47 100644 --- a/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css +++ b/components/TempDesignSystem/Form/FilterChip/_Chip/chip.module.css @@ -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; } diff --git a/env/client.ts b/env/client.ts index 4eafd5592..467100c01 100644 --- a/env/client.ts +++ b/env/client.ts @@ -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`, }, }) diff --git a/i18n/dictionaries/da.json b/i18n/dictionaries/da.json index a9fdcfae4..9bea132a9 100644 --- a/i18n/dictionaries/da.json +++ b/i18n/dictionaries/da.json @@ -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", diff --git a/i18n/dictionaries/de.json b/i18n/dictionaries/de.json index 96bcef2de..d63fe2700 100644 --- a/i18n/dictionaries/de.json +++ b/i18n/dictionaries/de.json @@ -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", diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 385861316..4abc07ea7 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -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": "{amount} {currency} 0 {currency}/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", diff --git a/i18n/dictionaries/fi.json b/i18n/dictionaries/fi.json index 09fb8a403..f179163ee 100644 --- a/i18n/dictionaries/fi.json +++ b/i18n/dictionaries/fi.json @@ -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", diff --git a/i18n/dictionaries/no.json b/i18n/dictionaries/no.json index dcbcdddf4..2ed440701 100644 --- a/i18n/dictionaries/no.json +++ b/i18n/dictionaries/no.json @@ -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", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 404896e15..ee77155fe 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -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", diff --git a/lib/graphql/batchRequest.ts b/lib/graphql/batchRequest.ts index b6b5dbe3f..86361d527 100644 --- a/lib/graphql/batchRequest.ts +++ b/lib/graphql/batchRequest.ts @@ -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( queries: (BatchRequestDocument & { options?: RequestInit })[] ): Promise> { diff --git a/next.config.js b/next.config.js index 222f085ac..34616f754 100644 --- a/next.config.js +++ b/next.config.js @@ -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", + }, ], } }, diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts index ae2e14cf4..5c8879c00 100644 --- a/server/routers/booking/output.ts +++ b/server/routers/booking/output.ts @@ -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(), diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 41f9a6b52..f9a5710de 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -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 ?? []) diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 34a1b3722..eb31aacb6 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -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, diff --git a/stores/details.ts b/stores/details.ts index a382ad7d7..30bf1ed81 100644 --- a/stores/details.ts +++ b/stores/details.ts @@ -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 > = JSON.parse(detailsStorageUnparsed) - initialState = merge(initialState, detailsStorage.state.data) + initialState = merge(detailsStorage.state.data, initialState, { + arrayMerge, + }) } } return create()( @@ -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 } } }, diff --git a/stores/steps.ts b/stores/steps.ts index cf14f6768..efa356c8b 100644 --- a/stores/steps.ts +++ b/stores/steps.ts @@ -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" diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 0afabf91a..0683c4739 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -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[] diff --git a/types/components/hotelReservation/selectRate/roomFilter.ts b/types/components/hotelReservation/selectRate/roomFilter.ts index f895ed73a..2bdd501b1 100644 --- a/types/components/hotelReservation/selectRate/roomFilter.ts +++ b/types/components/hotelReservation/selectRate/roomFilter.ts @@ -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) => void - filterOptions: RoomPackageData + filterOptions: DefaultFilterOptions[] } export type RoomPackage = z.output diff --git a/types/components/hotelReservation/selectRate/roomSelection.ts b/types/components/hotelReservation/selectRate/roomSelection.ts index bfb750490..abc07cf6a 100644 --- a/types/components/hotelReservation/selectRate/roomSelection.ts +++ b/types/components/hotelReservation/selectRate/roomSelection.ts @@ -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 } diff --git a/types/components/hotelReservation/selectRate/section.ts b/types/components/hotelReservation/selectRate/section.ts index 05d86ff6c..eaea36f2e 100644 --- a/types/components/hotelReservation/selectRate/section.ts +++ b/types/components/hotelReservation/selectRate/section.ts @@ -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 diff --git a/types/components/hotelReservation/selectRate/selectRate.ts b/types/components/hotelReservation/selectRate/selectRate.ts index a1da31b84..12eb83eb1 100644 --- a/types/components/hotelReservation/selectRate/selectRate.ts +++ b/types/components/hotelReservation/selectRate/selectRate.ts @@ -11,7 +11,7 @@ interface Room { adults: number roomtype: string ratecode: string - counterratecode?: string + counterratecode: string child?: Child[] packages?: string } diff --git a/types/stores/details.ts b/types/stores/details.ts index ef6d101dc..d10bb0d61 100644 --- a/types/stores/details.ts +++ b/types/stores/details.ts @@ -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 { interface Price { currency: string - price: number + amount: number } export interface TotalPrice { euro: Price | undefined local: Price -} \ No newline at end of file +} diff --git a/utils/merge.ts b/utils/merge.ts new file mode 100644 index 000000000..84aaae145 --- /dev/null +++ b/utils/merge.ts @@ -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 +}