diff --git a/actions/registerUser.ts b/actions/registerUser.ts index ecd2318b4..8def69ec3 100644 --- a/actions/registerUser.ts +++ b/actions/registerUser.ts @@ -35,9 +35,7 @@ export const registerUser = serviceServerActionProcedure const payload = { ...input, language: ctx.lang, - phoneNumber: parsePhoneNumber(input.phoneNumber) - .formatNational() - .replace(/\s+/g, ""), + phoneNumber: input.phoneNumber.replace(/\s+/g, ""), } const parsedPayload = registerUserPayload.safeParse(payload) diff --git a/actions/registerUserBookingFlow.ts b/actions/registerUserBookingFlow.ts deleted file mode 100644 index a34cad231..000000000 --- a/actions/registerUserBookingFlow.ts +++ /dev/null @@ -1,67 +0,0 @@ -"use server" - -import { parsePhoneNumber } from "libphonenumber-js" -import { z } from "zod" - -import { serviceServerActionProcedure } from "@/server/trpc" - -import { phoneValidator } from "@/utils/phoneValidator" - -const registerUserPayload = z.object({ - firstName: z.string(), - lastName: z.string(), - dateOfBirth: z.string(), - address: z.object({ - countryCode: z.string(), - zipCode: z.string(), - }), - email: z.string(), - phoneNumber: phoneValidator("Phone is required"), -}) - -export const registerUserBookingFlow = serviceServerActionProcedure - .input(registerUserPayload) - .mutation(async function ({ ctx, input }) { - const payload = { - ...input, - language: ctx.lang, - phoneNumber: parsePhoneNumber(input.phoneNumber) - .formatNational() - .replace(/\s+/g, ""), - } - - // TODO: Consume the API to register the user as soon as passwordless signup is enabled. - // let apiResponse - // try { - // apiResponse = await api.post(api.endpoints.v1.Profile.profile, { - // body: payload, - // headers: { - // Authorization: `Bearer ${ctx.serviceToken}`, - // }, - // }) - // } catch (error) { - // console.error("Unexpected error", error) - // return { success: false, error: "Unexpected error" } - // } - - // if (!apiResponse.ok) { - // const text = await apiResponse.text() - // console.error(text) - // console.error( - // "registerUserBookingFlow api error", - // JSON.stringify({ - // query: input, - // error: { - // status: apiResponse.status, - // statusText: apiResponse.statusText, - // error: text, - // }, - // }) - // ) - // return { success: false, error: "API error" } - // } - // const json = await apiResponse.json() - // console.log("registerUserBookingFlow: json", json) - - return { success: true, data: payload } - }) diff --git a/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx b/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx index ac3fec31a..528b7b211 100644 --- a/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/[...path]/page.tsx @@ -18,7 +18,7 @@ export default async function MyPages({ setLang(params.lang) const accountPageRes = await serverClient().contentstack.accountPage.get() - const { formatMessage } = await getIntl() + const intl = await getIntl() if (!accountPageRes) { return null @@ -33,7 +33,7 @@ export default async function MyPages({ {accountPage.content?.length ? ( ) : ( -

{formatMessage({ id: "No content published" })}

+

{intl.formatMessage({ id: "No content published" })}

)} diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx index 43f1d66ef..4341b3ed0 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@communication/page.tsx @@ -13,15 +13,15 @@ export default async function CommunicationSlot({ }: PageArgs) { setLang(params.lang) - const { formatMessage } = await getIntl() + const intl = await getIntl() return (
- {formatMessage({ id: "My communication preferences" })} + {intl.formatMessage({ id: "My communication preferences" })} - {formatMessage({ + {intl.formatMessage({ id: "Tell us what information and updates you'd like to receive, and how, by clicking the link below.", })} diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx index 4dbfc1ab4..ea54e71fd 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@creditCards/page.tsx @@ -13,17 +13,17 @@ import { LangParams, PageArgs } from "@/types/params" export default async function CreditCardSlot({ params }: PageArgs) { setLang(params.lang) - const { formatMessage } = await getIntl() + const intl = await getIntl() const creditCards = await serverClient().user.creditCards() return (
- {formatMessage({ id: "My payment cards" })} + {intl.formatMessage({ id: "My payment cards" })} - {formatMessage({ + {intl.formatMessage({ id: "Check out the credit cards saved to your profile. Pay with a saved card when signed in for a smoother web experience.", })} diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx index 693bdc171..913cde993 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@membershipCards/page.tsx @@ -15,14 +15,14 @@ export default async function MembershipCardSlot({ params, }: PageArgs) { setLang(params.lang) - const { formatMessage } = await getIntl() + const intl = await getIntl() const membershipCards = await getMembershipCards() return (
- {formatMessage({ id: "My membership cards" })} + {intl.formatMessage({ id: "My membership cards" })}
{membershipCards && @@ -41,7 +41,7 @@ export default async function MembershipCardSlot({ - {formatMessage({ id: "Add new card" })} + {intl.formatMessage({ id: "Add new card" })}
diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/@profile/page.tsx b/app/[lang]/(live)/(protected)/my-pages/profile/@profile/page.tsx index d858d5943..c12b61810 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/@profile/page.tsx +++ b/app/[lang]/(live)/(protected)/my-pages/profile/@profile/page.tsx @@ -24,7 +24,7 @@ import { LangParams, PageArgs } from "@/types/params" export default async function Profile({ params }: PageArgs) { setLang(params.lang) - const { formatMessage } = await getIntl() + const intl = await getIntl() const user = await getProfile() if (!user || "error" in user) { return null @@ -37,7 +37,7 @@ export default async function Profile({ params }: PageArgs) {
- {formatMessage({ id: "Welcome" })} + {intl.formatMessage({ id: "Welcome" })} {user.name} @@ -45,7 +45,7 @@ export default async function Profile({ params }: PageArgs<LangParams>) { </hgroup> <Button asChild intent="primary" size="small" theme="base"> <Link prefetch={false} color="none" href={profileEdit[params.lang]}> - {formatMessage({ id: "Edit profile" })} + {intl.formatMessage({ id: "Edit profile" })} </Link> </Button> </Header> @@ -54,35 +54,35 @@ export default async function Profile({ params }: PageArgs<LangParams>) { <div className={styles.item}> <CalendarIcon color="burgundy" /> <Body color="burgundy" textTransform="bold"> - {formatMessage({ id: "Date of Birth" })} + {intl.formatMessage({ id: "Date of Birth" })} </Body> <Body color="burgundy">{user.dateOfBirth}</Body> </div> <div className={styles.item}> <PhoneIcon color="burgundy" /> <Body color="burgundy" textTransform="bold"> - {formatMessage({ id: "Phone number" })} + {intl.formatMessage({ id: "Phone number" })} </Body> <Body color="burgundy">{user.phoneNumber}</Body> </div> <div className={styles.item}> <GlobeIcon color="burgundy" /> <Body color="burgundy" textTransform="bold"> - {formatMessage({ id: "Language" })} + {intl.formatMessage({ id: "Language" })} </Body> <Body color="burgundy">{language?.label ?? defaultLanguage}</Body> </div> <div className={styles.item}> <EmailIcon color="burgundy" /> <Body color="burgundy" textTransform="bold"> - {formatMessage({ id: "Email" })} + {intl.formatMessage({ id: "Email" })} </Body> <Body color="burgundy">{user.email}</Body> </div> <div className={styles.item}> <LocationIcon color="burgundy" /> <Body color="burgundy" textTransform="bold"> - {formatMessage({ id: "Address" })} + {intl.formatMessage({ id: "Address" })} </Body> <Body color="burgundy"> {user.address.streetAddress @@ -100,7 +100,7 @@ export default async function Profile({ params }: PageArgs<LangParams>) { <div className={styles.item}> <LockIcon color="burgundy" /> <Body color="burgundy" textTransform="bold"> - {formatMessage({ id: "Password" })} + {intl.formatMessage({ id: "Password" })} </Body> <Body color="burgundy">**********</Body> </div> diff --git a/app/[lang]/(live)/(protected)/my-pages/profile/profileLayout.css b/app/[lang]/(live)/(protected)/my-pages/profile/profileLayout.css index f3549d0d2..aefe1b30f 100644 --- a/app/[lang]/(live)/(protected)/my-pages/profile/profileLayout.css +++ b/app/[lang]/(live)/(protected)/my-pages/profile/profileLayout.css @@ -1,6 +1,7 @@ /** * Due to css import issues with parallel routes we are forced to * use a regular css file and import it in the page.tsx + * This is addressed in Next 15: https: //github.com/vercel/next.js/pull/66300 */ .profile-layout { background-color: var(--Main-Grey-White); diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/page.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/page.tsx new file mode 100644 index 000000000..6a29974b3 --- /dev/null +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/@preview/page.tsx @@ -0,0 +1,18 @@ +import { setPreviewData } from "@/lib/previewContext" + +import InitLivePreview from "@/components/LivePreview" + +import { PageArgs, UIDParams } from "@/types/params" + +export default function PreviewPage({ + searchParams, + params, +}: PageArgs<UIDParams, URLSearchParams>) { + const shouldInitializePreview = searchParams.isPreview === "true" + + if (searchParams.live_preview) { + setPreviewData({ hash: searchParams.live_preview, uid: params.uid }) + } + + return shouldInitializePreview ? <InitLivePreview /> : null +} diff --git a/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.tsx b/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.tsx index 76f26fe9f..8ffc3c82d 100644 --- a/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.tsx +++ b/app/[lang]/(live)/(public)/[contentType]/[uid]/layout.tsx @@ -9,15 +9,18 @@ import { export default function ContentTypeLayout({ breadcrumbs, + preview, children, }: React.PropsWithChildren< LayoutArgs<LangParams & ContentTypeParams & UIDParams> & { breadcrumbs: React.ReactNode + preview: React.ReactNode } >) { return ( <div className={styles.container}> <section className={styles.layout}> + {preview} {breadcrumbs} {children} </section> diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css index 06b39ef41..bb1cf59f0 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.module.css @@ -1,157 +1,7 @@ -.details, -.guest, -.header, -.hgroup, -.hotel, -.list, -.main, -.section, -.receipt, -.total { +.main { display: flex; flex-direction: column; -} - -.main { gap: var(--Spacing-x5); margin: 0 auto; - width: min(calc(100dvw - (var(--Spacing-x3) * 2)), 708px); -} - -.header, -.hgroup { - align-items: center; -} - -.header { - gap: var(--Spacing-x3); -} - -.hgroup { - gap: var(--Spacing-x-half); -} - -.body { - max-width: 560px; -} - -.section { - display: flex; - flex-direction: column; - gap: var(--Spacing-x9); -} - -.booking { - display: grid; - gap: var(--Spacing-x-one-and-half); - grid-template-areas: - "image" - "details" - "actions"; -} - -.actions, -.details { - background-color: var(--Base-Surface-Subtle-Normal); - border-radius: var(--Corner-radius-Medium); -} - -.details { - gap: var(--Spacing-x3); - grid-area: details; - padding: var(--Spacing-x2); -} - -.tempImage { - align-items: center; - background-color: lightgrey; - border-radius: var(--Corner-radius-Medium); - display: flex; - grid-area: image; - justify-content: center; -} - -.actions { - display: grid; - grid-area: actions; - padding: var(--Spacing-x1) var(--Spacing-x2); -} - -.list { - gap: var(--Spacing-x-one-and-half); - list-style: none; - margin: 0; - padding: 0; -} - -.listItem { - align-items: center; - display: flex; - gap: var(--Spacing-x1); - justify-content: space-between; -} - -.summary { - display: grid; - gap: var(--Spacing-x3); -} - -.guest, -.hotel { - gap: var(--Spacing-x-half); -} - -.receipt, -.total { - gap: var(--Spacing-x2); -} - -.divider { - grid-column: 1 / -1; -} - -@media screen and (max-width: 767px) { - .actions { - & > button[class*="btn"][class*="icon"][class*="small"] { - border-bottom: 1px solid var(--Base-Border-Subtle); - border-radius: 0; - justify-content: space-between; - - &:last-of-type { - border-bottom: none; - } - - & > svg { - order: 2; - } - } - } - - .tempImage { - min-height: 250px; - } -} - -@media screen and (min-width: 768px) { - .booking { - grid-template-areas: - "details image" - "actions actions"; - grid-template-columns: 1fr minmax(256px, min(256px, 100%)); - } - - .actions { - gap: var(--Spacing-x7); - grid-template-columns: repeat(auto-fit, minmax(50px, auto)); - justify-content: center; - padding: var(--Spacing-x1) var(--Spacing-x3); - } - - .details { - padding: var(--Spacing-x3) var(--Spacing-x3) var(--Spacing-x2); - } - - .summary { - grid-template-columns: 1fr 1fr; - } + width: min(calc(100dvw - (var(--Spacing-x3) * 2)), 948px); } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx index c4a682da9..b81003b9d 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -1,20 +1,7 @@ -import { dt } from "@/lib/dt" -import { serverClient } from "@/lib/trpc/server" +import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" -import { - CalendarIcon, - DownloadIcon, - ImageIcon, - PrinterIcon, -} from "@/components/Icons" -import Button from "@/components/TempDesignSystem/Button" -import Divider from "@/components/TempDesignSystem/Divider" -import Link from "@/components/TempDesignSystem/Link" -import Body from "@/components/TempDesignSystem/Text/Body" -import Caption from "@/components/TempDesignSystem/Text/Caption" -import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import Title from "@/components/TempDesignSystem/Text/Title" -import { getIntl } from "@/i18n" +import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation" +import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" @@ -24,269 +11,12 @@ export default async function BookingConfirmationPage({ params, searchParams, }: PageArgs<LangParams, { confirmationNumber: string }>) { + setLang(params.lang) const confirmationNumber = searchParams.confirmationNumber - const booking = await serverClient().booking.confirmation({ - confirmationNumber, - }) - - if (!booking) { - return null - } - - const intl = await getIntl() - const text = intl.formatMessage<React.ReactNode>( - { id: "booking.confirmation.text" }, - { - emailLink: (str) => ( - <Link color="burgundy" href="#" textDecoration="underline"> - {str} - </Link> - ), - } - ) - - const fromDate = dt(booking.checkInDate).locale(params.lang) - const toDate = dt(booking.checkOutDate).locale(params.lang) - const nights = intl.formatMessage( - { id: "booking.nights" }, - { - totalNights: dt(toDate.format("YYYY-MM-DD")).diff( - dt(fromDate.format("YYYY-MM-DD")), - "days" - ), - } - ) - + void getBookingConfirmation(confirmationNumber) return ( <main className={styles.main}> - <header className={styles.header}> - <hgroup className={styles.hgroup}> - <Title - as="h4" - color="red" - textAlign="center" - textTransform="regular" - type="h2" - > - {intl.formatMessage({ id: "booking.confirmation.title" })} - - - {booking.hotel?.data.attributes.name} - -
- - {text} - -
-
-
-
-
- - {intl.formatMessage( - { id: "Reference #{bookingNr}" }, - { bookingNr: booking.confirmationNumber } - )} - -
-
    -
  • - {intl.formatMessage({ id: "Check-in" })} - - {`${fromDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${fromDate.format("HH:mm")}`} - -
  • -
  • - {intl.formatMessage({ id: "Check-out" })} - - {`${toDate.format("ddd, D MMM")} ${intl.formatMessage({ id: "from" })} ${toDate.format("HH:mm")}`} - -
  • -
  • - {intl.formatMessage({ id: "Breakfast" })} - - {booking.temp.breakfastFrom} - {booking.temp.breakfastTo} - -
  • -
  • - {intl.formatMessage({ id: "Cancellation policy" })} - - {intl.formatMessage({ id: booking.temp.cancelPolicy })} - -
  • -
  • - {intl.formatMessage({ id: "Rebooking" })} - {`${intl.formatMessage({ id: "Free until" })} ${fromDate.subtract(3, "day").format("ddd, D MMM")}`} -
  • -
-
- -
- - - -
-
-
-
- - {intl.formatMessage({ id: "Guest" })} - -
- - {`${booking.guest.firstName} ${booking.guest.lastName}${booking.guest.memberbershipNumber ? ` (${intl.formatMessage({ id: "member no" })} ${booking.guest.memberbershipNumber})` : ""}`} - - {booking.guest.email} - - {booking.guest.phoneNumber} - -
-
-
- - {intl.formatMessage({ id: "Your hotel" })} - -
- - {booking.hotel?.data.attributes.name} - - - {booking.hotel?.data.attributes.contactInformation.email} - - - {booking.hotel?.data.attributes.contactInformation.phoneNumber} - -
-
- -
-
- - {`${booking.temp.room.type}, ${nights}`} - - {booking.temp.room.price} -
- {booking.temp.packages.map((pkg) => ( -
- - {pkg.name} - - {pkg.price} -
- ))} -
-
-
- - {intl.formatMessage({ id: "VAT" })} - - {booking.temp.room.vat} -
-
- - {intl.formatMessage({ id: "Total cost" })} - - - {" "} - {intl.formatMessage( - { id: "{amount} {currency}" }, - { - amount: intl.formatNumber(booking.totalPrice), - currency: booking.currencyCode, - } - )} - - - {`${intl.formatMessage({ id: "Approx." })} ${booking.temp.totalInEuro}`} - -
-
- -
- - {`${intl.formatMessage({ id: "Payment received" })} ${dt(booking.temp.payment).locale(params.lang).format("D MMM YYYY, h:mm z")}`} - - - {intl.formatMessage( - { id: "{card} ending with {cardno}" }, - { - card: "Mastercard", - cardno: "2202", - } - )} - -
-
-
+ ) } - -// const { email, hotel, stay, summary } = tempConfirmationData - -// const confirmationNumber = useMemo(() => { -// if (typeof window === "undefined") return "" - -// const storedConfirmationNumber = sessionStorage.getItem( -// BOOKING_CONFIRMATION_NUMBER -// ) -// TODO: cleanup stored values -// sessionStorage.removeItem(BOOKING_CONFIRMATION_NUMBER) -// return storedConfirmationNumber -// }, []) - -// const bookingStatus = useHandleBookingStatus( -// confirmationNumber, -// BookingStatusEnum.BookingCompleted, -// maxRetries, -// retryInterval -// ) - -// if ( -// confirmationNumber === null || -// bookingStatus.isError || -// (bookingStatus.isFetched && !bookingStatus.data) -// ) { -// // TODO: handle error -// throw new Error("Error fetching booking status") -// } - -// if ( -// bookingStatus.data?.reservationStatus === BookingStatusEnum.BookingCompleted -// ) { -// return 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 a40136c28..b39c2622b 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/@summary/page.tsx @@ -7,7 +7,6 @@ import Summary from "@/components/HotelReservation/EnterDetails/Summary" import { generateChildrenString, getQueryParamsForEnterDetails, - mapChildrenFromString, } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" @@ -17,9 +16,11 @@ export default async function SummaryPage({ searchParams, }: PageArgs>) { const selectRoomParams = new URLSearchParams(searchParams) - const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } = + const { hotel, rooms, fromDate, toDate } = getQueryParamsForEnterDetails(selectRoomParams) + const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms + const availability = await getSelectedRoomAvailability({ hotelId: hotel, adults, @@ -37,31 +38,32 @@ export default async function SummaryPage({ return null } - const prices = user - ? { - local: { - price: availability.memberRate?.localPrice.pricePerStay, - currency: availability.memberRate?.localPrice.currency, - }, - euro: { - price: availability.memberRate?.requestedPrice?.pricePerStay, - currency: availability.memberRate?.requestedPrice?.currency, - }, - } - : { - local: { - price: availability.publicRate?.localPrice.pricePerStay, - currency: availability.publicRate?.localPrice.currency, - }, - euro: { - price: availability.publicRate?.requestedPrice?.pricePerStay, - currency: availability.publicRate?.requestedPrice?.currency, - }, - } + const prices = + user && availability.memberRate + ? { + local: { + price: availability.memberRate?.localPrice.pricePerStay, + currency: availability.memberRate?.localPrice.currency, + }, + euro: { + price: availability.memberRate?.requestedPrice?.pricePerStay, + currency: availability.memberRate?.requestedPrice?.currency, + }, + } + : { + local: { + price: availability.publicRate?.localPrice.pricePerStay, + currency: availability.publicRate?.localPrice.currency, + }, + euro: { + price: availability.publicRate?.requestedPrice?.pricePerStay, + currency: availability.publicRate?.requestedPrice?.currency, + }, + } return ( -
+
{hotelHeader} -
+
{children} -
diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx index 5b445c4a3..37f1412ff 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/[step]/page.tsx @@ -1,3 +1,5 @@ +import "./enterDetailsLayout.css" + import { notFound } from "next/navigation" import { @@ -39,14 +41,13 @@ export default async function StepPage({ const selectRoomParams = new URLSearchParams(searchParams) const { hotel: hotelId, - adults, - children, - roomTypeCode, - rateCode, + rooms, fromDate, toDate, } = getQueryParamsForEnterDetails(selectRoomParams) + const { adults, children, roomTypeCode, rateCode } = rooms[0] // TODO: Handle multiple rooms + const childrenAsString = children && generateChildrenString(children) const breakfastInput = { adults, fromDate, hotelId, toDate } @@ -97,6 +98,11 @@ export default async function StepPage({ id: "Select payment method", }) + const roomPrice = + user && roomAvailability.memberRate + ? roomAvailability.memberRate?.localPrice.pricePerStay + : roomAvailability.publicRate!.localPrice.pricePerStay + return (
@@ -137,7 +143,7 @@ export default async function StepPage({ label={mustBeGuaranteed ? guaranteeWithCard : selectPaymentMethod} > ) 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 800ffe8f9..d8e3db57e 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 @@ -9,17 +9,22 @@ margin: 0 auto; } -.section { +.header { + display: flex; + margin: 0 auto; + padding: var(--Spacing-x4) var(--Spacing-x5) var(--Spacing-x3) + var(--Spacing-x5); + justify-content: space-between; + max-width: var(--max-width); +} + +.sideBar { display: flex; flex-direction: column; + max-width: 340px; } .link { - display: flex; - padding: var(--Spacing-x2) var(--Spacing-x0); -} - -.mapContainer { display: none; } @@ -34,11 +39,27 @@ } @media (min-width: 768px) { + .link { + display: flex; + padding-bottom: var(--Spacing-x6); + } .mapContainer { - display: block; + display: flex; + flex-direction: column; + background: var(--Base-Surface-Primary-light-Normal); + border-radius: var(--Corner-radius-Medium); + border: 1px solid var(--Base-Border-Subtle); + } + .mapLinkText { + display: flex; + align-items: center; + justify-content: center; + gap: var(--Spacing-x-half); + padding: var(--Spacing-x-one-and-half) var(--Spacing-x0); } .main { flex-direction: row; + gap: var(--Spacing-x5); } .buttonContainer { display: none; 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 65a1dae45..ddf1d9347 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/page.tsx @@ -11,6 +11,7 @@ import { } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" +import HotelSorter from "@/components/HotelReservation/SelectHotel/HotelSorter" import MobileMapButtonContainer from "@/components/HotelReservation/SelectHotel/MobileMapButtonContainer" import { generateChildrenString, @@ -96,34 +97,43 @@ export default async function SelectHotelPage({ } return ( -
-
-
- - - + <> +
+
{city.name}
+ +
+
+
- {intl.formatMessage({ id: "Show map" })} - +
+ +
+ {intl.formatMessage({ id: "Show map" })} + +
+
+ +
- - -
- - -
+ + +
+ ) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts index c3dc16c49..ee541bb7a 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils.ts @@ -3,16 +3,23 @@ import { serverClient } from "@/lib/trpc/server" import { getLang } from "@/i18n/serverContext" -import { BedTypeEnum } from "@/types/components/bookingWidget/enums" import type { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput" import type { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" -import type { Filter } from "@/types/components/hotelReservation/selectHotel/hotelFilters" -import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" -import { - type PointOfInterest, - PointOfInterestCategoryNameEnum, - PointOfInterestGroupEnum, -} from "@/types/hotel" +import type { + CategorizedFilters, + Filter, +} from "@/types/components/hotelReservation/selectHotel/hotelFilters" +import type { HotelPin } from "@/types/components/hotelReservation/selectHotel/map" +import { HotelListingEnum } from "@/types/enums/hotelListing" + +const hotelSurroundingsFilterNames = [ + "Hotel surroundings", + "Hotel omgivelser", + "Hotelumgebung", + "Hotellia lähellä", + "Hotellomgivelser", + "Omgivningar", +] export async function fetchAvailableHotels( input: AvailabilityInput @@ -22,7 +29,24 @@ export async function fetchAvailableHotels( if (!availableHotels) throw new Error() const language = getLang() - const hotels = availableHotels.availability.map(async (hotel) => { + const hotelMap = new Map() + + availableHotels.availability.forEach((hotel) => { + const existingHotel = hotelMap.get(hotel.hotelId) + if (existingHotel) { + if (hotel.ratePlanSet === HotelListingEnum.RatePlanSet.PUBLIC) { + existingHotel.bestPricePerNight.regularAmount = + hotel.bestPricePerNight?.regularAmount + } else if (hotel.ratePlanSet === HotelListingEnum.RatePlanSet.MEMBER) { + existingHotel.bestPricePerNight.memberAmount = + hotel.bestPricePerNight?.memberAmount + } + } else { + hotelMap.set(hotel.hotelId, { ...hotel }) + } + }) + + const hotels = Array.from(hotelMap.values()).map(async (hotel) => { const hotelData = await getHotelData({ hotelId: hotel.hotelId.toString(), language, @@ -39,7 +63,7 @@ export async function fetchAvailableHotels( return await Promise.all(hotels) } -export function getFiltersFromHotels(hotels: HotelData[]) { +export function getFiltersFromHotels(hotels: HotelData[]): CategorizedFilters { const filters = hotels.flatMap((hotel) => hotel.hotelData.detailedFacilities) const uniqueFilterIds = [...new Set(filters.map((filter) => filter.id))] @@ -47,51 +71,54 @@ export function getFiltersFromHotels(hotels: HotelData[]) { .map((filterId) => filters.find((filter) => filter.id === filterId)) .filter((filter): filter is Filter => filter !== undefined) - return filterList + return filterList.reduce( + (acc, filter) => { + if (filter.filter && hotelSurroundingsFilterNames.includes(filter.filter)) + return { + facilityFilters: acc.facilityFilters, + surroundingsFilters: [...acc.surroundingsFilters, filter], + } + + return { + facilityFilters: [...acc.facilityFilters, filter], + surroundingsFilters: acc.surroundingsFilters, + } + }, + { facilityFilters: [], surroundingsFilters: [] } + ) } -const bedTypeMap: Record = { - [BedTypeEnum.IN_ADULTS_BED]: "ParentsBed", - [BedTypeEnum.IN_CRIB]: "Crib", - [BedTypeEnum.IN_EXTRA_BED]: "ExtraBed", -} - -export function generateChildrenString(children: Child[]): string { - return `[${children - ?.map((child) => { - const age = child.age - const bedType = bedTypeMap[+child.bed] - return `${age}:${bedType}` - }) - .join(",")}]` -} - -export function getPointOfInterests(hotels: HotelData[]): PointOfInterest[] { - // TODO: this is just a quick transformation to get something there. May need rework +export function getHotelPins(hotels: HotelData[]): HotelPin[] { return hotels.map((hotel) => ({ coordinates: { lat: hotel.hotelData.location.latitude, lng: hotel.hotelData.location.longitude, }, name: hotel.hotelData.name, - distance: hotel.hotelData.location.distanceToCentre, - categoryName: PointOfInterestCategoryNameEnum.HOTEL, - group: PointOfInterestGroupEnum.LOCATION, + publicPrice: hotel.price?.regularAmount ?? null, + memberPrice: hotel.price?.memberAmount ?? null, + currency: hotel.price?.currency || null, + images: [ + hotel.hotelData.hotelContent.images, + ...(hotel.hotelData.gallery?.heroImages ?? []), + ], + amenities: hotel.hotelData.detailedFacilities.slice(0, 3), + ratings: hotel.hotelData.ratings?.tripAdvisor.rating ?? null, })) } -export function getCentralCoordinates(pointOfInterests: PointOfInterest[]) { - const centralCoordinates = pointOfInterests.reduce( - (acc, poi) => { - acc.lat += poi.coordinates.lat - acc.lng += poi.coordinates.lng +export function getCentralCoordinates(hotels: HotelPin[]) { + const centralCoordinates = hotels.reduce( + (acc, pin) => { + acc.lat += pin.coordinates.lat + acc.lng += pin.coordinates.lng return acc }, { lat: 0, lng: 0 } ) - centralCoordinates.lat /= pointOfInterests.length - centralCoordinates.lng /= pointOfInterests.length + centralCoordinates.lat /= hotels.length + centralCoordinates.lng /= hotels.length return centralCoordinates } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx index 816c937ae..99554d1c1 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-rate/page.tsx @@ -49,11 +49,11 @@ export default async function SelectRatePage({ searchParams.fromDate && dt(searchParams.fromDate).isAfter(dt().subtract(1, "day")) ? searchParams.fromDate - : dt().utc().format("YYYY-MM-DD") + : dt().utc().format("YYYY-MM-D") const validToDate = searchParams.toDate && dt(searchParams.toDate).isAfter(validFromDate) ? searchParams.toDate - : dt().utc().add(1, "day").format("YYYY-MM-DD") + : dt().utc().add(1, "day").format("YYYY-MM-D") const adults = selectRoomParamsObject.room[0].adults || 1 // TODO: Handle multiple rooms const childrenCount = selectRoomParamsObject.room[0].child?.length const children = selectRoomParamsObject.room[0].child diff --git a/app/[lang]/(preview)/error.tsx b/app/[lang]/(preview)/error.tsx deleted file mode 100644 index 4b2d7bb55..000000000 --- a/app/[lang]/(preview)/error.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client" - -export default function Error({ error }: { error: Error }) { - return ( -
-

Something went wrong!

-
- ) -} diff --git a/app/[lang]/(preview)/layout.tsx b/app/[lang]/(preview)/layout.tsx deleted file mode 100644 index 948bcceba..000000000 --- a/app/[lang]/(preview)/layout.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import "@/app/globals.css" -import "@scandic-hotels/design-system/style.css" - -import TrpcProvider from "@/lib/trpc/Provider" - -import InitLivePreview from "@/components/LivePreview" -import { getIntl } from "@/i18n" -import ServerIntlProvider from "@/i18n/Provider" -import { setLang } from "@/i18n/serverContext" - -import type { LangParams, LayoutArgs } from "@/types/params" - -export default async function RootLayout({ - children, - params, -}: React.PropsWithChildren>) { - setLang(params.lang) - const { defaultLocale, locale, messages } = await getIntl() - - return ( - - - - - {children} - - - - ) -} diff --git a/app/[lang]/(preview)/not-found.tsx b/app/[lang]/(preview)/not-found.tsx deleted file mode 100644 index 7fbf34706..000000000 --- a/app/[lang]/(preview)/not-found.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export default function NotFound() { - return ( -
-

Not found

-

Could not find requested resource

-
- ) -} diff --git a/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx b/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx deleted file mode 100644 index ff954a31d..000000000 --- a/app/[lang]/(preview)/preview/[contentType]/[uid]/page.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { ContentstackLivePreview } from "@contentstack/live-preview-utils" -import { notFound } from "next/navigation" - -import HotelPage from "@/components/ContentType/HotelPage" -import LoyaltyPage from "@/components/ContentType/LoyaltyPage" -import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage" -import ContentPage from "@/components/ContentType/StaticPages/ContentPage" -import LoadingSpinner from "@/components/LoadingSpinner" -import { setLang } from "@/i18n/serverContext" - -import type { - ContentTypeParams, - LangParams, - PageArgs, - UIDParams, -} from "@/types/params" - -export default async function PreviewPage({ - params, - searchParams, -}: PageArgs) { - setLang(params.lang) - - try { - ContentstackLivePreview.setConfigFromParams(searchParams) - - if (!searchParams.live_preview) { - return - } - - switch (params.contentType) { - case "content-page": - return - case "loyalty-page": - return - case "collection-page": - return - case "hotel-page": - return - default: - console.log({ PREVIEW: params }) - const type = params.contentType - console.error(`Unsupported content type given: ${type}`) - notFound() - } - } catch (error) { - // TODO: throw 500 - console.error("Error in preview page") - console.error(error) - throw new Error("Something went wrong") - } -} diff --git a/app/[lang]/(preview-current)/error.tsx b/app/[lang]/(preview-current)/error.tsx deleted file mode 100644 index 4b2d7bb55..000000000 --- a/app/[lang]/(preview-current)/error.tsx +++ /dev/null @@ -1,9 +0,0 @@ -"use client" - -export default function Error({ error }: { error: Error }) { - return ( -
-

Something went wrong!

-
- ) -} diff --git a/app/[lang]/(preview-current)/layout.tsx b/app/[lang]/(preview-current)/layout.tsx deleted file mode 100644 index e5ff698bb..000000000 --- a/app/[lang]/(preview-current)/layout.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import Footer from "@/components/Current/Footer" -import LangPopup from "@/components/Current/LangPopup" -import InitLivePreview from "@/components/LivePreview" -import SkipToMainContent from "@/components/SkipToMainContent" -import { setLang } from "@/i18n/serverContext" - -import type { Metadata } from "next" - -import type { LangParams, LayoutArgs } from "@/types/params" - -export const fetchCache = "default-no-store" - -export const metadata: Metadata = { - description: "New web", - title: "Scandic Hotels", -} - -export default function RootLayout({ - children, - params, -}: React.PropsWithChildren>) { - setLang(params.lang) - - return ( - - - {/* eslint-disable-next-line @next/next/no-css-tags */} - - {/* eslint-disable-next-line @next/next/no-css-tags */} - - - - - - - {children} -