From ccb15593ead5b0387bcfc9cfb2330aec432cf4ed Mon Sep 17 00:00:00 2001 From: Simon Emanuelsson Date: Tue, 12 Nov 2024 15:30:59 +0100 Subject: [PATCH] feat: merge stores, fix auto navigation, split summary --- .../booking-confirmation/page.tsx | 15 +- .../@sidePeek/{ => [...paths]}/loading.tsx | 0 .../(standard)/@sidePeek/[...paths]/page.tsx | 22 +- .../(standard)/@sidePeek/page.tsx | 26 +- .../(standard)/step/@hotelHeader/loading.tsx | 5 - .../(standard)/step/@summary/loading.tsx | 5 - .../(standard)/step/@summary/page.module.css | 68 --- .../(standard)/step/@summary/page.tsx | 136 ------ .../(standard)/step/_preload.ts | 9 - .../(standard)/step/enterDetailsLayout.css | 45 -- .../(standard)/step/layout.tsx | 39 -- .../(standard)/step/page.module.css | 35 ++ .../hotelreservation/(standard)/step/page.tsx | 231 +++++---- components/Footer/Details/details.module.css | 3 + .../Navigation/MainNav/mainnav.module.css | 2 + .../Actions/actions.module.css | 34 -- .../Header/Actions/actions.module.css | 15 + .../{ => Header}/Actions/index.tsx | 21 +- .../Header/header.module.css | 6 +- .../BookingConfirmation/Header/index.tsx | 27 +- .../BookingConfirmation/Rooms/Room/index.tsx | 5 + .../Rooms/Room/room.module.css | 0 .../BookingConfirmation/Rooms/index.tsx | 5 + .../Rooms/rooms.module.css | 6 + .../BookingConfirmation/Summary/index.tsx | 153 +----- .../Summary/summary.module.css | 31 +- .../BookingConfirmation/_Summary/index.tsx | 154 ++++++ .../_Summary/summary.module.css | 31 ++ .../bookingConfirmation.module.css | 23 - .../BookingConfirmation/index.tsx | 31 -- .../EnterDetails/BedType/index.tsx | 18 +- .../EnterDetails/Breakfast/index.tsx | 34 +- .../Details/JoinScandicFriendsCard/index.tsx | 3 +- .../EnterDetails/Details/index.tsx | 23 +- .../EnterDetails/Details/schema.ts | 15 +- .../HistoryStateManager/index.tsx | 6 +- .../HotelHeader/header.module.css | 0 .../EnterDetails/HotelHeader/index.tsx | 26 +- .../Payment/PaymentCallback/index.tsx | 8 +- .../EnterDetails/Payment/index.tsx | 29 +- .../EnterDetails/SectionAccordion/index.tsx | 46 +- .../SelectedRoom/ToggleSidePeek.tsx | 2 +- .../EnterDetails/SelectedRoom/index.tsx | 2 +- .../EnterDetails/StorageCleaner.tsx | 2 +- .../EnterDetails/Summary/Client.tsx | 95 ++++ .../EnterDetails/Summary/index.tsx | 360 ++------------ .../EnterDetails/Summary/summary.module.css | 127 +++-- .../BottomSheet/bottomSheet.module.css | 0 .../Summary/BottomSheet/index.tsx | 9 +- components/HotelReservation/Summary/index.tsx | 234 +++++++++ .../Summary/summary.module.css | 83 ++++ components/Icons/CalendarAdd.tsx | 31 ++ components/Icons/index.tsx | 1 + .../Form/ChoiceCard/_Card/index.tsx | 2 - contexts/Details.ts | 2 +- contexts/Steps.ts | 5 - i18n/dictionaries/en.json | 4 +- lib/trpc/memoizedRequests/index.ts | 26 +- package-lock.json | 11 +- providers/DetailsProvider.tsx | 30 -- providers/EnterDetailsProvider.tsx | 50 ++ providers/StepsProvider.tsx | 58 --- server/routers/booking/output.ts | 47 +- stores/details.ts | 189 -------- stores/enter-details/helpers.ts | 354 ++++++++++++++ stores/enter-details/index.ts | 448 ++++++++++++++++++ stores/steps.ts | 156 ------ .../enterDetails/bookingData.ts | 32 +- .../hotelReservation/enterDetails/details.ts | 11 +- .../enterDetails/hotelHeader.ts | 7 + .../hotelReservation/enterDetails/step.ts | 8 - .../hotelReservation/enterDetails/summary.ts | 16 +- types/components/hotelReservation/summary.ts | 39 ++ .../contexts/{details.ts => enter-details.ts} | 2 +- types/contexts/steps.ts | 3 - types/providers/details.ts | 3 - types/providers/enter-details.ts | 18 + types/providers/steps.ts | 11 - types/stores/details.ts | 39 -- types/stores/enter-details.ts | 68 +++ types/stores/steps.ts | 10 - types/trpc/routers/hotel/availability.ts | 5 + 82 files changed, 2149 insertions(+), 1842 deletions(-) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/{ => [...paths]}/loading.tsx (100%) delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/loading.tsx delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/loading.tsx delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.module.css delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/step/_preload.ts delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsLayout.css delete mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx create mode 100644 app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.module.css delete mode 100644 components/HotelReservation/BookingConfirmation/Actions/actions.module.css create mode 100644 components/HotelReservation/BookingConfirmation/Header/Actions/actions.module.css rename components/HotelReservation/BookingConfirmation/{ => Header}/Actions/index.tsx (53%) create mode 100644 components/HotelReservation/BookingConfirmation/Rooms/Room/index.tsx create mode 100644 components/HotelReservation/BookingConfirmation/Rooms/Room/room.module.css create mode 100644 components/HotelReservation/BookingConfirmation/Rooms/index.tsx create mode 100644 components/HotelReservation/BookingConfirmation/Rooms/rooms.module.css create mode 100644 components/HotelReservation/BookingConfirmation/_Summary/index.tsx create mode 100644 components/HotelReservation/BookingConfirmation/_Summary/summary.module.css delete mode 100644 components/HotelReservation/BookingConfirmation/bookingConfirmation.module.css delete mode 100644 components/HotelReservation/BookingConfirmation/index.tsx rename app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.module.css => components/HotelReservation/EnterDetails/HotelHeader/header.module.css (100%) rename app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/page.tsx => components/HotelReservation/EnterDetails/HotelHeader/index.tsx (72%) create mode 100644 components/HotelReservation/EnterDetails/Summary/Client.tsx rename components/HotelReservation/{EnterDetails => }/Summary/BottomSheet/bottomSheet.module.css (100%) rename components/HotelReservation/{EnterDetails => }/Summary/BottomSheet/index.tsx (87%) create mode 100644 components/HotelReservation/Summary/index.tsx create mode 100644 components/HotelReservation/Summary/summary.module.css create mode 100644 components/Icons/CalendarAdd.tsx delete mode 100644 contexts/Steps.ts delete mode 100644 providers/DetailsProvider.tsx create mode 100644 providers/EnterDetailsProvider.tsx delete mode 100644 providers/StepsProvider.tsx delete mode 100644 stores/details.ts create mode 100644 stores/enter-details/helpers.ts create mode 100644 stores/enter-details/index.ts delete mode 100644 stores/steps.ts create mode 100644 types/components/hotelReservation/enterDetails/hotelHeader.ts delete mode 100644 types/components/hotelReservation/enterDetails/step.ts create mode 100644 types/components/hotelReservation/summary.ts rename types/contexts/{details.ts => enter-details.ts} (52%) delete mode 100644 types/contexts/steps.ts delete mode 100644 types/providers/details.ts create mode 100644 types/providers/enter-details.ts delete mode 100644 types/providers/steps.ts delete mode 100644 types/stores/details.ts create mode 100644 types/stores/enter-details.ts delete mode 100644 types/stores/steps.ts create mode 100644 types/trpc/routers/hotel/availability.ts 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 b81003b9d..23a66f340 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(confirmation)/booking-confirmation/page.tsx @@ -1,6 +1,8 @@ import { getBookingConfirmation } from "@/lib/trpc/memoizedRequests" -import BookingConfirmation from "@/components/HotelReservation/BookingConfirmation" +import Header from "@/components/HotelReservation/BookingConfirmation/Header" +import Rooms from "@/components/HotelReservation/BookingConfirmation/Rooms" +import Summary from "@/components/HotelReservation/BookingConfirmation/Summary" import { setLang } from "@/i18n/serverContext" import styles from "./page.module.css" @@ -12,11 +14,12 @@ export default async function BookingConfirmationPage({ searchParams, }: PageArgs) { setLang(params.lang) - const confirmationNumber = searchParams.confirmationNumber - void getBookingConfirmation(confirmationNumber) + void getBookingConfirmation(searchParams.confirmationNumber) return ( -
- -
+
+
+ + +
) } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/loading.tsx similarity index 100% rename from app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/loading.tsx rename to app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/loading.tsx diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx index 03a82e5f5..fee05b878 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/[...paths]/page.tsx @@ -1 +1,21 @@ -export { default } from "../page" +import { getHotelData } from "@/lib/trpc/memoizedRequests" + +import SidePeek from "@/components/HotelReservation/SidePeek" + +import type { LangParams, PageArgs } from "@/types/params" + +export default async function HotelSidePeek({ + params, + searchParams, +}: PageArgs) { + if (!searchParams.hotel) { + return + } + + const hotel = await getHotelData({ + hotelId: searchParams.hotel, + language: params.lang, + }) + + return +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/page.tsx index a73eb305e..438f146ae 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/@sidePeek/page.tsx @@ -1,25 +1,3 @@ -import { getHotelData } from "@/lib/trpc/memoizedRequests" - -import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" -import SidePeek from "@/components/HotelReservation/SidePeek" - -import type { LangParams, PageArgs } from "@/types/params" - -export default async function HotelSidePeek({ - params, - searchParams, -}: PageArgs) { - const search = new URLSearchParams(searchParams) - const { hotel: hotelId } = getQueryParamsForEnterDetails(search) - - if (!hotelId) { - return - } - - const hotel = await getHotelData({ - hotelId: hotelId, - language: params.lang, - }) - - return +export default function HotelSidePeekSlot() { + return null } diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/loading.tsx deleted file mode 100644 index 0fad268cc..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@hotelHeader/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import LoadingSpinner from "@/components/LoadingSpinner" - -export default function LoadingHotelHeader() { - return -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/loading.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/loading.tsx deleted file mode 100644 index 78b79a040..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/loading.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import LoadingSpinner from "@/components/LoadingSpinner" - -export default function LoadingSummaryHeader() { - return -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.module.css deleted file mode 100644 index f680a23a1..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.module.css +++ /dev/null @@ -1,68 +0,0 @@ -.mobileSummary { - display: block; -} - -.desktopSummary { - display: none; -} - -.summary { - background-color: var(--Main-Grey-White); - - border-color: var(--Primary-Light-On-Surface-Divider-subtle); - border-style: solid; - border-width: 1px; - border-bottom: none; - z-index: 10; -} - -.hider { - display: none; -} - -.shadow { - display: none; -} - -@media screen and (min-width: 1367px) { - .mobileSummary { - display: none; - } - - .desktopSummary { - display: grid; - grid-template-rows: auto auto 1fr; - margin-top: calc(0px - var(--Spacing-x9)); - } - - .summary { - position: sticky; - top: calc( - var(--booking-widget-desktop-height) + var(--Spacing-x2) + - var(--Spacing-x-half) - ); - z-index: 10; - border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; - margin-top: calc(0px - var(--Spacing-x9)); - } - - .shadow { - display: block; - background-color: var(--Main-Grey-White); - border-color: var(--Primary-Light-On-Surface-Divider-subtle); - border-style: solid; - border-left-width: 1px; - border-right-width: 1px; - border-top: none; - border-bottom: none; - } - - .hider { - display: block; - background-color: var(--Scandic-Brand-Warm-White); - position: sticky; - top: calc(var(--booking-widget-desktop-height) - 6px); - margin-top: var(--Spacing-x4); - height: 40px; - } -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx deleted file mode 100644 index 0fb655de6..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/@summary/page.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import { redirect } from "next/navigation" - -import { selectRate } from "@/constants/routes/hotelReservation" -import { - getPackages, - getProfileSafely, - getSelectedRoomAvailability, -} from "@/lib/trpc/memoizedRequests" - -import Summary from "@/components/HotelReservation/EnterDetails/Summary" -import { SummaryBottomSheet } from "@/components/HotelReservation/EnterDetails/Summary/BottomSheet" -import { - generateChildrenString, - getQueryParamsForEnterDetails, -} from "@/components/HotelReservation/SelectRate/RoomSelection/utils" - -import styles from "./page.module.css" - -import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" -import { LangParams, PageArgs, SearchParams } from "@/types/params" - -export default async function SummaryPage({ - params, - searchParams, -}: PageArgs>) { - const selectRoomParams = new URLSearchParams(searchParams) - const { hotel, rooms, fromDate, toDate } = - getQueryParamsForEnterDetails(selectRoomParams) - - const { - adults, - children, - roomTypeCode, - rateCode, - packages: packageCodes, - } = rooms[0] // TODO: Handle multiple rooms - - const availability = await getSelectedRoomAvailability({ - hotelId: hotel, - adults, - children: children ? generateChildrenString(children) : undefined, - roomStayStartDate: fromDate, - roomStayEndDate: toDate, - rateCode, - roomTypeCode, - packageCodes, - }) - const user = await getProfileSafely() - - const packages = packageCodes - ? await getPackages({ - hotelId: hotel, - startDate: fromDate, - endDate: toDate, - adults, - children: children?.length, - packageCodes, - }) - : null - - if (!availability || !availability.selectedRoom) { - console.error("No hotel or availability data", availability) - // TODO: handle this case - redirect(selectRate(params.lang)) - } - - 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: { - amount: availability.memberRate.localPrice.pricePerStay, - currency: availability.memberRate.localPrice.currency, - }, - euro: availability.memberRate.requestedPrice - ? { - amount: availability.memberRate.requestedPrice.pricePerStay, - currency: availability.memberRate.requestedPrice.currency, - } - : undefined, - } - : undefined, - } - - return ( - <> -
- -
- -
-
-
-
-
-
- -
-
-
- - ) -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/_preload.ts b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/_preload.ts deleted file mode 100644 index 6013a49cc..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/_preload.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { - getCreditCardsSafely, - getProfileSafely, -} from "@/lib/trpc/memoizedRequests" - -export function preload() { - void getProfileSafely() - void getCreditCardsSafely() -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsLayout.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsLayout.css deleted file mode 100644 index 0322e44a7..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/enterDetailsLayout.css +++ /dev/null @@ -1,45 +0,0 @@ -/** - * 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 - */ - -.enter-details-layout { - background-color: var(--Scandic-Brand-Warm-White); -} - -.enter-details-layout__container { - display: grid; - gap: var(--Spacing-x3) var(--Spacing-x9); - /* simulates padding on viewport smaller than --max-width-navigation */ -} - -.enter-details-layout__content { - margin: var(--Spacing-x3) var(--Spacing-x2) 0; -} - -.enter-details-layout__summaryContainer { - position: sticky; - bottom: 0; - left: 0; - right: 0; -} - -@media screen and (min-width: 1367px) { - .enter-details-layout__container { - grid-template-columns: 1fr 340px; - grid-template-rows: auto 1fr; - margin: var(--Spacing-x5) auto 0; - width: min( - calc(100dvw - (var(--Spacing-x2) * 2)), - var(--max-width-navigation) - ); - } - - .enter-details-layout__summaryContainer { - position: static; - display: grid; - grid-column: 2/3; - grid-row: 1/-1; - } -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx deleted file mode 100644 index 2bd8a5102..000000000 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/layout.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { getProfileSafely } from "@/lib/trpc/memoizedRequests" - -import { setLang } from "@/i18n/serverContext" -import DetailsProvider from "@/providers/DetailsProvider" - -import { preload } from "./_preload" - -import type { LangParams, LayoutArgs } from "@/types/params" - -export default async function StepLayout({ - children, - hotelHeader, - params, - summary, -}: React.PropsWithChildren< - LayoutArgs & { - hotelHeader: React.ReactNode - summary: React.ReactNode - } ->) { - setLang(params.lang) - preload() - - const user = await getProfileSafely() - - return ( - -
- {hotelHeader} -
-
{children}
- -
-
-
- ) -} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.module.css new file mode 100644 index 000000000..5c757de70 --- /dev/null +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.module.css @@ -0,0 +1,35 @@ +.container { + display: grid; + gap: var(--Spacing-x3) var(--Spacing-x9); +} + +.content { + margin: var(--Spacing-x3) var(--Spacing-x2) 0; +} + +.summary { + position: sticky; + bottom: 0; + left: 0; + right: 0; +} + +@media screen and (min-width: 1367px) { + .container { + grid-template-columns: 1fr 340px; + grid-template-rows: auto 1fr; + margin: var(--Spacing-x5) auto 0; + /* simulates padding on viewport smaller than --max-width-navigation */ + width: min( + calc(100dvw - (var(--Spacing-x2) * 2)), + var(--max-width-navigation) + ); + } + + .summary { + position: static; + display: grid; + grid-column: 2/3; + grid-row: 1/-1; + } +} diff --git a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx index ade5f7c8f..67f221c78 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/(standard)/step/page.tsx @@ -1,5 +1,3 @@ -import "./enterDetailsLayout.css" - import { notFound } from "next/navigation" import { Suspense } from "react" @@ -7,6 +5,7 @@ import { getBreakfastPackages, getCreditCardsSafely, getHotelData, + getPackages, getProfileSafely, getSelectedRoomAvailability, } from "@/lib/trpc/memoizedRequests" @@ -15,15 +14,20 @@ import BedType from "@/components/HotelReservation/EnterDetails/BedType" import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast" import Details from "@/components/HotelReservation/EnterDetails/Details" import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager" +import HotelHeader from "@/components/HotelReservation/EnterDetails/HotelHeader" import Payment from "@/components/HotelReservation/EnterDetails/Payment" import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" +import Summary from "@/components/HotelReservation/EnterDetails/Summary" import { generateChildrenString, getQueryParamsForEnterDetails, } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getIntl } from "@/i18n" -import StepsProvider from "@/providers/StepsProvider" +import { setLang } from "@/i18n/serverContext" +import EnterDetailsProvider from "@/providers/EnterDetailsProvider" + +import styles from "./page.module.css" import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import { StepEnum } from "@/types/enums/step" @@ -37,60 +41,78 @@ export default async function StepPage({ params: { lang }, searchParams, }: PageArgs) { + if (!isValidStep(searchParams.step)) { + return notFound() + } + setLang(lang) + const intl = await getIntl() const selectRoomParams = new URLSearchParams(searchParams) + // Deleting step to avoid double searchparams after rewrite selectRoomParams.delete("step") - const searchParamsString = selectRoomParams.toString() + const booking = getQueryParamsForEnterDetails(selectRoomParams) + const { hotel: hotelId, - rooms, + rooms: [ + { adults, children, roomTypeCode, rateCode, packages: packageCodes }, + ], // TODO: Handle multiple rooms fromDate, toDate, - } = getQueryParamsForEnterDetails(selectRoomParams) - - const { - adults, - children, - roomTypeCode, - rateCode, - packages: packageCodes, - } = rooms[0] // TODO: Handle multiple rooms + } = booking const childrenAsString = children && generateChildrenString(children) - const breakfastInput = { adults, fromDate, hotelId, toDate } - void getBreakfastPackages(breakfastInput) - void getSelectedRoomAvailability({ - hotelId, + const selectedRoomAvailabilityInput = { adults, children: childrenAsString, + hotelId, + packageCodes, + rateCode, roomStayStartDate: fromDate, roomStayEndDate: toDate, - rateCode, roomTypeCode, - packageCodes, - }) + } - const roomAvailability = await getSelectedRoomAvailability({ - hotelId, - adults, - children: childrenAsString, - roomStayStartDate: fromDate, - roomStayEndDate: toDate, - rateCode, - roomTypeCode, - packageCodes, - }) + void getProfileSafely() + void getCreditCardsSafely() + void getBreakfastPackages(breakfastInput) + void getSelectedRoomAvailability(selectedRoomAvailabilityInput) + if (packageCodes?.length) { + void getPackages({ + adults, + children: children?.length, + endDate: toDate, + hotelId, + packageCodes, + startDate: fromDate, + }) + } + + const packages = packageCodes + ? await getPackages({ + adults, + children: children?.length, + endDate: toDate, + hotelId, + packageCodes, + startDate: fromDate, + }) + : null + + const roomAvailability = await getSelectedRoomAvailability( + selectedRoomAvailabilityInput + ) const hotelData = await getHotelData({ hotelId, - language: lang, isCardOnlyPayment: roomAvailability?.mustBeGuaranteed, + language: lang, }) const breakfastPackages = await getBreakfastPackages(breakfastInput) const user = await getProfileSafely() const savedCreditCards = await getCreditCardsSafely() - if (!isValidStep(searchParams.step) || !hotelData || !roomAvailability) { + if (!hotelData || !roomAvailability) { return notFound() } @@ -116,75 +138,102 @@ export default async function StepPage({ const memberPrice = roomAvailability.memberRate ? { - price: roomAvailability.memberRate.localPrice.pricePerStay, - currency: roomAvailability.memberRate.localPrice.currency, - } + price: roomAvailability.memberRate.localPrice.pricePerStay, + currency: roomAvailability.memberRate.localPrice.currency, + } : undefined return ( - -
- - +
+ +
+
+
+ + - {/* TODO: How to handle no beds found? */} - {roomAvailability.bedTypes ? ( - - - - ) : null} + {/* TODO: How to handle no beds found? */} + {roomAvailability.bedTypes ? ( + + + + ) : null} - {breakfastPackages?.length ? ( - - - - ) : null} + {breakfastPackages?.length ? ( + + + + ) : null} - -
- + +
+ - - - + + + + +
+
+
-
+ +
+ + ) } diff --git a/components/Footer/Details/details.module.css b/components/Footer/Details/details.module.css index d62f08949..2d624e064 100644 --- a/components/Footer/Details/details.module.css +++ b/components/Footer/Details/details.module.css @@ -40,6 +40,7 @@ content: "·"; margin-left: var(--Spacing-x1); } + &:last-child { &::after { content: ""; @@ -56,12 +57,14 @@ .details { padding: var(--Spacing-x6) var(--Spacing-x5) var(--Spacing-x4); } + .bottomContainer { border-top: 1px solid var(--Base-Text-Medium-contrast); padding-top: var(--Spacing-x2); flex-direction: row; align-items: center; } + .navigationContainer { border-bottom: 0; padding-bottom: 0; diff --git a/components/Footer/Navigation/MainNav/mainnav.module.css b/components/Footer/Navigation/MainNav/mainnav.module.css index 24cf373e2..26bb9e8c8 100644 --- a/components/Footer/Navigation/MainNav/mainnav.module.css +++ b/components/Footer/Navigation/MainNav/mainnav.module.css @@ -9,9 +9,11 @@ .mainNavigationItem { padding: var(--Spacing-x3) 0; border-bottom: 1px solid var(--Base-Border-Normal); + &:first-child { padding-top: 0; } + &:last-child { border-bottom: 0; } diff --git a/components/HotelReservation/BookingConfirmation/Actions/actions.module.css b/components/HotelReservation/BookingConfirmation/Actions/actions.module.css deleted file mode 100644 index 1e6ba4fb6..000000000 --- a/components/HotelReservation/BookingConfirmation/Actions/actions.module.css +++ /dev/null @@ -1,34 +0,0 @@ -.actions { - background-color: var(--Base-Surface-Subtle-Normal); - border-radius: var(--Corner-radius-Medium); - display: grid; - grid-area: actions; - padding: var(--Spacing-x1) var(--Spacing-x2); -} - -@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; - } - } - } -} - -@media screen and (min-width: 768px) { - .actions { - gap: var(--Spacing-x1); - grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr; - justify-content: center; - padding: var(--Spacing-x1) var(--Spacing-x3); - } -} diff --git a/components/HotelReservation/BookingConfirmation/Header/Actions/actions.module.css b/components/HotelReservation/BookingConfirmation/Header/Actions/actions.module.css new file mode 100644 index 000000000..5e9865b30 --- /dev/null +++ b/components/HotelReservation/BookingConfirmation/Header/Actions/actions.module.css @@ -0,0 +1,15 @@ +.actions { + border-radius: var(--Corner-radius-Medium); + display: grid; + grid-area: actions; +} + +@media screen and (min-width: 768px) { + .actions { + gap: var(--Spacing-x3); + grid-auto-columns: auto; + grid-auto-flow: column; + grid-template-columns: auto; + justify-content: flex-start; + } +} diff --git a/components/HotelReservation/BookingConfirmation/Actions/index.tsx b/components/HotelReservation/BookingConfirmation/Header/Actions/index.tsx similarity index 53% rename from components/HotelReservation/BookingConfirmation/Actions/index.tsx rename to components/HotelReservation/BookingConfirmation/Header/Actions/index.tsx index 7ad9bc2a0..e37296182 100644 --- a/components/HotelReservation/BookingConfirmation/Actions/index.tsx +++ b/components/HotelReservation/BookingConfirmation/Header/Actions/index.tsx @@ -1,11 +1,5 @@ -import { - CalendarIcon, - ContractIcon, - DownloadIcon, - PrinterIcon, -} from "@/components/Icons" +import { CalendarAddIcon, DownloadIcon, EditIcon } from "@/components/Icons" import Button from "@/components/TempDesignSystem/Button" -import Divider from "@/components/TempDesignSystem/Divider" import { getIntl } from "@/i18n" import styles from "./actions.module.css" @@ -15,20 +9,13 @@ export default async function Actions() { return (
- - - - - - -
-
-
- {room.roomType} - - {intl.formatMessage( - { id: "{amount} {currency}" }, - { - amount: intl.formatNumber(price.local.amount), - currency: price.local.currency, - } - )} - -
- - {intl.formatMessage( - { id: "booking.adults" }, - { totalAdults: room.adults } - )} - - {room.children?.length ? ( - - {intl.formatMessage( - { id: "booking.children" }, - { totalChildren: room.children.length } - )} - - ) : null} - - {room.cancellationText} - - - {intl.formatMessage({ id: "Rate details" })} - - } - > - - -
- {room.packages - ? room.packages.map((roomPackage) => ( -
-
- - {roomPackage.description} - -
- - - {intl.formatMessage( - { id: "{amount} {currency}" }, - { - amount: roomPackage.localPrice.price, - currency: roomPackage.localPrice.currency, - } - )} - -
- )) - : null} - {chosenBed ? ( -
-
- {chosenBed.description} - - {intl.formatMessage({ id: "Based on availability" })} - -
- - - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "0", currency: price.local.currency } - )} - -
- ) : null} - - {chosenBreakfast === false ? ( -
- - {intl.formatMessage({ id: "No breakfast" })} - - - {intl.formatMessage( - { id: "{amount} {currency}" }, - { amount: "0", currency: price.local.currency } - )} - -
- ) : chosenBreakfast?.code ? ( -
- - {intl.formatMessage({ id: "Breakfast buffet" })} - - - {intl.formatMessage( - { id: "{amount} {currency}" }, - { - amount: chosenBreakfast.localPrice.totalPrice, - currency: chosenBreakfast.localPrice.currency, - } - )} - -
- ) : null} -
- -
-
-
- - {intl.formatMessage( - { id: "Total price (incl VAT)" }, - { b: (str) => {str} } - )} - - - {intl.formatMessage({ id: "Price details" })} - -
-
- {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.amount), - currency: totalPrice.euro.currency, - } - )} - - )} -
-
- -
- + ) } diff --git a/components/HotelReservation/EnterDetails/Summary/summary.module.css b/components/HotelReservation/EnterDetails/Summary/summary.module.css index e4ee465a8..5cc412082 100644 --- a/components/HotelReservation/EnterDetails/Summary/summary.module.css +++ b/components/HotelReservation/EnterDetails/Summary/summary.module.css @@ -1,83 +1,68 @@ +.mobileSummary { + display: block; +} + +.desktopSummary { + display: none; +} + .summary { - border-radius: var(--Corner-radius-Large); - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); - padding: var(--Spacing-x3); - height: 100%; + background-color: var(--Main-Grey-White); + + border-color: var(--Primary-Light-On-Surface-Divider-subtle); + border-style: solid; + border-width: 1px; + border-bottom: none; + z-index: 10; } -.header { - display: grid; - grid-template-areas: "title button" "date button"; +.hider { + display: none; } -.title { - grid-area: title; -} - -.chevronButton { - grid-area: button; - justify-self: end; - align-items: center; - margin-right: calc(0px - var(--Spacing-x2)); -} - -.date { - align-items: center; - display: flex; - gap: var(--Spacing-x1); - justify-content: flex-start; - grid-area: date; -} - -.link { - margin-top: var(--Spacing-x1); -} - -.addOns { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-one-and-half); -} - -.rateDetailsPopover { - display: flex; - flex-direction: column; - gap: var(--Spacing-x-half); - max-width: 360px; -} - -.entry { - display: flex; - gap: var(--Spacing-x-half); - justify-content: space-between; -} - -.entry > :last-child { - justify-items: flex-end; -} - -.total { - display: flex; - flex-direction: column; - gap: var(--Spacing-x2); -} - -.bottomDivider { +.shadow { display: none; } @media screen and (min-width: 1367px) { - .bottomDivider { - display: block; - } - - .header { - display: block; - } - - .chevronButton { + .mobileSummary { display: none; } + + .desktopSummary { + display: grid; + grid-template-rows: auto auto 1fr; + margin-top: calc(0px - var(--Spacing-x9)); + } + + .summary { + position: sticky; + top: calc( + var(--booking-widget-desktop-height) + var(--Spacing-x2) + + var(--Spacing-x-half) + ); + z-index: 9; + border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0; + margin-top: calc(0px - var(--Spacing-x9)); + } + + .shadow { + display: block; + background-color: var(--Main-Grey-White); + border-color: var(--Primary-Light-On-Surface-Divider-subtle); + border-style: solid; + border-left-width: 1px; + border-right-width: 1px; + border-top: none; + border-bottom: none; + } + + .hider { + display: block; + background-color: var(--Scandic-Brand-Warm-White); + position: sticky; + top: calc(var(--booking-widget-desktop-height) - 6px); + margin-top: var(--Spacing-x4); + height: 40px; + } } diff --git a/components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css b/components/HotelReservation/Summary/BottomSheet/bottomSheet.module.css similarity index 100% rename from components/HotelReservation/EnterDetails/Summary/BottomSheet/bottomSheet.module.css rename to components/HotelReservation/Summary/BottomSheet/bottomSheet.module.css diff --git a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx b/components/HotelReservation/Summary/BottomSheet/index.tsx similarity index 87% rename from components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx rename to components/HotelReservation/Summary/BottomSheet/index.tsx index b6ea4b3c6..c71484e6b 100644 --- a/components/HotelReservation/EnterDetails/Summary/BottomSheet/index.tsx +++ b/components/HotelReservation/Summary/BottomSheet/index.tsx @@ -3,21 +3,20 @@ import { PropsWithChildren } from "react" import { useIntl } from "react-intl" -import { useDetailsStore } from "@/stores/details" +import { useEnterDetailsStore } from "@/stores/enter-details" +import { formId } from "@/components/HotelReservation/EnterDetails/Payment" import Button from "@/components/TempDesignSystem/Button" import Caption from "@/components/TempDesignSystem/Text/Caption" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" -import { formId } from "../../Payment" - import styles from "./bottomSheet.module.css" export function SummaryBottomSheet({ children }: PropsWithChildren) { const intl = useIntl() const { isSummaryOpen, toggleSummaryOpen, totalPrice, isSubmittingDisabled } = - useDetailsStore((state) => ({ + useEnterDetailsStore((state) => ({ isSummaryOpen: state.isSummaryOpen, toggleSummaryOpen: state.actions.toggleSummaryOpen, totalPrice: state.totalPrice, @@ -38,7 +37,7 @@ export function SummaryBottomSheet({ children }: PropsWithChildren) { {intl.formatMessage( { id: "{amount} {currency}" }, { - amount: intl.formatNumber(totalPrice.local.amount), + amount: intl.formatNumber(totalPrice.local.price), currency: totalPrice.local.currency, } )} diff --git a/components/HotelReservation/Summary/index.tsx b/components/HotelReservation/Summary/index.tsx new file mode 100644 index 000000000..1b309362a --- /dev/null +++ b/components/HotelReservation/Summary/index.tsx @@ -0,0 +1,234 @@ +"use client" +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" + +import { ArrowRightIcon, ChevronDownSmallIcon } from "@/components/Icons" +import Button from "@/components/TempDesignSystem/Button" +import Divider from "@/components/TempDesignSystem/Divider" +import Link from "@/components/TempDesignSystem/Link" +import Popover from "@/components/TempDesignSystem/Popover" +import Body from "@/components/TempDesignSystem/Text/Body" +import Caption from "@/components/TempDesignSystem/Text/Caption" +import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" +import useLang from "@/hooks/useLang" + +import styles from "./summary.module.css" + +import type { SummaryProps } from "@/types/components/hotelReservation/summary" +import { CurrencyEnum } from "@/types/enums/currency" + +export default function Summary({ + bedType, + breakfast, + fromDate, + showMemberPrice, + room, + toDate, + toggleSummaryOpen, + totalPrice, +}: SummaryProps) { + const intl = useIntl() + const lang = useLang() + + const diff = dt(toDate).diff(fromDate, "days") + + const nights = intl.formatMessage( + { id: "booking.nights" }, + { totalNights: diff } + ) + + function handleToggleSummary() { + if (toggleSummaryOpen) { + toggleSummaryOpen() + } + } + + return ( +
+
+ + {intl.formatMessage({ id: "Summary" })} + + + {dt(fromDate).locale(lang).format("ddd, D MMM")} + + {dt(toDate).locale(lang).format("ddd, D MMM")} ({nights}) + + +
+ +
+
+
+ {room.roomType} + + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: intl.formatNumber(room.roomPrice.local.price), + currency: room.roomPrice.local.currency, + } + )} + +
+ + {intl.formatMessage( + { id: "booking.adults" }, + { totalAdults: room.adults } + )} + + {room.children?.length ? ( + + {intl.formatMessage( + { id: "booking.children" }, + { totalChildren: room.children.length } + )} + + ) : null} + + {room.cancellationText} + + + {intl.formatMessage({ id: "Rate details" })} + + } + > + + +
+ {room.packages + ? room.packages.map((roomPackage) => ( +
+
+ + {roomPackage.description} + +
+ + + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: roomPackage.localPrice.price, + currency: roomPackage.localPrice.currency, + } + )} + +
+ )) + : null} + {bedType ? ( +
+
+ {bedType.description} + + {intl.formatMessage({ id: "Based on availability" })} + +
+ + + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: "0", currency: room.roomPrice.local.currency } + )} + +
+ ) : null} + + {breakfast === false ? ( +
+ + {intl.formatMessage({ id: "No breakfast" })} + + + {intl.formatMessage( + { id: "{amount} {currency}" }, + { amount: "0", currency: room.roomPrice.local.currency } + )} + +
+ ) : null} + {breakfast ? ( +
+ + {intl.formatMessage({ id: "Breakfast buffet" })} + + + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: breakfast.localPrice.totalPrice, + currency: breakfast.localPrice.currency, + } + )} + +
+ ) : null} +
+ +
+
+
+ + {intl.formatMessage( + { id: "Total price (incl VAT)" }, + { b: (str) => {str} } + )} + + + {intl.formatMessage({ id: "Price details" })} + +
+
+ + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: intl.formatNumber(totalPrice.local.price, { + currency: totalPrice.local.currency, + style: "currency", + }), + currency: totalPrice.local.currency, + } + )} + + {totalPrice.euro && ( + + {intl.formatMessage({ id: "Approx." })}{" "} + {intl.formatMessage( + { id: "{amount} {currency}" }, + { + amount: intl.formatNumber(totalPrice.euro.price, { + currency: CurrencyEnum.EUR, + style: "currency", + }), + currency: totalPrice.euro.currency, + } + )} + + )} +
+
+ +
+
+ ) +} diff --git a/components/HotelReservation/Summary/summary.module.css b/components/HotelReservation/Summary/summary.module.css new file mode 100644 index 000000000..9ab1f7ef3 --- /dev/null +++ b/components/HotelReservation/Summary/summary.module.css @@ -0,0 +1,83 @@ +.summary { + border-radius: var(--Corner-radius-Large); + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); + padding: var(--Spacing-x3); + height: 100%; +} + +.header { + display: grid; + grid-template-areas: "title button" "date button"; +} + +.title { + grid-area: title; +} + +.chevronButton { + grid-area: button; + justify-self: end; + align-items: center; + margin-right: calc(0px - var(--Spacing-x2)); +} + +.date { + align-items: center; + display: flex; + gap: var(--Spacing-x1); + justify-content: flex-start; + grid-area: date; +} + +.link { + margin-top: var(--Spacing-x1); +} + +.addOns { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-one-and-half); +} + +.rateDetailsPopover { + display: flex; + flex-direction: column; + gap: var(--Spacing-x-half); + max-width: 360px; +} + +.entry { + display: flex; + gap: var(--Spacing-x-half); + justify-content: space-between; +} + +.entry > :last-child { + justify-items: flex-end; +} + +.total { + display: flex; + flex-direction: column; + gap: var(--Spacing-x2); +} + +.bottomDivider { + display: none; +} + +@media screen and (min-width: 1367px) { + .bottomDivider { + display: block; + } + + .header { + display: block; + } + + .summary .header .chevronButton { + display: none; + } +} diff --git a/components/Icons/CalendarAdd.tsx b/components/Icons/CalendarAdd.tsx new file mode 100644 index 000000000..5b5e1465c --- /dev/null +++ b/components/Icons/CalendarAdd.tsx @@ -0,0 +1,31 @@ +import { iconVariants } from "./variants" + +import type { IconProps } from "@/types/components/icon" + +export default function CalendarAddIcon({ + className, + color, + ...props +}: IconProps) { + const classNames = iconVariants({ className, color }) + return ( + + + + + ) +} diff --git a/components/Icons/index.tsx b/components/Icons/index.tsx index b8d3ef8d9..f56e5128f 100644 --- a/components/Icons/index.tsx +++ b/components/Icons/index.tsx @@ -20,6 +20,7 @@ export { default as BreakfastIcon } from "./Breakfast" export { default as BusinessIcon } from "./Business" export { default as CableIcon } from "./Cable" export { default as CalendarIcon } from "./Calendar" +export { default as CalendarAddIcon } from "./CalendarAdd" export { default as CameraIcon } from "./Camera" export { default as CellphoneIcon } from "./Cellphone" export { default as ChairIcon } from "./Chair" diff --git a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx index 29ce8e531..013f1a373 100644 --- a/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx +++ b/components/TempDesignSystem/Form/ChoiceCard/_Card/index.tsx @@ -15,7 +15,6 @@ export default function Card({ iconHeight = 32, iconWidth = 32, declined = false, - defaultChecked, highlightSubtitle = false, id, list, @@ -58,7 +57,6 @@ export default function Card({ (null) diff --git a/contexts/Steps.ts b/contexts/Steps.ts deleted file mode 100644 index 220365fbe..000000000 --- a/contexts/Steps.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { createContext } from "react" - -import type { StepsStore } from "@/types/contexts/steps" - -export const StepsContext = createContext(null) diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index 4361b460f..38d6050b2 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -466,8 +466,8 @@ "booking.basedOnAvailability": "Based on availability", "booking.bedOptions": "Bed options", "booking.children": "{totalChildren, plural, one {# child} other {# children}}", - "booking.confirmation.text": "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please email us.", - "booking.confirmation.title": "Your booking is confirmed", + "booking.confirmation.text": "Thank you for booking with us! We look forward to welcoming you and hope you have a pleasant stay. If you have any questions or need to make changes to your reservation, please contact us.", + "booking.confirmation.title": "Booking confirmation", "booking.guests": "Max {nrOfGuests, plural, one {# guest} other {# guests}}", "booking.nights": "{totalNights, plural, one {# night} other {# nights}}", "booking.rooms": "{totalRooms, plural, one {# room} other {# rooms}}", diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 195443645..cb7453f0b 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -1,9 +1,4 @@ import { Lang } from "@/constants/languages" -import { - GetRoomsAvailabilityInput, - GetSelectedRoomAvailabilityInput, - HotelDataInput, -} from "@/server/routers/hotels/input" import { cache } from "@/utils/cache" @@ -13,6 +8,11 @@ import type { BreackfastPackagesInput, PackagesInput, } from "@/types/requests/packages" +import type { + GetRoomsAvailabilityInput, + GetSelectedRoomAvailabilityInput, + HotelDataInput, +} from "@/server/routers/hotels/input" export const getLocations = cache(async function getMemoizedLocations() { return serverClient().hotel.locations.get() @@ -60,21 +60,21 @@ export const getUserTracking = cache(async function getMemoizedUserTracking() { return serverClient().user.tracking() }) -export const getHotelData = cache(function getMemoizedHotelData( - props: HotelDataInput +export const getHotelData = cache(async function getMemoizedHotelData( + input: HotelDataInput ) { - return serverClient().hotel.hotelData.get(props) + return serverClient().hotel.hotelData.get(input) }) export const getHotelPage = cache(async function getMemoizedHotelPage() { return serverClient().contentstack.hotelPage.get() }) -export const getRoomsAvailability = cache(function getMemoizedRoomAvailability( - args: GetRoomsAvailabilityInput -) { - return serverClient().hotel.availability.rooms(args) -}) +export const getRoomsAvailability = cache( + async function getMemoizedRoomAvailability(input: GetRoomsAvailabilityInput) { + return serverClient().hotel.availability.rooms(input) + } +) export const getSelectedRoomAvailability = cache( function getMemoizedSelectedRoomAvailability( diff --git a/package-lock.json b/package-lock.json index 202dbf8ea..6785a5baa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3450,7 +3450,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -3466,7 +3465,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -3482,7 +3480,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -3498,7 +3495,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -3514,7 +3510,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -3530,7 +3525,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "linux" @@ -3546,7 +3540,6 @@ "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -3562,7 +3555,6 @@ "cpu": [ "ia32" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -3578,7 +3570,6 @@ "cpu": [ "x64" ], - "license": "MIT", "optional": true, "os": [ "win32" @@ -20392,4 +20383,4 @@ } } } -} +} \ No newline at end of file diff --git a/providers/DetailsProvider.tsx b/providers/DetailsProvider.tsx deleted file mode 100644 index 328307ee7..000000000 --- a/providers/DetailsProvider.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client" -import { useSearchParams } from "next/navigation" -import { useRef } from "react" - -import { createDetailsStore } from "@/stores/details" - -import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils" -import { DetailsContext } from "@/contexts/Details" - -import type { DetailsStore } from "@/types/contexts/details" -import type { DetailsProviderProps } from "@/types/providers/details" - -export default function DetailsProvider({ - children, - isMember, -}: DetailsProviderProps) { - const storeRef = useRef() - const searchParams = useSearchParams() - - if (!storeRef.current) { - const booking = getQueryParamsForEnterDetails(searchParams) - storeRef.current = createDetailsStore({ booking }, isMember) - } - - return ( - - {children} - - ) -} diff --git a/providers/EnterDetailsProvider.tsx b/providers/EnterDetailsProvider.tsx new file mode 100644 index 000000000..025fe14d5 --- /dev/null +++ b/providers/EnterDetailsProvider.tsx @@ -0,0 +1,50 @@ +"use client" +import { useRef } from "react" + +import { createDetailsStore } from "@/stores/enter-details" + +import { DetailsContext } from "@/contexts/Details" + +import type { DetailsStore } from "@/types/contexts/enter-details" +import type { DetailsProviderProps } from "@/types/providers/enter-details" +import type { InitialState } from "@/types/stores/enter-details" + +export default function EnterDetailsProvider({ + bedTypes, + booking, + breakfastPackages, + children, + packages, + roomRate, + searchParamsStr, + step, + user, +}: DetailsProviderProps) { + const storeRef = useRef() + + if (!storeRef.current) { + const initialData: InitialState = { booking, packages, roomRate } + if (bedTypes.length === 1) { + initialData.bedType = { + description: bedTypes[0].description, + roomTypeCode: bedTypes[0].value, + } + } + if (!breakfastPackages?.length) { + initialData.breakfast = false + } + + storeRef.current = createDetailsStore( + initialData, + step, + searchParamsStr, + user + ) + } + + return ( + + {children} + + ) +} diff --git a/providers/StepsProvider.tsx b/providers/StepsProvider.tsx deleted file mode 100644 index 9aaf6166f..000000000 --- a/providers/StepsProvider.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client" -import { useRouter } from "next/navigation" -import { useRef } from "react" - -import { useDetailsStore } from "@/stores/details" -import { createStepsStore } from "@/stores/steps" - -import { StepsContext } from "@/contexts/Steps" - -import type { StepsStore } from "@/types/contexts/steps" -import type { StepsProviderProps } from "@/types/providers/steps" - -export default function StepsProvider({ - bedTypes, - breakfastPackages, - children, - isMember, - searchParams, - step, -}: StepsProviderProps) { - const storeRef = useRef() - const updateBedType = useDetailsStore((state) => state.actions.updateBedType) - const updateBreakfast = useDetailsStore( - (state) => state.actions.updateBreakfast - ) - const router = useRouter() - - if (!storeRef.current) { - const noBedChoices = bedTypes.length === 1 - const noBreakfast = !breakfastPackages?.length - - if (noBedChoices) { - updateBedType({ - description: bedTypes[0].description, - roomTypeCode: bedTypes[0].value, - }) - } - - if (noBreakfast) { - updateBreakfast(false) - } - - storeRef.current = createStepsStore( - step, - isMember, - noBedChoices, - noBreakfast, - searchParams, - router.push - ) - } - - return ( - - {children} - - ) -} diff --git a/server/routers/booking/output.ts b/server/routers/booking/output.ts index 997d7baaa..0260d4484 100644 --- a/server/routers/booking/output.ts +++ b/server/routers/booking/output.ts @@ -54,19 +54,20 @@ export const createBookingSchema = z // QUERY const extraBedTypesSchema = z.object({ - quantity: z.number(), bedType: z.nativeEnum(ChildBedTypeEnum), + quantity: z.number().int(), }) const guestSchema = z.object({ email: z.string().email().nullable().default(""), - firstName: z.string(), - lastName: z.string(), + firstName: z.string().nullable().default(""), + lastName: z.string().nullable().default(""), + membershipNumber: z.string().nullable().default(""), phoneNumber: phoneValidator().nullable().default(""), }) const packageSchema = z.object({ - code: z.string().default(""), + code: z.string().nullable().default(""), currency: z.nativeEnum(CurrencyEnum), quantity: z.number().int(), totalPrice: z.number(), @@ -74,35 +75,37 @@ const packageSchema = z.object({ unitPrice: z.number(), }) +const rateDefinitionSchema = z.object({ + breakfastIncluded: z.boolean().default(false), + cancellationRule: z.string().nullable().default(""), + cancellationText: z.string().nullable().default(""), + generalTerms: z.array(z.string()).default([]), + isMemberRate: z.boolean().default(false), + mustBeGuaranteed: z.boolean().default(false), + rateCode: z.string().nullable().default(""), + title: z.string().nullable().default(""), +}) + export const bookingConfirmationSchema = z .object({ data: z.object({ attributes: z.object({ - adults: z.number(), + adults: z.number().int(), checkInDate: z.date({ coerce: true }), checkOutDate: z.date({ coerce: true }), createDateTime: z.date({ coerce: true }), - childrenAges: z.array(z.number()), + childrenAges: z.array(z.number().int()).default([]), extraBedTypes: z.array(extraBedTypesSchema).default([]), - computedReservationStatus: z.string(), - confirmationNumber: z.string(), + computedReservationStatus: z.string().nullable().default(""), + confirmationNumber: z.string().nullable().default(""), currencyCode: z.nativeEnum(CurrencyEnum), guest: guestSchema, hotelId: z.string(), - packages: z.array(packageSchema), - rateDefinition: z.object({ - rateCode: z.string(), - title: z.string().nullable(), - breakfastIncluded: z.boolean(), - isMemberRate: z.boolean(), - generalTerms: z.array(z.string()).optional(), - cancellationRule: z.string().optional(), - cancellationText: z.string().optional(), - mustBeGuaranteed: z.boolean(), - }), - reservationStatus: z.string(), - roomPrice: z.number().int(), - roomTypeCode: z.string(), + packages: z.array(packageSchema).default([]), + rateDefinition: rateDefinitionSchema, + reservationStatus: z.string().nullable().default(""), + roomPrice: z.number(), + roomTypeCode: z.string().nullable().default(""), totalPrice: z.number(), totalPriceExVat: z.number(), vatAmount: z.number(), diff --git a/stores/details.ts b/stores/details.ts deleted file mode 100644 index 250262ade..000000000 --- a/stores/details.ts +++ /dev/null @@ -1,189 +0,0 @@ -import merge from "deepmerge" -import { produce } from "immer" -import { useContext } from "react" -import { create, useStore } from "zustand" -import { createJSONStorage, persist } from "zustand/middleware" - -import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" -import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" -import { - guestDetailsSchema, - 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 detailsStorageName = "details-storage" -export function createDetailsStore( - initialState: InitialState, - isMember: boolean -) { - if (typeof window !== "undefined") { - /** - * We need to initialize the store from sessionStorage ourselves - * since `persist` does it first after render and therefore - * we cannot use the data as `defaultValues` for our forms. - * RHF caches defaultValues on mount. - */ - const detailsStorageUnparsed = sessionStorage.getItem(detailsStorageName) - if (detailsStorageUnparsed) { - const detailsStorage: Record< - "state", - Pick - > = JSON.parse(detailsStorageUnparsed) - initialState = merge(detailsStorage.state.data, initialState, { - arrayMerge, - }) - } - } - return create()( - persist( - (set) => ({ - actions: { - setIsSubmittingDisabled(isSubmittingDisabled) { - return set( - produce((state: DetailsState) => { - state.isSubmittingDisabled = isSubmittingDisabled - }) - ) - }, - setTotalPrice(totalPrice) { - return set( - produce((state: DetailsState) => { - state.totalPrice = totalPrice - }) - ) - }, - toggleSummaryOpen() { - return set( - produce((state: DetailsState) => { - state.isSummaryOpen = !state.isSummaryOpen - }) - ) - }, - updateBedType(bedType) { - return set( - produce((state: DetailsState) => { - state.isValid["select-bed"] = true - state.data.bedType = bedType - }) - ) - }, - updateBreakfast(breakfast) { - return set( - produce((state: DetailsState) => { - state.isValid.breakfast = true - state.data.breakfast = breakfast - }) - ) - }, - updateDetails(data) { - return set( - produce((state: DetailsState) => { - state.isValid.details = true - - state.data.countryCode = data.countryCode - state.data.dateOfBirth = data.dateOfBirth - state.data.email = data.email - state.data.firstName = data.firstName - state.data.join = data.join - state.data.lastName = data.lastName - if (data.join) { - state.data.membershipNo = undefined - } else { - state.data.membershipNo = data.membershipNo - } - state.data.phoneNumber = data.phoneNumber - state.data.zipCode = data.zipCode - }) - ) - }, - }, - - data: merge( - { - bedType: undefined, - breakfast: undefined, - countryCode: "", - dateOfBirth: "", - email: "", - firstName: "", - join: false, - lastName: "", - membershipNo: "", - phoneNumber: "", - termsAccepted: false, - zipCode: "", - }, - initialState - ), - - isSubmittingDisabled: false, - isSummaryOpen: false, - isValid: { - [StepEnum.selectBed]: false, - [StepEnum.breakfast]: false, - [StepEnum.details]: false, - [StepEnum.payment]: false, - }, - - totalPrice: { - euro: { currency: "", amount: 0 }, - local: { currency: "", amount: 0 }, - }, - }), - { - name: detailsStorageName, - onRehydrateStorage(prevState) { - return function (state) { - if (state) { - const validatedBedType = bedTypeSchema.safeParse(state.data) - if (validatedBedType.success !== state.isValid["select-bed"]) { - state.isValid["select-bed"] = validatedBedType.success - } - - const validatedBreakfast = breakfastStoreSchema.safeParse( - state.data - ) - 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.isValid.details) { - state.isValid.details = validatedDetails.success - } - - const mergedState = merge(state.data, prevState.data, { - arrayMerge, - }) - state.data = mergedState - } - } - }, - partialize(state) { - return { - data: state.data, - } - }, - storage: createJSONStorage(() => sessionStorage), - } - ) - ) -} - -export function useDetailsStore(selector: (store: DetailsState) => T) { - const store = useContext(DetailsContext) - - if (!store) { - throw new Error("useDetailsStore must be used within DetailsProvider") - } - - return useStore(store, selector) -} diff --git a/stores/enter-details/helpers.ts b/stores/enter-details/helpers.ts new file mode 100644 index 000000000..5958c5b45 --- /dev/null +++ b/stores/enter-details/helpers.ts @@ -0,0 +1,354 @@ +import { z } from "zod" + +import { Lang } from "@/constants/languages" +import { breakfastPackageSchema } from "@/server/routers/hotels/output" + +import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" +import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" +import { + guestDetailsSchema, + signedInDetailsSchema, +} from "@/components/HotelReservation/EnterDetails/Details/schema" +import { getLang } from "@/i18n/serverContext" + +import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" +import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" +import { CurrencyEnum } from "@/types/enums/currency" +import { StepEnum } from "@/types/enums/step" +import type { DetailsState, RoomRate } from "@/types/stores/enter-details" +import type { SafeUser } from "@/types/user" + +export function langToCurrency() { + const lang = getLang() + switch (lang) { + case Lang.da: + return CurrencyEnum.DKK + case Lang.de: + case Lang.en: + case Lang.fi: + return CurrencyEnum.EUR + case Lang.no: + return CurrencyEnum.NOK + case Lang.sv: + return CurrencyEnum.SEK + default: + throw new Error(`Unexpected lang: ${lang}`) + } +} + +export function extractGuestFromUser(user: NonNullable) { + return { + countryCode: user.address.countryCode?.toString(), + email: user.email, + firstName: user.firstName, + lastName: user.lastName, + join: false, + membershipNo: user.membership?.membershipNumber, + phoneNumber: user.phoneNumber ?? "", + } +} + +export function navigate(step: StepEnum, searchParams: string) { + window.history.pushState({ step }, "", `${step}?${searchParams}`) +} + +export function checkIsSameBooking(prev: BookingData, next: BookingData) { + return ( + prev.fromDate === next.fromDate || + prev.toDate === next.toDate || + prev.hotel === next.hotel || + prev.rooms[0].adults === next.rooms[0].adults || + prev.rooms[0].children === next.rooms[0].children || + prev.rooms[0].roomTypeCode === next.rooms[0].roomTypeCode + ) +} + +export function validateSteps(currentState: DetailsState, isMember: boolean) { + const validPaths = [StepEnum.selectBed] + const validatedBedType = bedTypeSchema.safeParse(currentState) + if (validatedBedType.success) { + currentState.isValid["select-bed"] = true + validPaths.push(currentState.steps[1]) + } + + const validatedBreakfast = breakfastStoreSchema.safeParse(currentState) + if (validatedBreakfast.success) { + currentState.isValid.breakfast = true + validPaths.push(StepEnum.details) + } + + const detailsSchema = isMember ? signedInDetailsSchema : guestDetailsSchema + const validatedDetails = detailsSchema.safeParse(currentState.guest) + // Need to add the breakfast check here too since + // when a member comes into the flow, their data is + // already added and valid, and thus to avoid showing a + // step the user hasn't been on yet as complete + if (currentState.isValid.breakfast && validatedDetails.success) { + currentState.isValid.details = true + validPaths.push(StepEnum.payment) + } + + return validPaths +} + +export function add(...nums: (number | string | undefined)[]) { + return nums.reduce((total: number, num) => { + if (typeof num === "undefined") { + num = 0 + } + total = total + parseInt(`${num}`) + return total + }, 0) +} + +export function subtract(...nums: (number | string | undefined)[]) { + return nums.reduce((total: number, num, idx) => { + if (typeof num === "undefined") { + num = 0 + } + if (idx === 0) { + return parseInt(`${num}`) + } + total = total - parseInt(`${num}`) + if (total < 0) { + return 0 + } + return total + }, 0) +} + +export function getInitialRoomPrice(roomRate: RoomRate, isMember: boolean) { + if (isMember && roomRate.memberRate) { + return { + euro: { + currency: CurrencyEnum.EUR, + price: roomRate.memberRate.requestedPrice?.pricePerStay ?? 0, + }, + local: { + currency: roomRate.memberRate.localPrice.currency, + price: roomRate.memberRate.localPrice.pricePerStay, + }, + } + } + + return { + euro: { + currency: CurrencyEnum.EUR, + price: roomRate.publicRate.requestedPrice?.pricePerStay ?? 0, + }, + local: { + currency: roomRate.publicRate.localPrice.currency, + price: roomRate.publicRate.localPrice.pricePerStay, + }, + } +} + +export function getInitialTotalPrice(roomRate: RoomRate, isMember: boolean) { + if (isMember && roomRate.memberRate) { + return { + euro: { + currency: CurrencyEnum.EUR, + price: roomRate.memberRate.requestedPrice?.pricePerStay ?? 0, + }, + local: { + currency: roomRate.memberRate.localPrice.currency, + price: roomRate.memberRate.localPrice.pricePerStay, + }, + } + } + + return { + euro: { + currency: CurrencyEnum.EUR, + price: roomRate.publicRate.requestedPrice?.pricePerStay ?? 0, + }, + local: { + currency: roomRate.publicRate.localPrice.currency, + price: roomRate.publicRate.localPrice.pricePerStay, + }, + } +} + +export function calcTotalMemberPrice(state: DetailsState) { + if (!state.roomRate.memberRate) { + return { + roomPrice: state.roomPrice, + totalPrice: state.totalPrice, + } + } + + const roomAndTotalPrice = { + roomPrice: state.roomPrice, + totalPrice: state.totalPrice, + } + if (state.roomRate.memberRate.requestedPrice?.pricePerStay) { + roomAndTotalPrice.roomPrice.euro = { + currency: CurrencyEnum.EUR, + price: state.roomRate.memberRate.requestedPrice.pricePerStay, + } + + let totalPriceEuro = state.roomRate.memberRate.requestedPrice.pricePerStay + if (state.breakfast) { + totalPriceEuro = add( + totalPriceEuro, + state.breakfast.requestedPrice.totalPrice + ) + } + + if (state.packages) { + totalPriceEuro = state.packages.reduce((total, pkg) => { + if (pkg.requestedPrice.totalPrice) { + total = add(total, pkg.requestedPrice.totalPrice) + } + return total + }, totalPriceEuro) + } + + roomAndTotalPrice.totalPrice.euro = { + currency: CurrencyEnum.EUR, + price: totalPriceEuro, + } + } + + const roomPriceLocal = state.roomRate.memberRate.localPrice + roomAndTotalPrice.roomPrice.local = { + currency: roomPriceLocal.currency, + price: roomPriceLocal.pricePerStay, + } + + let totalPriceLocal = roomPriceLocal.pricePerStay + if (state.breakfast) { + totalPriceLocal = add( + totalPriceLocal, + state.breakfast.localPrice.totalPrice + ) + } + + if (state.packages) { + totalPriceLocal = state.packages.reduce((total, pkg) => { + if (pkg.localPrice.totalPrice) { + total = add(total, pkg.localPrice.totalPrice) + } + return total + }, totalPriceLocal) + } + roomAndTotalPrice.totalPrice.local = { + currency: roomPriceLocal.currency, + price: totalPriceLocal, + } + + return roomAndTotalPrice +} + +export function getHydratedMemberPrice( + memberRate: NonNullable, + breakfast: DetailsState["breakfast"], + packages: DetailsState["packages"] +) { + const memberPrice = { + euro: { + currency: CurrencyEnum.EUR, + price: memberRate.requestedPrice?.pricePerStay ?? 0, + }, + local: { + currency: memberRate.localPrice.currency, + price: memberRate.localPrice.pricePerStay, + }, + } + + if (breakfast) { + memberPrice.euro.price = add( + memberPrice.euro.price, + breakfast.requestedPrice.totalPrice + ) + memberPrice.local.price = add( + memberPrice.local.price, + breakfast.localPrice.totalPrice + ) + } + + if (packages) { + packages.forEach((pkg) => { + memberPrice.euro.price = add( + memberPrice.euro.price, + pkg.requestedPrice.totalPrice + ) + memberPrice.local.price = add( + memberPrice.local.price, + pkg.localPrice.totalPrice + ) + }) + } + + return { + roomPrice: { + euro: { + currency: CurrencyEnum.EUR, + price: memberRate.requestedPrice?.pricePerStay ?? 0, + }, + local: { + currency: memberRate.localPrice.currency, + price: memberRate.localPrice.pricePerStay, + }, + }, + totalPrice: memberPrice, + } +} + +export const persistedStateSchema = z + .object({ + bedType: z + .object({ + description: z.string(), + roomTypeCode: z.string(), + }) + .optional(), + booking: z.object({ + hotel: z.string(), + fromDate: z.string(), + toDate: z.string(), + rooms: z.array( + z.object({ + adults: z.number().int(), + counterRateCode: z.string(), + rateCode: z.string(), + roomTypeCode: z.string(), + children: z + .array( + z.object({ + age: z.number().int(), + bed: z.string(), + }) + ) + .optional(), + packages: z.array(z.nativeEnum(RoomPackageCodeEnum)).optional(), + }) + ), + }), + breakfast: breakfastPackageSchema.or(z.literal(false)).optional(), + guest: z.object({ + countryCode: z.string().default(""), + email: z.string().default(""), + firstName: z.string().default(""), + lastName: z.string().default(""), + membershipNo: z.string().default(""), + phoneNumber: z.string().default(""), + join: z + .boolean() + .optional() + .transform((_) => false), + dateOfBirth: z.string().default(""), + zipCode: z.string().default(""), + }), + totalPrice: z.object({ + euro: z.object({ + currency: z.literal(CurrencyEnum.EUR), + price: z.number().int(), + }), + local: z.object({ + currency: z.nativeEnum(CurrencyEnum), + price: z.number().int(), + }), + }), + }) + .optional() diff --git a/stores/enter-details/index.ts b/stores/enter-details/index.ts new file mode 100644 index 000000000..f40bc2c2d --- /dev/null +++ b/stores/enter-details/index.ts @@ -0,0 +1,448 @@ +import deepmerge from "deepmerge" +import { produce } from "immer" +import { useContext } from "react" +import { create, useStore } from "zustand" +import { createJSONStorage, persist } from "zustand/middleware" + +import { DetailsContext } from "@/contexts/Details" +import { arrayMerge } from "@/utils/merge" + +import { + add, + calcTotalMemberPrice, + checkIsSameBooking, + extractGuestFromUser, + getHydratedMemberPrice, + getInitialRoomPrice, + getInitialTotalPrice, + langToCurrency, + navigate, + persistedStateSchema, + validateSteps, +} from "./helpers" + +import { CurrencyEnum } from "@/types/enums/currency" +import { StepEnum } from "@/types/enums/step" +import type { + DetailsState, + FormValues, + InitialState, + PersistedState, +} from "@/types/stores/enter-details" +import type { SafeUser } from "@/types/user" + +const defaultGuestState = { + countryCode: "", + dateOfBirth: "", + email: "", + firstName: "", + join: false, + lastName: "", + membershipNo: "", + phoneNumber: "", + zipCode: "", +} + +export const detailsStorageName = "details-storage" +export function createDetailsStore( + initialState: InitialState, + currentStep: StepEnum, + searchParams: string, + user: SafeUser +) { + const isMember = !!user + const isBrowser = typeof window !== "undefined" + + // Spread is done on purpose since we want + // a copy of initialState and not alter the + // original + const formValues: FormValues = { + bedType: initialState.bedType, + booking: initialState.booking, + breakfast: undefined, + guest: isMember + ? deepmerge(defaultGuestState, extractGuestFromUser(user)) + : defaultGuestState, + } + if (isBrowser) { + /** + * We need to initialize the store from sessionStorage ourselves + * since `persist` does it first after render and therefore + * we cannot use the data as `defaultValues` for our forms. + * RHF caches defaultValues on mount. + */ + const detailsStorageUnparsed = sessionStorage.getItem(detailsStorageName) + if (detailsStorageUnparsed) { + const detailsStorage: Record<"state", FormValues> = JSON.parse( + detailsStorageUnparsed + ) + + const isSameBooking = checkIsSameBooking( + detailsStorage.state.booking, + initialState.booking + ) + + if (isSameBooking) { + if (!initialState.bedType && detailsStorage.state.bedType) { + formValues.bedType = detailsStorage.state.bedType + } + + if ("breakfast" in detailsStorage.state) { + formValues.breakfast = detailsStorage.state.breakfast + } + + if ("guest" in detailsStorage.state) { + if (!user) { + formValues.guest = deepmerge( + defaultGuestState, + detailsStorage.state.guest, + { arrayMerge } + ) + } + } + } + } + } + + const initialRoomPrice = getInitialRoomPrice(initialState.roomRate, isMember) + const initialTotalPrice = getInitialTotalPrice( + initialState.roomRate, + isMember + ) + + if (initialState.packages) { + initialState.packages.forEach((pkg) => { + initialTotalPrice.euro.price = add( + initialTotalPrice.euro.price, + pkg.requestedPrice.totalPrice + ) + initialTotalPrice.local.price = add( + initialTotalPrice.local.price, + pkg.localPrice.totalPrice + ) + }) + } + + return create()( + persist( + (set) => ({ + actions: { + completeStep() { + return set( + produce((state: DetailsState) => { + const currentStepIndex = state.steps.indexOf(state.currentStep) + const nextStep = state.steps[currentStepIndex + 1] + state.currentStep = nextStep + navigate(nextStep, searchParams) + }) + ) + }, + navigate(step: StepEnum) { + return set( + produce((state) => { + state.currentStep = step + navigate(step, searchParams) + }) + ) + }, + setIsSubmittingDisabled(isSubmittingDisabled) { + return set( + produce((state: DetailsState) => { + state.isSubmittingDisabled = isSubmittingDisabled + }) + ) + }, + setStep(step: StepEnum) { + return set( + produce((state: DetailsState) => { + state.currentStep = step + }) + ) + }, + setTotalPrice(totalPrice) { + return set( + produce((state: DetailsState) => { + state.totalPrice.euro = totalPrice.euro + state.totalPrice.local = totalPrice.local + }) + ) + }, + toggleSummaryOpen() { + return set( + produce((state: DetailsState) => { + state.isSummaryOpen = !state.isSummaryOpen + }) + ) + }, + updateBedType(bedType) { + return set( + produce((state: DetailsState) => { + state.isValid["select-bed"] = true + state.bedType = bedType + + const currentStepIndex = state.steps.indexOf(state.currentStep) + const nextStep = state.steps[currentStepIndex + 1] + state.currentStep = nextStep + navigate(nextStep, searchParams) + }) + ) + }, + updateBreakfast(breakfast) { + return set( + produce((state: DetailsState) => { + state.isValid.breakfast = true + const stateTotalEuroPrice = state.totalPrice.euro?.price || 0 + const stateTotalLocalPrice = state.totalPrice.local.price + + const addToTotalPrice = + (state.breakfast === undefined || + state.breakfast === false) && + !!breakfast + const subtractFromTotalPrice = + (state.breakfast === undefined || state.breakfast) && + breakfast === false + + if (addToTotalPrice) { + const breakfastTotalEuroPrice = parseInt( + breakfast.requestedPrice.totalPrice + ) + const breakfastTotalPrice = parseInt( + breakfast.localPrice.totalPrice + ) + + state.totalPrice = { + euro: { + currency: CurrencyEnum.EUR, + price: stateTotalEuroPrice + breakfastTotalEuroPrice, + }, + local: { + currency: breakfast.localPrice.currency, + price: stateTotalLocalPrice + breakfastTotalPrice, + }, + } + } + + if (subtractFromTotalPrice) { + let currency = + state.totalPrice.local.currency ?? langToCurrency() + let currentBreakfastTotalPrice = 0 + let currentBreakfastTotalEuroPrice = 0 + if (state.breakfast) { + currentBreakfastTotalPrice = parseInt( + state.breakfast.localPrice.totalPrice + ) + currentBreakfastTotalEuroPrice = parseInt( + state.breakfast.requestedPrice.totalPrice + ) + currency = state.breakfast.localPrice.currency + } + + let euroPrice = + stateTotalEuroPrice - currentBreakfastTotalEuroPrice + if (euroPrice < 0) { + euroPrice = 0 + } + let localPrice = + stateTotalLocalPrice - currentBreakfastTotalPrice + if (localPrice < 0) { + localPrice = 0 + } + + state.totalPrice = { + euro: { + currency: CurrencyEnum.EUR, + price: euroPrice, + }, + local: { + currency, + price: localPrice, + }, + } + } + + state.breakfast = breakfast + + const currentStepIndex = state.steps.indexOf(state.currentStep) + const nextStep = state.steps[currentStepIndex + 1] + state.currentStep = nextStep + navigate(nextStep, searchParams) + }) + ) + }, + updateDetails(data) { + return set( + produce((state: DetailsState) => { + state.isValid.details = true + + state.guest.countryCode = data.countryCode + state.guest.dateOfBirth = data.dateOfBirth + state.guest.email = data.email + state.guest.firstName = data.firstName + state.guest.join = data.join + state.guest.lastName = data.lastName + if (data.join) { + state.guest.membershipNo = undefined + } else { + state.guest.membershipNo = data.membershipNo + } + state.guest.phoneNumber = data.phoneNumber + state.guest.zipCode = data.zipCode + + if (data.join || data.membershipNo || isMember) { + const memberPrice = calcTotalMemberPrice(state) + state.roomPrice = memberPrice.roomPrice + state.totalPrice = memberPrice.totalPrice + } + + const currentStepIndex = state.steps.indexOf(state.currentStep) + const nextStep = state.steps[currentStepIndex + 1] + state.currentStep = nextStep + navigate(nextStep, searchParams) + }) + ) + }, + }, + + bedType: initialState.bedType ?? undefined, + booking: initialState.booking, + breakfast: undefined, + currentStep, + formValues, + guest: isMember + ? deepmerge(defaultGuestState, extractGuestFromUser(user)) + : defaultGuestState, + isSubmittingDisabled: false, + isSummaryOpen: false, + isValid: { + [StepEnum.selectBed]: false, + [StepEnum.breakfast]: false, + [StepEnum.details]: false, + [StepEnum.payment]: false, + }, + packages: initialState.packages, + roomPrice: initialRoomPrice, + roomRate: initialState.roomRate, + steps: [ + StepEnum.selectBed, + StepEnum.breakfast, + StepEnum.details, + StepEnum.payment, + ], + totalPrice: initialTotalPrice, + }), + { + name: detailsStorageName, + merge(_persistedState, currentState) { + const parsedPersistedState = + persistedStateSchema.safeParse(_persistedState) + let persistedState + if (parsedPersistedState.success) { + if (parsedPersistedState.data) { + persistedState = parsedPersistedState.data as PersistedState + } + } + + if (!persistedState) { + persistedState = currentState as DetailsState + } + + if ( + currentState.guest.join || + !!currentState.guest.membershipNo || + isMember + ) { + if (currentState.roomRate.memberRate) { + const memberPrice = getHydratedMemberPrice( + currentState.roomRate.memberRate, + currentState.breakfast, + currentState.packages + ) + + currentState.roomPrice = memberPrice.roomPrice + currentState.totalPrice = memberPrice.totalPrice + } + } + + const isSameBooking = checkIsSameBooking( + persistedState.booking, + currentState.booking + ) + + let mergedState + if (isSameBooking) { + mergedState = deepmerge( + currentState, + persistedState, + { arrayMerge } + ) + } else { + mergedState = deepmerge( + persistedState, + currentState, + { arrayMerge } + ) + } + + /** + * TODO: + * - when included in rate, can packages still be received? + * - no hotels yet with breakfast included in the rate so + * impossible to build for atm. + * + * checking against initialState since that means the + * hotel doesn't offer breakfast + * + * matching breakfast first so the steps array is altered + * before the bedTypes possible step altering + */ + if (initialState.breakfast === false) { + mergedState.steps = mergedState.steps.filter( + (step) => step === StepEnum.breakfast + ) + if (mergedState.currentStep === StepEnum.breakfast) { + mergedState.currentStep = mergedState.steps[1] + } + } + + if (initialState.bedType) { + if (mergedState.currentStep === StepEnum.selectBed) { + mergedState.currentStep = mergedState.steps[1] + } + } + + const validPaths = validateSteps(mergedState, isMember) + if (!validPaths.includes(mergedState.currentStep)) { + mergedState.currentStep = validPaths.at(-1)! + } + + if (currentStep !== mergedState.currentStep) { + setTimeout(() => { + navigate(mergedState.currentStep, searchParams) + }) + } + return mergedState + }, + partialize(state) { + return { + bedType: state.bedType, + booking: state.booking, + breakfast: state.breakfast, + guest: state.guest, + totalPrice: state.totalPrice, + } + }, + storage: createJSONStorage(() => sessionStorage), + } + ) + ) +} + +export function useEnterDetailsStore(selector: (store: DetailsState) => T) { + const store = useContext(DetailsContext) + + if (!store) { + throw new Error("useEnterDetailsStore must be used within DetailsProvider") + } + + return useStore(store, selector) +} diff --git a/stores/steps.ts b/stores/steps.ts deleted file mode 100644 index efa356c8b..000000000 --- a/stores/steps.ts +++ /dev/null @@ -1,156 +0,0 @@ -"use client" -import merge from "deepmerge" -import { produce } from "immer" -import { AppRouterInstance } from "next/dist/shared/lib/app-router-context.shared-runtime" -import { useContext } from "react" -import { create, useStore } from "zustand" - -import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" -import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" -import { - guestDetailsSchema, - signedInDetailsSchema, -} from "@/components/HotelReservation/EnterDetails/Details/schema" -import { StepsContext } from "@/contexts/Steps" - -import { detailsStorageName as detailsStorageName } from "./details" - -import { StepEnum } from "@/types/enums/step" -import type { DetailsState } from "@/types/stores/details" -import type { StepState } from "@/types/stores/steps" - -export function createStepsStore( - currentStep: StepEnum, - isMember: boolean, - noBedChoices: boolean, - noBreakfast: boolean, - searchParams: string, - push: AppRouterInstance["push"] -) { - const isBrowser = typeof window !== "undefined" - const steps = [ - StepEnum.selectBed, - StepEnum.breakfast, - StepEnum.details, - StepEnum.payment, - ] - - /** - * TODO: - * - when included in rate, can packages still be received? - * - no hotels yet with breakfast included in the rate so - * impossible to build for atm. - * - * matching breakfast first so the steps array is altered - * before the bedTypes possible step altering - */ - if (noBreakfast) { - steps.splice(1, 1) - if (currentStep === StepEnum.breakfast) { - currentStep = steps[1] - push(`${currentStep}?${searchParams}`) - } - } - - if (noBedChoices) { - if (currentStep === StepEnum.selectBed) { - currentStep = steps[1] - push(`${currentStep}?${searchParams}`) - } - } - - const detailsStorageUnparsed = isBrowser - ? sessionStorage.getItem(detailsStorageName) - : null - if (detailsStorageUnparsed) { - const detailsStorage: Record< - "state", - Pick - > = JSON.parse(detailsStorageUnparsed) - - const validPaths = [StepEnum.selectBed] - - const validatedBedType = bedTypeSchema.safeParse(detailsStorage.state.data) - if (validatedBedType.success) { - validPaths.push(steps[1]) - } - - const validatedBreakfast = breakfastStoreSchema.safeParse( - detailsStorage.state.data - ) - if (validatedBreakfast.success) { - validPaths.push(StepEnum.details) - } - - const detailsSchema = isMember ? signedInDetailsSchema : guestDetailsSchema - const validatedDetails = detailsSchema.safeParse(detailsStorage.state.data) - if (validatedDetails.success) { - validPaths.push(StepEnum.payment) - } - - if (!validPaths.includes(currentStep) && isBrowser) { - // We will always have at least one valid path - currentStep = validPaths.pop()! - push(`${currentStep}?${searchParams}`) - } - } - - const initalData = { - currentStep, - steps, - } - - return create()((set) => - merge( - { - currentStep: StepEnum.selectBed, - steps: [], - - completeStep() { - return set( - produce((state: StepState) => { - const currentStepIndex = state.steps.indexOf(state.currentStep) - const nextStep = state.steps[currentStepIndex + 1] - state.currentStep = nextStep - window.history.pushState( - { step: nextStep }, - "", - nextStep + window.location.search - ) - }) - ) - }, - navigate(step: StepEnum) { - return set( - produce((state) => { - state.currentStep = step - window.history.pushState( - { step }, - "", - step + window.location.search - ) - }) - ) - }, - setStep(step: StepEnum) { - return set( - produce((state: StepState) => { - state.currentStep = step - }) - ) - }, - }, - initalData - ) - ) -} - -export function useStepsStore(selector: (store: StepState) => T) { - const store = useContext(StepsContext) - - if (!store) { - throw new Error(`useStepsStore must be used within StepsProvider`) - } - - return useStore(store, selector) -} diff --git a/types/components/hotelReservation/enterDetails/bookingData.ts b/types/components/hotelReservation/enterDetails/bookingData.ts index 0683c4739..c78b5c90e 100644 --- a/types/components/hotelReservation/enterDetails/bookingData.ts +++ b/types/components/hotelReservation/enterDetails/bookingData.ts @@ -1,7 +1,5 @@ -import { RoomPackageCodeEnum } from "../selectRate/roomFilter" -import { Child } from "../selectRate/selectRate" - -import { Packages } from "@/types/requests/packages" +import type { RoomPackageCodeEnum } from "../selectRate/roomFilter" +import type { Child } from "../selectRate/selectRate" interface Room { adults: number @@ -17,29 +15,3 @@ export interface BookingData { toDate: string rooms: Room[] } - -type Price = { - amount: number - currency: string -} - -export type RoomsData = { - roomType: string - prices: { - public: { - local: Price - euro: Price | undefined - } - member: - | { - local: Price - euro: Price | undefined - } - | undefined - } - adults: number - children?: Child[] - rateDetails?: string[] - cancellationText: string - packages: Packages | null -} diff --git a/types/components/hotelReservation/enterDetails/details.ts b/types/components/hotelReservation/enterDetails/details.ts index 25004467a..3d7fc41c1 100644 --- a/types/components/hotelReservation/enterDetails/details.ts +++ b/types/components/hotelReservation/enterDetails/details.ts @@ -1,12 +1,19 @@ import { z } from "zod" -import { guestDetailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" +import { + guestDetailsSchema, + signedInDetailsSchema, +} from "@/components/HotelReservation/EnterDetails/Details/schema" import type { SafeUser } from "@/types/user" export type DetailsSchema = z.output +export type SignedInDetailsSchema = z.output -type MemberPrice = { price: number; currency: string } +type MemberPrice = { + currency: string + price: number +} export interface DetailsProps { user: SafeUser diff --git a/types/components/hotelReservation/enterDetails/hotelHeader.ts b/types/components/hotelReservation/enterDetails/hotelHeader.ts new file mode 100644 index 000000000..2d7d2185d --- /dev/null +++ b/types/components/hotelReservation/enterDetails/hotelHeader.ts @@ -0,0 +1,7 @@ +import type { RouterOutput } from "@/lib/trpc/client" + +type HotelDataGet = RouterOutput["hotel"]["hotelData"]["get"] + +export interface HotelHeaderProps { + hotel: NonNullable["data"]["attributes"] +} diff --git a/types/components/hotelReservation/enterDetails/step.ts b/types/components/hotelReservation/enterDetails/step.ts deleted file mode 100644 index 8c8c967ef..000000000 --- a/types/components/hotelReservation/enterDetails/step.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { StepEnum } from "@/types/enums/step" - -export const StepStoreKeys: Record = { - "select-bed": "bedType", - breakfast: "breakfast", - details: null, - payment: null, -} diff --git a/types/components/hotelReservation/enterDetails/summary.ts b/types/components/hotelReservation/enterDetails/summary.ts index 901113414..5b6f82754 100644 --- a/types/components/hotelReservation/enterDetails/summary.ts +++ b/types/components/hotelReservation/enterDetails/summary.ts @@ -1,6 +1,14 @@ -import type { RoomsData } from "./bookingData" +import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate" +import type { Packages } from "@/types/requests/packages" +import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability" -export interface SummaryProps { - showMemberPrice: boolean - room: RoomsData +export interface ClientSummaryProps + extends Pick< + RoomAvailability, + "cancellationText" | "memberRate" | "rateDetails" + >, + Pick { + adults: number + isMember: boolean + kids: Child[] | undefined } diff --git a/types/components/hotelReservation/summary.ts b/types/components/hotelReservation/summary.ts new file mode 100644 index 000000000..de8e20c4c --- /dev/null +++ b/types/components/hotelReservation/summary.ts @@ -0,0 +1,39 @@ +import { RoomPackageCodeEnum } from "./selectRate/roomFilter" + +import type { Packages } from "@/types/requests/packages" +import type { DetailsState, Price } from "@/types/stores/enter-details" +import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability" +import type { BedTypeSchema } from "./enterDetails/bedType" +import type { BreakfastPackage } from "./enterDetails/breakfast" +import type { Child } from "./selectRate/selectRate" + +export type RoomsData = Pick & + Pick & + Pick & { + adults: number + children?: Child[] + packages: Packages | null + } + +interface SharedSummaryProps { + fromDate: string + toDate: string +} + +export interface SummaryProps extends SharedSummaryProps { + bedType: BedTypeSchema | undefined + breakfast: BreakfastPackage | false | undefined + showMemberPrice: boolean + room: RoomsData + toggleSummaryOpen?: () => void + totalPrice: Price +} + +export interface SummaryPageProps extends SharedSummaryProps { + adults: number + hotelId: string + kids: Child[] | undefined + packageCodes: RoomPackageCodeEnum[] | undefined + rateCode: string + roomTypeCode: string +} diff --git a/types/contexts/details.ts b/types/contexts/enter-details.ts similarity index 52% rename from types/contexts/details.ts rename to types/contexts/enter-details.ts index ea6b65edd..176418279 100644 --- a/types/contexts/details.ts +++ b/types/contexts/enter-details.ts @@ -1,3 +1,3 @@ -import { createDetailsStore } from "@/stores/details" +import { createDetailsStore } from "@/stores/enter-details" export type DetailsStore = ReturnType diff --git a/types/contexts/steps.ts b/types/contexts/steps.ts deleted file mode 100644 index 40c3cb55e..000000000 --- a/types/contexts/steps.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createStepsStore } from "@/stores/steps" - -export type StepsStore = ReturnType diff --git a/types/providers/details.ts b/types/providers/details.ts deleted file mode 100644 index c58effb2c..000000000 --- a/types/providers/details.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface DetailsProviderProps extends React.PropsWithChildren { - isMember: boolean -} diff --git a/types/providers/enter-details.ts b/types/providers/enter-details.ts new file mode 100644 index 000000000..456145817 --- /dev/null +++ b/types/providers/enter-details.ts @@ -0,0 +1,18 @@ +import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" +import { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" +import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" +import { StepEnum } from "@/types/enums/step" +import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability" +import type { SafeUser } from "@/types/user" +import type { Packages } from "../requests/packages" + +export interface DetailsProviderProps extends React.PropsWithChildren { + booking: BookingData + bedTypes: BedTypeSelection[] + breakfastPackages: BreakfastPackage[] | null + packages: Packages | null + roomRate: Pick + searchParamsStr: string + step: StepEnum + user: SafeUser +} diff --git a/types/providers/steps.ts b/types/providers/steps.ts deleted file mode 100644 index 9ba0361eb..000000000 --- a/types/providers/steps.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" -import { StepEnum } from "@/types/enums/step" -import type { BreakfastPackage } from "../components/hotelReservation/enterDetails/breakfast" - -export interface StepsProviderProps extends React.PropsWithChildren { - bedTypes: BedTypeSelection[] - breakfastPackages: BreakfastPackage[] | null - isMember: boolean - searchParams: string - step: StepEnum -} diff --git a/types/stores/details.ts b/types/stores/details.ts deleted file mode 100644 index 72b7f490b..000000000 --- a/types/stores/details.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" -import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" -import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" -import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details" -import { StepEnum } from "@/types/enums/step" - -export interface DetailsState { - actions: { - setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void - setTotalPrice: (totalPrice: TotalPrice) => void - toggleSummaryOpen: () => void - updateBedType: (data: BedTypeSchema) => void - updateBreakfast: (data: BreakfastPackage | false) => void - updateDetails: (data: DetailsSchema) => void - } - data: DetailsSchema & { - bedType: BedTypeSchema | undefined - breakfast: BreakfastPackage | false | undefined - booking: BookingData - } - isSubmittingDisabled: boolean - isSummaryOpen: boolean - isValid: Record - totalPrice: TotalPrice -} - -export interface InitialState extends Partial { - booking: BookingData -} - -interface Price { - currency: string - amount: number -} - -export interface TotalPrice { - euro: Price | undefined - local: Price -} diff --git a/types/stores/enter-details.ts b/types/stores/enter-details.ts new file mode 100644 index 000000000..a7943e35b --- /dev/null +++ b/types/stores/enter-details.ts @@ -0,0 +1,68 @@ +import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType" +import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData" +import type { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast" +import type { + DetailsSchema, + SignedInDetailsSchema, +} from "@/types/components/hotelReservation/enterDetails/details" +import { StepEnum } from "@/types/enums/step" +import type { DetailsProviderProps } from "../providers/enter-details" +import type { Packages } from "../requests/packages" + +interface TPrice { + currency: string + price: number +} + +export interface Price { + euro: TPrice | undefined + local: TPrice +} + +export interface FormValues { + bedType: BedTypeSchema | undefined + booking: BookingData + breakfast: BreakfastPackage | false | undefined + guest: DetailsSchema | SignedInDetailsSchema +} + +export interface DetailsState { + actions: { + completeStep: () => void + navigate: (step: StepEnum) => void + setIsSubmittingDisabled: (isSubmittingDisabled: boolean) => void + setStep: (step: StepEnum) => void + setTotalPrice: (totalPrice: Price) => void + toggleSummaryOpen: () => void + updateBedType: (data: BedTypeSchema) => void + updateBreakfast: (data: BreakfastPackage | false) => void + updateDetails: (data: DetailsSchema) => void + } + bedType: BedTypeSchema | undefined + booking: BookingData + breakfast: BreakfastPackage | false | undefined + currentStep: StepEnum + formValues: FormValues + guest: DetailsSchema + isSubmittingDisabled: boolean + isSummaryOpen: boolean + isValid: Record + packages: Packages | null + roomRate: DetailsProviderProps["roomRate"] + roomPrice: Price + steps: StepEnum[] + totalPrice: Price +} + +export type InitialState = Pick & + Pick & { + bedType?: BedTypeSchema + breakfast?: false + } + +export type PersistedState = Pick< + DetailsState, + "bedType" | "booking" | "breakfast" | "guest" | "totalPrice" +> + +export type RoomRate = DetailsProviderProps["roomRate"] diff --git a/types/stores/steps.ts b/types/stores/steps.ts deleted file mode 100644 index bfdafdae7..000000000 --- a/types/stores/steps.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { StepEnum } from "@/types/enums/step" - -export interface StepState { - completeStep: () => void - navigate: (step: StepEnum) => void - setStep: (step: StepEnum) => void - - currentStep: StepEnum - steps: StepEnum[] -} diff --git a/types/trpc/routers/hotel/availability.ts b/types/trpc/routers/hotel/availability.ts new file mode 100644 index 000000000..760766837 --- /dev/null +++ b/types/trpc/routers/hotel/availability.ts @@ -0,0 +1,5 @@ +import type { RouterOutput } from "@/lib/trpc/client" + +export type RoomAvailability = NonNullable< + RouterOutput["hotel"]["availability"]["room"] +>