Merged in feat/sw-610-summary (pull request #774)

Feat/SW-610 summary

Approved-by: Tobias Johansson
Approved-by: Simon.Emanuelsson
This commit is contained in:
Christel Westerberg
2024-10-30 18:52:48 +00:00
committed by Simon.Emanuelsson
49 changed files with 858 additions and 320 deletions

View File

@@ -14,7 +14,10 @@ export default async function HotelHeader({
if (!searchParams.hotel) { if (!searchParams.hotel) {
redirect(home) redirect(home)
} }
const hotel = await getHotelData(searchParams.hotel, params.lang) const hotel = await getHotelData({
hotelId: searchParams.hotel,
language: params.lang,
})
if (!hotel?.data) { if (!hotel?.data) {
redirect(home) redirect(home)
} }

View File

@@ -13,7 +13,10 @@ export default async function HotelSidePeek({
if (!searchParams.hotel) { if (!searchParams.hotel) {
redirect(`/${params.lang}`) redirect(`/${params.lang}`)
} }
const hotel = await getHotelData(searchParams.hotel, params.lang) const hotel = await getHotelData({
hotelId: searchParams.hotel,
language: params.lang,
})
if (!hotel?.data) { if (!hotel?.data) {
redirect(`/${params.lang}`) redirect(`/${params.lang}`)
} }

View File

@@ -0,0 +1,78 @@
import { notFound } from "next/navigation"
import {
getProfileSafely,
getSelectedRoomAvailability,
} from "@/lib/trpc/memoizedRequests"
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import { LangParams, PageArgs, SearchParams } from "@/types/params"
export default async function SummaryPage({
params,
searchParams,
}: PageArgs<LangParams, SearchParams<SelectRateSearchParams>>) {
const selectRoomParams = new URLSearchParams(searchParams)
const { hotel, adults, children, roomTypeCode, rateCode, fromDate, toDate } =
getQueryParamsForEnterDetails(selectRoomParams)
if (!roomTypeCode || !rateCode) {
console.log("No roomTypeCode or rateCode")
return notFound()
}
const availability = await getSelectedRoomAvailability({
hotelId: parseInt(hotel),
adults,
children,
roomStayStartDate: fromDate,
roomStayEndDate: toDate,
rateCode,
roomTypeCode,
})
const user = await getProfileSafely()
if (!availability) {
console.error("No hotel or availability data", availability)
// TODO: handle this case
return null
}
const prices = user
? {
local: {
price: availability.memberRate?.localPrice.pricePerStay,
currency: availability.memberRate?.localPrice.currency,
},
euro: {
price: availability.memberRate?.requestedPrice?.pricePerStay,
currency: availability.memberRate?.requestedPrice?.currency,
},
}
: {
local: {
price: availability.publicRate?.localPrice.pricePerStay,
currency: availability.publicRate?.localPrice.currency,
},
euro: {
price: availability.publicRate?.requestedPrice?.pricePerStay,
currency: availability.publicRate?.requestedPrice?.currency,
},
}
return (
<Summary
isMember={!!user}
room={{
roomType: availability.selectedRoom.roomType,
localPrice: prices.local,
euroPrice: prices.euro,
adults,
cancellationText: availability.cancellationText,
}}
/>
)
}

View File

@@ -1,5 +1,4 @@
.layout { .layout {
min-height: 100dvh;
background-color: var(--Scandic-Brand-Warm-White); background-color: var(--Scandic-Brand-Warm-White);
} }
@@ -9,7 +8,6 @@
grid-template-columns: 1fr 340px; grid-template-columns: 1fr 340px;
grid-template-rows: auto 1fr; grid-template-rows: auto 1fr;
margin: var(--Spacing-x5) auto 0; margin: var(--Spacing-x5) auto 0;
padding-top: var(--Spacing-x6);
/* simulates padding on viewport smaller than --max-width-navigation */ /* simulates padding on viewport smaller than --max-width-navigation */
width: min( width: min(
calc(100dvw - (var(--Spacing-x2) * 2)), calc(100dvw - (var(--Spacing-x2) * 2)),
@@ -17,8 +15,81 @@
); );
} }
.summary { .summaryContainer {
align-self: flex-start;
grid-column: 2 / 3; grid-column: 2 / 3;
grid-row: 1/-1; grid-row: 1/-1;
} }
.summary {
background-color: var(--Main-Grey-White);
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
border-style: solid;
border-width: 1px;
border-radius: var(--Corner-radius-Large);
z-index: 1;
}
.hider {
display: none;
}
.shadow {
display: none;
}
@media screen and (min-width: 950px) {
.summaryContainer {
display: grid;
grid-template-rows: auto auto 1fr;
margin-top: calc(0px - var(--Spacing-x9));
}
.summary {
position: sticky;
top: calc(
var(--booking-widget-desktop-height) +
var(--booking-widget-desktop-height) + var(--Spacing-x-one-and-half)
);
margin-top: calc(0px - var(--Spacing-x9));
border-bottom: none;
border-radius: var(--Corner-radius-Large) var(--Corner-radius-Large) 0 0;
}
.hider {
display: block;
background-color: var(--Scandic-Brand-Warm-White);
position: sticky;
margin-top: var(--Spacing-x4);
top: calc(
var(--booking-widget-desktop-height) +
var(--booking-widget-desktop-height) - 6px
);
height: 40px;
}
.shadow {
display: block;
background-color: var(--Main-Grey-White);
border-color: var(--Primary-Light-On-Surface-Divider-subtle);
border-style: solid;
border-left-width: 1px;
border-right-width: 1px;
border-top: none;
border-bottom: none;
}
}
@media screen and (min-width: 1367px) {
.summary {
top: calc(
var(--booking-widget-desktop-height) + var(--Spacing-x2) +
var(--Spacing-x-half)
);
}
.hider {
top: calc(var(--booking-widget-desktop-height) - 6px);
}
}

View File

@@ -1,16 +1,16 @@
import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider" import EnterDetailsProvider from "@/components/HotelReservation/EnterDetails/Provider"
import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom" import SelectedRoom from "@/components/HotelReservation/EnterDetails/SelectedRoom"
import Summary from "@/components/HotelReservation/EnterDetails/Summary"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
import { preload } from "./page" import { preload } from "./page"
import styles from "./layout.module.css" import styles from "./layout.module.css"
import { StepEnum } from "@/types/components/enterDetails/step" import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import type { LangParams, LayoutArgs } from "@/types/params" import type { LangParams, LayoutArgs } from "@/types/params"
export default async function StepLayout({ export default async function StepLayout({
summary,
children, children,
hotelHeader, hotelHeader,
params, params,
@@ -19,19 +19,21 @@ export default async function StepLayout({
LayoutArgs<LangParams & { step: StepEnum }> & { LayoutArgs<LangParams & { step: StepEnum }> & {
hotelHeader: React.ReactNode hotelHeader: React.ReactNode
sidePeek: React.ReactNode sidePeek: React.ReactNode
} summary: React.ReactNode
>) { }>) {
setLang(params.lang) setLang(params.lang)
preload() preload()
return ( return (
<EnterDetailsProvider step={params.step}> <EnterDetailsProvider step={params.step} >
<main className={styles.layout}> <main className={styles.layout}>
{hotelHeader} {hotelHeader}
<div className={styles.content}> <div className={styles.content}>
<SelectedRoom /> <SelectedRoom />
{children} {children}
<aside className={styles.summary}> <aside className={styles.summaryContainer}>
<Summary /> <div className={styles.hider} />
<div className={styles.summary}>{summary}</div>
<div className={styles.shadow} />
</aside> </aside>
</div> </div>
{sidePeek} {sidePeek}

View File

@@ -6,7 +6,9 @@ import {
getHotelData, getHotelData,
getProfileSafely, getProfileSafely,
getRoomAvailability, getRoomAvailability,
getSelectedRoomAvailability,
} from "@/lib/trpc/memoizedRequests" } from "@/lib/trpc/memoizedRequests"
import { HotelIncludeEnum } from "@/server/routers/hotels/input"
import BedType from "@/components/HotelReservation/EnterDetails/BedType" import BedType from "@/components/HotelReservation/EnterDetails/BedType"
import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast" import Breakfast from "@/components/HotelReservation/EnterDetails/Breakfast"
@@ -14,10 +16,11 @@ import Details from "@/components/HotelReservation/EnterDetails/Details"
import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager" import HistoryStateManager from "@/components/HotelReservation/EnterDetails/HistoryStateManager"
import Payment from "@/components/HotelReservation/EnterDetails/Payment" import Payment from "@/components/HotelReservation/EnterDetails/Payment"
import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion" import SectionAccordion from "@/components/HotelReservation/EnterDetails/SectionAccordion"
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { StepEnum } from "@/types/components/enterDetails/step" import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { LangParams, PageArgs } from "@/types/params" import type { LangParams, PageArgs } from "@/types/params"
export function preload() { export function preload() {
@@ -32,35 +35,56 @@ function isValidStep(step: string): step is StepEnum {
export default async function StepPage({ export default async function StepPage({
params, params,
searchParams, searchParams,
}: PageArgs<LangParams & { step: StepEnum }, { hotel: string }>) { }: PageArgs<LangParams & { step: StepEnum }, SelectRateSearchParams>) {
if (!searchParams.hotel) { const { lang } = params
redirect(`/${params.lang}`)
}
void getBreakfastPackages(searchParams.hotel)
const stepParams = new URLSearchParams(searchParams)
const paramsObject = getHotelReservationQueryParams(stepParams)
void getRoomAvailability({
hotelId: paramsObject.hotel,
adults: paramsObject.room[0].adults,
roomStayStartDate: paramsObject.fromDate,
roomStayEndDate: paramsObject.toDate,
})
const intl = await getIntl()
const hotel = await getHotelData(searchParams.hotel, params.lang) void getBreakfastPackages(searchParams.hotel)
const intl = await getIntl()
const selectRoomParams = new URLSearchParams(searchParams)
const {
hotel: hotelId,
adults,
children,
roomTypeCode,
rateCode,
fromDate,
toDate,
} = getQueryParamsForEnterDetails(selectRoomParams)
if (!rateCode || !roomTypeCode) {
return notFound()
}
void getSelectedRoomAvailability({
hotelId: parseInt(searchParams.hotel),
adults,
children,
roomStayStartDate: fromDate,
roomStayEndDate: toDate,
rateCode,
roomTypeCode,
})
const hotelData = await getHotelData({
hotelId,
language: lang,
include: [HotelIncludeEnum.RoomCategories],
})
const roomAvailability = await getSelectedRoomAvailability({
hotelId: parseInt(searchParams.hotel),
adults,
children,
roomStayStartDate: fromDate,
roomStayEndDate: toDate,
rateCode,
roomTypeCode,
})
const breakfastPackages = await getBreakfastPackages(searchParams.hotel)
const user = await getProfileSafely() const user = await getProfileSafely()
const savedCreditCards = await getCreditCardsSafely() const savedCreditCards = await getCreditCardsSafely()
const breakfastPackages = await getBreakfastPackages(searchParams.hotel)
const roomAvailability = await getRoomAvailability({ if (!isValidStep(params.step) || !hotelData || !roomAvailability) {
hotelId: paramsObject.hotel,
adults: paramsObject.room[0].adults,
roomStayStartDate: paramsObject.fromDate,
roomStayEndDate: paramsObject.toDate,
rateCode: paramsObject.room[0].ratecode,
})
if (!isValidStep(params.step) || !hotel || !roomAvailability) {
return notFound() return notFound()
} }
@@ -79,16 +103,30 @@ export default async function StepPage({
id: "Select payment method", id: "Select payment method",
}) })
const availableRoom = roomAvailability.selectedRoom?.roomType
const bedTypes = hotelData.included
?.find((room) => room.name === availableRoom)
?.roomTypes.map((room) => ({
description: room.mainBed.description,
size: room.mainBed.widthRange,
value: room.code,
}))
return ( return (
<section> <section>
<HistoryStateManager /> <HistoryStateManager />
{/* TODO: How to handle no beds found? */}
{bedTypes ? (
<SectionAccordion <SectionAccordion
header={intl.formatMessage({ id: "Select bed" })} header="Select bed"
step={StepEnum.selectBed} step={StepEnum.selectBed}
label={intl.formatMessage({ id: "Request bedtype" })} label={intl.formatMessage({ id: "Request bedtype" })}
> >
<BedType /> <BedType bedTypes={bedTypes} />
</SectionAccordion> </SectionAccordion>
) : null}
<SectionAccordion <SectionAccordion
header={intl.formatMessage({ id: "Food options" })} header={intl.formatMessage({ id: "Food options" })}
step={StepEnum.breakfast} step={StepEnum.breakfast}
@@ -111,7 +149,7 @@ export default async function StepPage({
<Payment <Payment
hotelId={searchParams.hotel} hotelId={searchParams.hotel}
otherPaymentOptions={ otherPaymentOptions={
hotel.data.attributes.merchantInformationData hotelData.data.attributes.merchantInformationData
.alternatePaymentOptions .alternatePaymentOptions
} }
savedCreditCards={savedCreditCards} savedCreditCards={savedCreditCards}

View File

@@ -1,4 +1,3 @@
.layout { .layout {
min-height: 100dvh;
background-color: var(--Base-Background-Primary-Normal); background-color: var(--Base-Background-Primary-Normal);
} }

View File

@@ -10,7 +10,7 @@ import {
} from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils" } from "@/app/[lang]/(live)/(public)/hotelreservation/(standard)/select-hotel/utils"
import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import HotelCardListing from "@/components/HotelReservation/HotelCardListing"
import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter"
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { ChevronRightIcon } from "@/components/Icons" import { ChevronRightIcon } from "@/components/Icons"
import StaticMap from "@/components/Maps/StaticMap" import StaticMap from "@/components/Maps/StaticMap"
import Link from "@/components/TempDesignSystem/Link" import Link from "@/components/TempDesignSystem/Link"

View File

@@ -2,10 +2,11 @@ import { notFound } from "next/navigation"
import { getProfileSafely } from "@/lib/trpc/memoizedRequests" import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server" import { serverClient } from "@/lib/trpc/server"
import { HotelIncludeEnum } from "@/server/routers/hotels/input"
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard" import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
import Rooms from "@/components/HotelReservation/SelectRate/Rooms" import Rooms from "@/components/HotelReservation/SelectRate/Rooms"
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import { getHotelReservationQueryParams } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { setLang } from "@/i18n/serverContext" import { setLang } from "@/i18n/serverContext"
import { generateChildrenString } from "../select-hotel/utils" import { generateChildrenString } from "../select-hotel/utils"
@@ -38,7 +39,7 @@ export default async function SelectRatePage({
serverClient().hotel.hotelData.get({ serverClient().hotel.hotelData.get({
hotelId: searchParams.hotel, hotelId: searchParams.hotel,
language: params.lang, language: params.lang,
include: ["RoomCategories"], include: [HotelIncludeEnum.RoomCategories],
}), }),
serverClient().hotel.availability.rooms({ serverClient().hotel.availability.rooms({
hotelId: parseInt(searchParams.hotel, 10), hotelId: parseInt(searchParams.hotel, 10),

View File

@@ -4,6 +4,7 @@ import { dt } from "@/lib/dt"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import { getIntl } from "@/i18n" import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import { formatNumber } from "@/utils/format"
import { getMembership } from "@/utils/user" import { getMembership } from "@/utils/user"
import type { UserProps } from "@/types/components/myPages/user" import type { UserProps } from "@/types/components/myPages/user"
@@ -16,9 +17,6 @@ export default async function ExpiringPoints({ user }: UserProps) {
// TODO: handle this case? // TODO: handle this case?
return null return null
} }
// sv hardcoded to force space on thousands
const formatter = new Intl.NumberFormat(Lang.sv)
const d = dt(membership.pointsExpiryDate) const d = dt(membership.pointsExpiryDate)
const dateFormat = getLang() == Lang.fi ? "DD.MM.YYYY" : "YYYY-MM-DD" const dateFormat = getLang() == Lang.fi ? "DD.MM.YYYY" : "YYYY-MM-DD"
@@ -29,7 +27,7 @@ export default async function ExpiringPoints({ user }: UserProps) {
{intl.formatMessage( {intl.formatMessage(
{ id: "spendable points expiring by" }, { id: "spendable points expiring by" },
{ {
points: formatter.format(membership.pointsToExpire), points: formatNumber(membership.pointsToExpire),
date: d.format(dateFormat), date: d.format(dateFormat),
} }
)} )}

View File

@@ -1,8 +1,7 @@
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { Lang } from "@/constants/languages"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import { formatNumber } from "@/utils/format"
import { awardPointsVariants } from "./awardPointsVariants" import { awardPointsVariants } from "./awardPointsVariants"
@@ -32,12 +31,10 @@ export default function AwardPoints({
variant, variant,
}) })
// sv hardcoded to force space on thousands
const formatter = new Intl.NumberFormat(Lang.sv)
return ( return (
<Body textTransform="bold" className={classNames}> <Body textTransform="bold" className={classNames}>
{isCalculated {isCalculated
? formatter.format(awardPoints) ? formatNumber(awardPoints)
: intl.formatMessage({ id: "Points being calculated" })} : intl.formatMessage({ id: "Points being calculated" })}
</Body> </Body>
) )

View File

@@ -11,7 +11,6 @@ import { CloseLargeIcon } from "@/components/Icons"
import { debounce } from "@/utils/debounce" import { debounce } from "@/utils/debounce"
import { getFormattedUrlQueryParams } from "@/utils/url" import { getFormattedUrlQueryParams } from "@/utils/url"
import getHotelReservationQueryParams from "../HotelReservation/SelectRate/RoomSelection/utils"
import MobileToggleButton from "./MobileToggleButton" import MobileToggleButton from "./MobileToggleButton"
import styles from "./bookingWidget.module.css" import styles from "./bookingWidget.module.css"

View File

@@ -18,7 +18,7 @@ export default function FooterSecondaryNav({
<div className={styles.secondaryNavigation}> <div className={styles.secondaryNavigation}>
{appDownloads && ( {appDownloads && (
<nav className={styles.secondaryNavigationGroup}> <nav className={styles.secondaryNavigationGroup}>
<Body color="peach80" textTransform="uppercase"> <Body color="baseTextMediumContrast" textTransform="uppercase">
{appDownloads.title} {appDownloads.title}
</Body> </Body>
<ul className={styles.secondaryNavigationList}> <ul className={styles.secondaryNavigationList}>
@@ -50,7 +50,7 @@ export default function FooterSecondaryNav({
)} )}
{secondaryLinks.map((link) => ( {secondaryLinks.map((link) => (
<nav className={styles.secondaryNavigationGroup} key={link.title}> <nav className={styles.secondaryNavigationGroup} key={link.title}>
<Body color="peach80" textTransform="uppercase"> <Body color="baseTextMediumContrast" textTransform="uppercase">
{link.title} {link.title}
</Body> </Body>
<ul className={styles.secondaryNavigationList}> <ul className={styles.secondaryNavigationList}>

View File

@@ -14,12 +14,14 @@ import { bedTypeSchema } from "./schema"
import styles from "./bedOptions.module.css" import styles from "./bedOptions.module.css"
import type { BedTypeSchema } from "@/types/components/enterDetails/bedType" import type {
import { BedTypeEnum } from "@/types/enums/bedType" BedTypeProps,
BedTypeSchema,
} from "@/types/components/hotelReservation/enterDetails/bedType"
export default function BedType() { export default function BedType({ bedTypes }: BedTypeProps) {
const intl = useIntl() const intl = useIntl()
const bedType = useEnterDetailsStore((state) => state.data.bedType) const bedType = useEnterDetailsStore((state) => state.userData.bedType)
const methods = useForm<BedTypeSchema>({ const methods = useForm<BedTypeSchema>({
defaultValues: bedType defaultValues: bedType
@@ -33,10 +35,6 @@ export default function BedType() {
reValidateMode: "onChange", reValidateMode: "onChange",
}) })
const text = intl.formatMessage<React.ReactNode>(
{ id: "<b>Included</b> (based on availability)" },
{ b: (str) => <b>{str}</b> }
)
const completeStep = useEnterDetailsStore((state) => state.completeStep) const completeStep = useEnterDetailsStore((state) => state.completeStep)
const onSubmit = useCallback( const onSubmit = useCallback(
@@ -58,38 +56,24 @@ export default function BedType() {
return ( return (
<FormProvider {...methods}> <FormProvider {...methods}>
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}> <form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
{bedTypes.map((roomType) => {
const width =
roomType.size.max === roomType.size.min
? `${roomType.size.min} cm`
: `${roomType.size.min} cm - ${roomType.size.max} cm`
return (
<RadioCard <RadioCard
key={roomType.value}
Icon={KingBedIcon} Icon={KingBedIcon}
iconWidth={46} iconWidth={46}
id={BedTypeEnum.KING} id={roomType.value}
name="bedType" name="bedType"
subtitle={intl.formatMessage( subtitle={width}
{ id: "{width} cm × {length} cm" }, title={roomType.description}
{ value={roomType.description}
length: "210",
width: "180",
}
)}
text={text}
title={intl.formatMessage({ id: "King bed" })}
value={BedTypeEnum.KING}
/>
<RadioCard
Icon={KingBedIcon}
iconWidth={46}
id={BedTypeEnum.QUEEN}
name="bedType"
subtitle={intl.formatMessage(
{ id: "{width} cm × {length} cm" },
{
length: "200",
width: "160",
}
)}
text={text}
title={intl.formatMessage({ id: "Queen bed" })}
value={BedTypeEnum.QUEEN}
/> />
)
})}
</form> </form>
</FormProvider> </FormProvider>
) )

View File

@@ -3,5 +3,5 @@ import { z } from "zod"
import { BedTypeEnum } from "@/types/enums/bedType" import { BedTypeEnum } from "@/types/enums/bedType"
export const bedTypeSchema = z.object({ export const bedTypeSchema = z.object({
bedType: z.nativeEnum(BedTypeEnum), bedType: z.string(),
}) })

View File

@@ -17,13 +17,13 @@ import styles from "./breakfast.module.css"
import type { import type {
BreakfastFormSchema, BreakfastFormSchema,
BreakfastProps, BreakfastProps,
} from "@/types/components/enterDetails/breakfast" } from "@/types/components/hotelReservation/enterDetails/breakfast"
import { BreakfastPackageEnum } from "@/types/enums/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast"
export default function Breakfast({ packages }: BreakfastProps) { export default function Breakfast({ packages }: BreakfastProps) {
const intl = useIntl() const intl = useIntl()
const breakfast = useEnterDetailsStore((state) => state.data.breakfast) const breakfast = useEnterDetailsStore((state) => state.userData.breakfast)
let defaultValues = undefined let defaultValues = undefined
if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) { if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) {

View File

@@ -22,21 +22,21 @@ import styles from "./details.module.css"
import type { import type {
DetailsProps, DetailsProps,
DetailsSchema, DetailsSchema,
} from "@/types/components/enterDetails/details" } from "@/types/components/hotelReservation/enterDetails/details"
const formID = "enter-details" const formID = "enter-details"
export default function Details({ user }: DetailsProps) { export default function Details({ user }: DetailsProps) {
const intl = useIntl() const intl = useIntl()
const initialData = useEnterDetailsStore((state) => ({ const initialData = useEnterDetailsStore((state) => ({
countryCode: state.data.countryCode, countryCode: state.userData.countryCode,
email: state.data.email, email: state.userData.email,
firstName: state.data.firstName, firstName: state.userData.firstName,
lastName: state.data.lastName, lastName: state.userData.lastName,
phoneNumber: state.data.phoneNumber, phoneNumber: state.userData.phoneNumber,
join: state.data.join, join: state.userData.join,
dateOfBirth: state.data.dateOfBirth, dateOfBirth: state.userData.dateOfBirth,
zipCode: state.data.zipCode, zipCode: state.userData.zipCode,
termsAccepted: state.data.termsAccepted, termsAccepted: state.userData.termsAccepted,
})) }))
const methods = useForm<DetailsSchema>({ const methods = useForm<DetailsSchema>({

View File

@@ -56,7 +56,7 @@ export default function Payment({
const intl = useIntl() const intl = useIntl()
const queryParams = useSearchParams() const queryParams = useSearchParams()
const { firstName, lastName, email, phoneNumber, countryCode } = const { firstName, lastName, email, phoneNumber, countryCode } =
useEnterDetailsStore((state) => state.data) useEnterDetailsStore((state) => state.userData)
const [confirmationNumber, setConfirmationNumber] = useState<string>("") const [confirmationNumber, setConfirmationNumber] = useState<string>("")
const methods = useForm<PaymentFormData>({ const methods = useForm<PaymentFormData>({

View File

@@ -1,4 +1,5 @@
"use client" "use client"
import { useSearchParams } from "next/navigation"
import { PropsWithChildren, useRef } from "react" import { PropsWithChildren, useRef } from "react"
import { import {
@@ -7,15 +8,16 @@ import {
initEditDetailsState, initEditDetailsState,
} from "@/stores/enter-details" } from "@/stores/enter-details"
import { StepEnum } from "@/types/components/enterDetails/step" import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
export default function EnterDetailsProvider({ export default function EnterDetailsProvider({
step, step,
children, children,
}: PropsWithChildren<{ step: StepEnum }>) { }: PropsWithChildren<{ step: StepEnum }>) {
const searchParams = useSearchParams()
const initialStore = useRef<EnterDetailsStore>() const initialStore = useRef<EnterDetailsStore>()
if (!initialStore.current) { if (!initialStore.current) {
initialStore.current = initEditDetailsState(step) initialStore.current = initEditDetailsState(step, searchParams)
} }
return ( return (

View File

@@ -14,7 +14,7 @@ import styles from "./enterDetailsSidePeek.module.css"
import { import {
SidePeekEnum, SidePeekEnum,
SidePeekProps, SidePeekProps,
} from "@/types/components/enterDetails/sidePeek" } from "@/types/components/hotelReservation/enterDetails/sidePeek"
export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) { export default function EnterDetailsSidePeek({ hotel }: SidePeekProps) {
const activeSidePeek = useEnterDetailsStore((state) => state.activeSidePeek) const activeSidePeek = useEnterDetailsStore((state) => state.activeSidePeek)

View File

@@ -7,7 +7,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
import { ChevronRightSmallIcon } from "@/components/Icons" import { ChevronRightSmallIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek" import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek"
export default function ToggleSidePeek() { export default function ToggleSidePeek() {
const intl = useIntl() const intl = useIntl()

View File

@@ -1,153 +1,201 @@
"use client"
import { useEffect, useState } from "react"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { ArrowRightIcon } from "@/components/Icons" import { ArrowRightIcon } from "@/components/Icons"
import Divider from "@/components/TempDesignSystem/Divider" import Divider from "@/components/TempDesignSystem/Divider"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import { getIntl } from "@/i18n" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getLang } from "@/i18n/serverContext" import useLang from "@/hooks/useLang"
import { formatNumber } from "@/utils/format"
import ToggleSidePeek from "./ToggleSidePeek"
import styles from "./summary.module.css" import styles from "./summary.module.css"
// TEMP import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData"
const rooms = [ import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
{ import { BreakfastPackageEnum } from "@/types/enums/breakfast"
adults: 1,
type: "Cozy cabin", export default function Summary({
}, isMember,
] room,
}: {
isMember: boolean
room: RoomsData
}) {
const [chosenBed, setChosenBed] = useState<string>()
const [chosenBreakfast, setChosenBreakfast] = useState<
BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST
>()
const intl = useIntl()
const lang = useLang()
const { fromDate, toDate, bedType, breakfast } = useEnterDetailsStore(
(state) => ({
fromDate: state.roomData.fromDate,
toDate: state.roomData.toDate,
bedType: state.userData.bedType,
breakfast: state.userData.breakfast,
})
)
export default async function Summary() {
const intl = await getIntl()
const lang = getLang()
const fromDate = dt().locale(lang).format("ddd, D MMM")
const toDate = dt().add(1, "day").locale(lang).format("ddd, D MMM")
const diff = dt(toDate).diff(fromDate, "days") const diff = dt(toDate).diff(fromDate, "days")
const totalAdults = rooms.reduce((total, room) => total + room.adults, 0)
const adults = intl.formatMessage(
{ id: "booking.adults" },
{ totalAdults: totalAdults }
)
const nights = intl.formatMessage( const nights = intl.formatMessage(
{ id: "booking.nights" }, { id: "booking.nights" },
{ totalNights: diff } { totalNights: diff }
) )
const addOns = [ let color: "uiTextHighContrast" | "red" = "uiTextHighContrast"
{ if (isMember) {
price: intl.formatMessage({ id: "Included" }), color = "red"
title: intl.formatMessage({ id: "King bed" }), }
},
{
price: intl.formatMessage({ id: "Included" }),
title: intl.formatMessage({ id: "Breakfast buffet" }),
},
]
const mappedRooms = Array.from( useEffect(() => {
rooms setChosenBed(bedType)
.reduce((acc, room) => {
const currentRoom = acc.get(room.type) if (breakfast) {
acc.set(room.type, { setChosenBreakfast(breakfast)
total: currentRoom ? currentRoom.total + 1 : 1, }
type: room.type, }, [bedType, breakfast])
})
return acc
}, new Map())
.values()
)
return ( return (
<section className={styles.summary}> <section className={styles.summary}>
<header> <header>
<Body textTransform="bold"> <Subtitle type="two">{intl.formatMessage({ id: "Summary" })}</Subtitle>
{mappedRooms.map( <Body className={styles.date} color="baseTextMediumContrast">
(room, idx) => {dt(fromDate).locale(lang).format("ddd, D MMM")}
`${room.total} x ${room.type}${mappedRooms.length > 1 && idx + 1 !== mappedRooms.length ? ", " : ""}` <ArrowRightIcon color="peach80" height={15} width={15} />
)} {dt(toDate).locale(lang).format("ddd, D MMM")} ({nights})
</Body> </Body>
<Body className={styles.date} color="textMediumContrast">
{fromDate}
<ArrowRightIcon color="uiTextMediumContrast" height={15} width={15} />
{toDate}
</Body>
<ToggleSidePeek />
</header> </header>
<Divider color="primaryLightSubtle" /> <Divider color="primaryLightSubtle" />
<div className={styles.addOns}> <div className={styles.addOns}>
<div>
<div className={styles.entry}> <div className={styles.entry}>
<Caption color="uiTextMediumContrast"> <Body color="textHighContrast">{room.roomType}</Body>
{`${nights}, ${adults}`} <Caption color={color}>
</Caption>
<Caption color="uiTextHighContrast">
{intl.formatMessage( {intl.formatMessage(
{ id: "{amount} {currency}" }, { id: "{amount} {currency}" },
{ amount: "4536", currency: "SEK" } {
amount: formatNumber(parseInt(room.localPrice.price ?? "0")),
currency: room.localPrice.currency,
}
)} )}
</Caption> </Caption>
</div> </div>
{addOns.map((addOn) => ( <Caption color="uiTextMediumContrast">
<div className={styles.entry} key={addOn.title}> {intl.formatMessage(
<Caption color="uiTextMediumContrast">{addOn.title}</Caption> { id: "booking.adults" },
<Caption color="uiTextHighContrast">{addOn.price}</Caption> { totalAdults: room.adults }
)}
</Caption>
{room.children?.length ? (
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "booking.children" },
{ totalChildren: room.children.length }
)}
</Caption>
) : null}
<Caption color="uiTextMediumContrast">
{room.cancellationText}
</Caption>
<Link color="burgundy" href="#" variant="underscored" size="small">
{intl.formatMessage({ id: "Rate details" })}
</Link>
</div> </div>
))}
{chosenBed ? (
<div className={styles.entry}>
<div>
<Body color="textHighContrast">{chosenBed}</Body>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Based on availability" })}
</Caption>
</div>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "0", currency: room.localPrice.currency }
)}
</Caption>
</div>
) : null}
{chosenBreakfast ? (
chosenBreakfast === BreakfastPackageEnum.NO_BREAKFAST ? (
<div className={styles.entry}>
<Body color="textHighContrast">
{intl.formatMessage({ id: "No breakfast" })}
</Body>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "0", currency: room.localPrice.currency }
)}
</Caption>
</div>
) : (
<div className={styles.entry}>
<Body color="textHighContrast">
{intl.formatMessage({ id: "Breakfast buffet" })}
</Body>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{
amount: chosenBreakfast.totalPrice,
currency: chosenBreakfast.currency,
}
)}
</Caption>
</div>
)
) : null}
</div> </div>
<Divider color="primaryLightSubtle" /> <Divider color="primaryLightSubtle" />
<div className={styles.total}> <div className={styles.total}>
<div>
<div className={styles.entry}> <div className={styles.entry}>
<Body textTransform="bold"> <div>
{intl.formatMessage({ id: "Total incl VAT" })} <Body>
{intl.formatMessage<React.ReactNode>(
{ id: "<b>Total price</b> (incl VAT)" },
{ b: (str) => <b>{str}</b> }
)}
</Body> </Body>
<Link color="burgundy" href="#" variant="underscored" size="small">
{intl.formatMessage({ id: "Price details" })}
</Link>
</div>
<div>
<Body textTransform="bold"> <Body textTransform="bold">
{intl.formatMessage( {intl.formatMessage(
{ id: "{amount} {currency}" }, { id: "{amount} {currency}" },
{ amount: "4686", currency: "SEK" } {
amount: formatNumber(parseInt(room.localPrice.price ?? "0")),
currency: room.localPrice.currency,
}
)} )}
</Body> </Body>
</div>
<div className={styles.entry}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Approx." })}
</Caption>
<Caption color="uiTextMediumContrast"> <Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Approx." })}{" "}
{intl.formatMessage( {intl.formatMessage(
{ id: "{amount} {currency}" }, { id: "{amount} {currency}" },
{ amount: "455", currency: "EUR" } {
)} amount: formatNumber(parseInt(room.euroPrice.price ?? "0")),
</Caption> currency: room.euroPrice.currency,
</div> }
</div>
<div>
<div className={styles.entry}>
<Body color="red" textTransform="bold">
{intl.formatMessage({ id: "Member price" })}
</Body>
<Body color="red" textTransform="bold">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "4219", currency: "SEK" }
)}
</Body>
</div>
<div className={styles.entry}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Approx." })}
</Caption>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "412", currency: "EUR" }
)} )}
</Caption> </Caption>
</div> </div>
</div> </div>
<Divider color="primaryLightSubtle" />
</div> </div>
</section> </section>
) )

View File

@@ -1,11 +1,10 @@
.summary { .summary {
background-color: var(--Main-Grey-White);
border: 1px solid var(--Primary-Light-On-Surface-Divider-subtle);
border-radius: var(--Corner-radius-Large); border-radius: var(--Corner-radius-Large);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--Spacing-x2); gap: var(--Spacing-x2);
padding: var(--Spacing-x2); padding: var(--Spacing-x3);
height: 100%;
} }
.date { .date {
@@ -22,7 +21,7 @@
.addOns { .addOns {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--Spacing-x1); gap: var(--Spacing-x-one-and-half);
} }
.entry { .entry {
@@ -31,6 +30,9 @@
justify-content: space-between; justify-content: space-between;
} }
.entry > :last-child {
justify-items: flex-end;
}
.total { .total {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -4,7 +4,7 @@ import { useMemo, useState } from "react"
import RateSummary from "./RateSummary" import RateSummary from "./RateSummary"
import RoomCard from "./RoomCard" import RoomCard from "./RoomCard"
import getHotelReservationQueryParams from "./utils" import { getHotelReservationQueryParams } from "./utils"
import styles from "./roomSelection.module.css" import styles from "./roomSelection.module.css"

View File

@@ -2,11 +2,22 @@ import { getFormattedUrlQueryParams } from "@/utils/url"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate" import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
function getHotelReservationQueryParams(searchParams: URLSearchParams) { export function getHotelReservationQueryParams(searchParams: URLSearchParams) {
return getFormattedUrlQueryParams(searchParams, { return getFormattedUrlQueryParams(searchParams, {
adults: "number", adults: "number",
age: "number", age: "number",
}) as SelectRateSearchParams }) as SelectRateSearchParams
} }
export default getHotelReservationQueryParams export function getQueryParamsForEnterDetails(searchParams: URLSearchParams) {
const selectRoomParamsObject = getHotelReservationQueryParams(searchParams)
const { room } = selectRoomParamsObject
return {
...selectRoomParamsObject,
adults: room[0].adults, // TODO: Handle multiple rooms
children: room[0].child?.length.toString(), // TODO: Handle multiple rooms and children
roomTypeCode: room[0].roomtype,
rateCode: room[0].ratecode,
}
}

View File

@@ -26,7 +26,12 @@ interface TextCardProps extends BaseCardProps {
text: React.ReactNode text: React.ReactNode
} }
export type CardProps = ListCardProps | TextCardProps interface CleanCardProps extends BaseCardProps {
list?: never
text?: never
}
export type CardProps = ListCardProps | TextCardProps | CleanCardProps
export type CheckboxProps = export type CheckboxProps =
| Omit<ListCardProps, "type"> | Omit<ListCardProps, "type">
@@ -34,6 +39,7 @@ export type CheckboxProps =
export type RadioProps = export type RadioProps =
| Omit<ListCardProps, "type"> | Omit<ListCardProps, "type">
| Omit<TextCardProps, "type"> | Omit<TextCardProps, "type">
| Omit<CleanCardProps, "type">
export interface ListProps extends Pick<ListCardProps, "declined"> { export interface ListProps extends Pick<ListCardProps, "declined"> {
list?: ListCardProps["list"] list?: ListCardProps["list"]

View File

@@ -92,7 +92,7 @@
color: var(--Primary-Dark-On-Surface-Accent); color: var(--Primary-Dark-On-Surface-Accent);
} }
.peach80 { .baseTextMediumContrast {
color: var(--Base-Text-Medium-contrast); color: var(--Base-Text-Medium-contrast);
} }

View File

@@ -16,7 +16,6 @@ const config = {
textHighContrast: styles.textHighContrast, textHighContrast: styles.textHighContrast,
white: styles.white, white: styles.white,
peach50: styles.peach50, peach50: styles.peach50,
peach80: styles.peach80,
uiTextHighContrast: styles.uiTextHighContrast, uiTextHighContrast: styles.uiTextHighContrast,
uiTextMediumContrast: styles.uiTextMediumContrast, uiTextMediumContrast: styles.uiTextMediumContrast,
uiTextPlaceholder: styles.uiTextPlaceholder, uiTextPlaceholder: styles.uiTextPlaceholder,

View File

@@ -1,5 +1,7 @@
{ {
"<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)", "<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)",
"<b>Total price</b> (incl VAT)": "<b>Samlet pris</b> (inkl. moms)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/nat pr. voksen",
"A destination or hotel name is needed to be able to search for a hotel room.": "Et destinations- eller hotelnavn er nødvendigt for at kunne søge efter et hotelværelse.", "A destination or hotel name is needed to be able to search for a hotel room.": "Et destinations- eller hotelnavn er nødvendigt for at kunne søge efter et hotelværelse.",
"A photo of the room": "Et foto af værelset", "A photo of the room": "Et foto af værelset",
"ACCE": "Tilgængelighed", "ACCE": "Tilgængelighed",
@@ -35,6 +37,7 @@
"Attractions": "Attraktioner", "Attractions": "Attraktioner",
"Back to scandichotels.com": "Tilbage til scandichotels.com", "Back to scandichotels.com": "Tilbage til scandichotels.com",
"Bar": "Bar", "Bar": "Bar",
"Based on availability": "Baseret på tilgængelighed",
"Bed type": "Seng type", "Bed type": "Seng type",
"Birth date": "Fødselsdato", "Birth date": "Fødselsdato",
"Book": "Book", "Book": "Book",
@@ -242,12 +245,14 @@
"Points needed to stay on level": "Point nødvendige for at holde sig på niveau", "Points needed to stay on level": "Point nødvendige for at holde sig på niveau",
"Previous": "Forudgående", "Previous": "Forudgående",
"Previous victories": "Tidligere sejre", "Previous victories": "Tidligere sejre",
"Price details": "Prisoplysninger",
"Proceed to login": "Fortsæt til login", "Proceed to login": "Fortsæt til login",
"Proceed to payment method": "Fortsæt til betalingsmetode", "Proceed to payment method": "Fortsæt til betalingsmetode",
"Provide a payment card in the next step": "Giv os dine betalingsoplysninger i næste skridt", "Provide a payment card in the next step": "Giv os dine betalingsoplysninger i næste skridt",
"Public price from": "Offentlig pris fra", "Public price from": "Offentlig pris fra",
"Public transport": "Offentlig transport", "Public transport": "Offentlig transport",
"Queen bed": "Queensize-seng", "Queen bed": "Queensize-seng",
"Rate details": "Oplysninger om værelsespris",
"Read more": "Læs mere", "Read more": "Læs mere",
"Read more & book a table": "Read more & book a table", "Read more & book a table": "Read more & book a table",
"Read more about the hotel": "Læs mere om hotellet", "Read more about the hotel": "Læs mere om hotellet",
@@ -313,8 +318,8 @@
"Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}", "Things nearby HOTEL_NAME": "Ting i nærheden af {hotelName}",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "For at sikre din reservation, beder vi om at du giver os dine betalingsoplysninger. Du kan så være sikker på, at ingen gebyrer vil blive opkrævet på dette tidspunkt.",
"Total Points": "Samlet antal point", "Total Points": "Samlet antal point",
"Total incl VAT": "Inkl. moms",
"Total price": "Samlet pris", "Total price": "Samlet pris",
"Total price (incl VAT)": "Samlet pris (inkl. moms)",
"Tourist": "Turist", "Tourist": "Turist",
"Transaction date": "Overførselsdato", "Transaction date": "Overførselsdato",
"Transactions": "Transaktioner", "Transactions": "Transaktioner",
@@ -405,6 +410,5 @@
"uppercase letter": "stort bogstav", "uppercase letter": "stort bogstav",
"{amount} out of {total}": "{amount} ud af {total}", "{amount} out of {total}": "{amount} ud af {total}",
"{amount} {currency}": "{amount} {currency}", "{amount} {currency}": "{amount} {currency}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}"
"{width} cm × {length} cm": "{width} cm × {length} cm"
} }

View File

@@ -1,5 +1,7 @@
{ {
"<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)", "<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)",
"<b>Total price</b> (incl VAT)": "<b>Gesamtpreis</b> (inkl. MwSt.)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/Nacht pro Erwachsener",
"A destination or hotel name is needed to be able to search for a hotel room.": "Ein Reiseziel oder Hotelname wird benötigt, um nach einem Hotelzimmer suchen zu können.", "A destination or hotel name is needed to be able to search for a hotel room.": "Ein Reiseziel oder Hotelname wird benötigt, um nach einem Hotelzimmer suchen zu können.",
"A photo of the room": "Ein Foto des Zimmers", "A photo of the room": "Ein Foto des Zimmers",
"ACCE": "Zugänglichkeit", "ACCE": "Zugänglichkeit",
@@ -35,6 +37,7 @@
"Attraction": "Attraktion", "Attraction": "Attraktion",
"Back to scandichotels.com": "Zurück zu scandichotels.com", "Back to scandichotels.com": "Zurück zu scandichotels.com",
"Bar": "Bar", "Bar": "Bar",
"Based on availability": "Je nach Verfügbarkeit",
"Bed type": "Bettentyp", "Bed type": "Bettentyp",
"Birth date": "Geburtsdatum", "Birth date": "Geburtsdatum",
"Book": "Buchen", "Book": "Buchen",
@@ -240,12 +243,14 @@
"Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben", "Points needed to stay on level": "Erforderliche Punkte, um auf diesem Level zu bleiben",
"Previous": "Früher", "Previous": "Früher",
"Previous victories": "Bisherige Siege", "Previous victories": "Bisherige Siege",
"Price details": "Preisdetails",
"Proceed to login": "Weiter zum Login", "Proceed to login": "Weiter zum Login",
"Proceed to payment method": "Weiter zur Zahlungsmethode", "Proceed to payment method": "Weiter zur Zahlungsmethode",
"Provide a payment card in the next step": "Geben Sie Ihre Zahlungskarteninformationen im nächsten Schritt an", "Provide a payment card in the next step": "Geben Sie Ihre Zahlungskarteninformationen im nächsten Schritt an",
"Public price from": "Öffentlicher Preis ab", "Public price from": "Öffentlicher Preis ab",
"Public transport": "Öffentliche Verkehrsmittel", "Public transport": "Öffentliche Verkehrsmittel",
"Queen bed": "Queensize-Bett", "Queen bed": "Queensize-Bett",
"Rate details": "Preisdetails",
"Read more": "Mehr lesen", "Read more": "Mehr lesen",
"Read more & book a table": "Read more & book a table", "Read more & book a table": "Read more & book a table",
"Read more about the hotel": "Lesen Sie mehr über das Hotel", "Read more about the hotel": "Lesen Sie mehr über das Hotel",
@@ -312,8 +317,8 @@
"Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}", "Things nearby HOTEL_NAME": "Dinge in der Nähe von {hotelName}",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Um Ihre Reservierung zu sichern, bitten wir Sie, Ihre Zahlungskarteninformationen zu geben. Sie können sicher sein, dass keine Gebühren zu diesem Zeitpunkt erhoben werden.",
"Total Points": "Gesamtpunktzahl", "Total Points": "Gesamtpunktzahl",
"Total incl VAT": "Gesamt inkl. MwSt.",
"Total price": "Gesamtpreis", "Total price": "Gesamtpreis",
"Total price (incl VAT)": "Gesamtpreis (inkl. MwSt.)",
"Tourist": "Tourist", "Tourist": "Tourist",
"Transaction date": "Transaktionsdatum", "Transaction date": "Transaktionsdatum",
"Transactions": "Transaktionen", "Transactions": "Transaktionen",
@@ -404,6 +409,5 @@
"uppercase letter": "großbuchstabe", "uppercase letter": "großbuchstabe",
"{amount} out of {total}": "{amount} von {total}", "{amount} out of {total}": "{amount} von {total}",
"{amount} {currency}": "{amount} {currency}", "{amount} {currency}": "{amount} {currency}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}"
"{width} cm × {length} cm": "{width} cm × {length} cm"
} }

View File

@@ -1,5 +1,7 @@
{ {
"<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)", "<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)",
"<b>Total price</b> (incl VAT)": "<b>Total price</b> (incl VAT)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/night per adult",
"A destination or hotel name is needed to be able to search for a hotel room.": "A destination or hotel name is needed to be able to search for a hotel room.", "A destination or hotel name is needed to be able to search for a hotel room.": "A destination or hotel name is needed to be able to search for a hotel room.",
"A photo of the room": "A photo of the room", "A photo of the room": "A photo of the room",
"ACCE": "Accessibility", "ACCE": "Accessibility",
@@ -38,6 +40,7 @@
"Attractions": "Attractions", "Attractions": "Attractions",
"Back to scandichotels.com": "Back to scandichotels.com", "Back to scandichotels.com": "Back to scandichotels.com",
"Bar": "Bar", "Bar": "Bar",
"Based on availability": "Based on availability",
"Bed": "Bed", "Bed": "Bed",
"Bed type": "Bed type", "Bed type": "Bed type",
"Birth date": "Birth date", "Birth date": "Birth date",
@@ -252,6 +255,7 @@
"Points needed to stay on level": "Points needed to stay on level", "Points needed to stay on level": "Points needed to stay on level",
"Previous": "Previous", "Previous": "Previous",
"Previous victories": "Previous victories", "Previous victories": "Previous victories",
"Price details": "Price details",
"Print confirmation": "Print confirmation", "Print confirmation": "Print confirmation",
"Proceed to login": "Proceed to login", "Proceed to login": "Proceed to login",
"Proceed to payment method": "Proceed to payment method", "Proceed to payment method": "Proceed to payment method",
@@ -259,6 +263,7 @@
"Public price from": "Public price from", "Public price from": "Public price from",
"Public transport": "Public transport", "Public transport": "Public transport",
"Queen bed": "Queen bed", "Queen bed": "Queen bed",
"Rate details": "Rate details",
"Read more": "Read more", "Read more": "Read more",
"Read more & book a table": "Read more & book a table", "Read more & book a table": "Read more & book a table",
"Read more about the hotel": "Read more about the hotel", "Read more about the hotel": "Read more about the hotel",
@@ -326,10 +331,9 @@
"There are no transactions to display": "There are no transactions to display", "There are no transactions to display": "There are no transactions to display",
"Things nearby HOTEL_NAME": "Things nearby {hotelName}", "Things nearby HOTEL_NAME": "Things nearby {hotelName}",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.",
"Total Points": "Total Points",
"Total cost": "Total cost", "Total cost": "Total cost",
"Total incl VAT": "Total incl VAT",
"Total price": "Total price", "Total price": "Total price",
"Total Points": "Total Points",
"Tourist": "Tourist", "Tourist": "Tourist",
"Transaction date": "Transaction date", "Transaction date": "Transaction date",
"Transactions": "Transactions", "Transactions": "Transactions",
@@ -427,6 +431,5 @@
"{amount} out of {total}": "{amount} out of {total}", "{amount} out of {total}": "{amount} out of {total}",
"{amount} {currency}": "{amount} {currency}", "{amount} {currency}": "{amount} {currency}",
"{card} ending with {cardno}": "{card} ending with {cardno}", "{card} ending with {cardno}": "{card} ending with {cardno}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}"
"{width} cm × {length} cm": "{width} cm × {length} cm"
} }

View File

@@ -1,5 +1,7 @@
{ {
"<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)", "<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)",
"<b>Total price</b> (incl VAT)": "<b>Kokonaishinta</b> (sis. ALV)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/yö per aikuinen",
"A destination or hotel name is needed to be able to search for a hotel room.": "Kohteen tai hotellin nimi tarvitaan, jotta hotellihuonetta voidaan hakea.", "A destination or hotel name is needed to be able to search for a hotel room.": "Kohteen tai hotellin nimi tarvitaan, jotta hotellihuonetta voidaan hakea.",
"A photo of the room": "Kuva huoneesta", "A photo of the room": "Kuva huoneesta",
"ACCE": "Saavutettavuus", "ACCE": "Saavutettavuus",
@@ -35,6 +37,7 @@
"Attractions": "Nähtävyydet", "Attractions": "Nähtävyydet",
"Back to scandichotels.com": "Takaisin scandichotels.com", "Back to scandichotels.com": "Takaisin scandichotels.com",
"Bar": "Bar", "Bar": "Bar",
"Based on availability": "Saatavuuden mukaan",
"Bed type": "Vuodetyyppi", "Bed type": "Vuodetyyppi",
"Birth date": "Syntymäaika", "Birth date": "Syntymäaika",
"Book": "Varaa", "Book": "Varaa",
@@ -242,12 +245,14 @@
"Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet", "Points needed to stay on level": "Tällä tasolla pysymiseen tarvittavat pisteet",
"Previous": "Aikaisempi", "Previous": "Aikaisempi",
"Previous victories": "Edelliset voitot", "Previous victories": "Edelliset voitot",
"Price details": "Hintatiedot",
"Proceed to login": "Jatka kirjautumiseen", "Proceed to login": "Jatka kirjautumiseen",
"Proceed to payment method": "Siirry maksutavalle", "Proceed to payment method": "Siirry maksutavalle",
"Provide a payment card in the next step": "Anna maksukortin tiedot seuraavassa vaiheessa", "Provide a payment card in the next step": "Anna maksukortin tiedot seuraavassa vaiheessa",
"Public price from": "Julkinen hinta alkaen", "Public price from": "Julkinen hinta alkaen",
"Public transport": "Julkinen liikenne", "Public transport": "Julkinen liikenne",
"Queen bed": "Queen-vuode", "Queen bed": "Queen-vuode",
"Rate details": "Hintatiedot",
"Read more": "Lue lisää", "Read more": "Lue lisää",
"Read more & book a table": "Read more & book a table", "Read more & book a table": "Read more & book a table",
"Read more about the hotel": "Lue lisää hotellista", "Read more about the hotel": "Lue lisää hotellista",
@@ -314,8 +319,8 @@
"Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}", "Things nearby HOTEL_NAME": "Lähellä olevia asioita {hotelName}",
"To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.", "To secure your reservation, we kindly ask you to provide your payment card details. Rest assured, no charges will be made at this time.": "Varmistaaksesi varauksen, pyydämme sinua antamaan meille maksukortin tiedot. Varmista, että ei veloiteta maksusi tällä hetkellä.",
"Total Points": "Kokonaispisteet", "Total Points": "Kokonaispisteet",
"Total incl VAT": "Yhteensä sis. alv",
"Total price": "Kokonaishinta", "Total price": "Kokonaishinta",
"Total price (incl VAT)": "Kokonaishinta (sis. ALV)",
"Tourist": "Turisti", "Tourist": "Turisti",
"Transaction date": "Tapahtuman päivämäärä", "Transaction date": "Tapahtuman päivämäärä",
"Transactions": "Tapahtumat", "Transactions": "Tapahtumat",
@@ -404,6 +409,5 @@
"uppercase letter": "iso kirjain", "uppercase letter": "iso kirjain",
"{amount} out of {total}": "{amount}/{total}", "{amount} out of {total}": "{amount}/{total}",
"{amount} {currency}": "{amount} {currency}", "{amount} {currency}": "{amount} {currency}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}"
"{width} cm × {length} cm": "{width} cm × {length} cm"
} }

View File

@@ -1,5 +1,7 @@
{ {
"<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)", "<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)",
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl. mva)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/natt per voksen",
"A destination or hotel name is needed to be able to search for a hotel room.": "Et reisemål eller hotellnavn er nødvendig for å kunne søke etter et hotellrom.", "A destination or hotel name is needed to be able to search for a hotel room.": "Et reisemål eller hotellnavn er nødvendig for å kunne søke etter et hotellrom.",
"A photo of the room": "Et bilde av rommet", "A photo of the room": "Et bilde av rommet",
"ACCE": "Tilgjengelighet", "ACCE": "Tilgjengelighet",
@@ -35,6 +37,7 @@
"Attractions": "Attraksjoner", "Attractions": "Attraksjoner",
"Back to scandichotels.com": "Tilbake til scandichotels.com", "Back to scandichotels.com": "Tilbake til scandichotels.com",
"Bar": "Bar", "Bar": "Bar",
"Based on availability": "Basert på tilgjengelighet",
"Bed type": "Seng type", "Bed type": "Seng type",
"Birth date": "Fødselsdato", "Birth date": "Fødselsdato",
"Book": "Bestill", "Book": "Bestill",
@@ -240,12 +243,14 @@
"Points needed to stay on level": "Poeng som trengs for å holde seg på nivå", "Points needed to stay on level": "Poeng som trengs for å holde seg på nivå",
"Previous": "Tidligere", "Previous": "Tidligere",
"Previous victories": "Tidligere seire", "Previous victories": "Tidligere seire",
"Price details": "Prisdetaljer",
"Proceed to login": "Fortsett til innlogging", "Proceed to login": "Fortsett til innlogging",
"Proceed to payment method": "Fortsett til betalingsmetode", "Proceed to payment method": "Fortsett til betalingsmetode",
"Provide a payment card in the next step": "Gi oss dine betalingskortdetaljer i neste steg", "Provide a payment card in the next step": "Gi oss dine betalingskortdetaljer i neste steg",
"Public price from": "Offentlig pris fra", "Public price from": "Offentlig pris fra",
"Public transport": "Offentlig transport", "Public transport": "Offentlig transport",
"Queen bed": "Queen-size-seng", "Queen bed": "Queen-size-seng",
"Rate details": "Prisdetaljer",
"Read more": "Les mer", "Read more": "Les mer",
"Read more & book a table": "Read more & book a table", "Read more & book a table": "Read more & book a table",
"Read more about the hotel": "Les mer om hotellet", "Read more about the hotel": "Les mer om hotellet",
@@ -402,6 +407,5 @@
"uppercase letter": "stor bokstav", "uppercase letter": "stor bokstav",
"{amount} out of {total}": "{amount} av {total}", "{amount} out of {total}": "{amount} av {total}",
"{amount} {currency}": "{amount} {currency}", "{amount} {currency}": "{amount} {currency}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}"
"{width} cm × {length} cm": "{width} cm × {length} cm"
} }

View File

@@ -1,5 +1,7 @@
{ {
"<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)", "<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)",
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl moms)",
"<b>{amount} {currency}</b>/night per adult": "<b>{amount} {currency}</b>/natt per vuxen",
"A destination or hotel name is needed to be able to search for a hotel room.": "Ett destinations- eller hotellnamn behövs för att kunna söka efter ett hotellrum.", "A destination or hotel name is needed to be able to search for a hotel room.": "Ett destinations- eller hotellnamn behövs för att kunna söka efter ett hotellrum.",
"A photo of the room": "Ett foto av rummet", "A photo of the room": "Ett foto av rummet",
"ACCE": "Tillgänglighet", "ACCE": "Tillgänglighet",
@@ -35,6 +37,7 @@
"Attractions": "Sevärdheter", "Attractions": "Sevärdheter",
"Back to scandichotels.com": "Tillbaka till scandichotels.com", "Back to scandichotels.com": "Tillbaka till scandichotels.com",
"Bar": "Bar", "Bar": "Bar",
"Based on availability": "Baserat på tillgänglighet",
"Bed type": "Sängtyp", "Bed type": "Sängtyp",
"Birth date": "Födelsedatum", "Birth date": "Födelsedatum",
"Book": "Boka", "Book": "Boka",
@@ -240,12 +243,14 @@
"Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå", "Points needed to stay on level": "Poäng som behövs för att hålla sig på nivå",
"Previous": "Föregående", "Previous": "Föregående",
"Previous victories": "Tidigare segrar", "Previous victories": "Tidigare segrar",
"Price details": "Prisdetaljer",
"Proceed to login": "Fortsätt till inloggning", "Proceed to login": "Fortsätt till inloggning",
"Proceed to payment method": "Gå vidare till betalningsmetod", "Proceed to payment method": "Gå vidare till betalningsmetod",
"Provide a payment card in the next step": "Ge oss dina betalkortdetaljer i nästa steg", "Provide a payment card in the next step": "Ge oss dina betalkortdetaljer i nästa steg",
"Public price from": "Offentligt pris från", "Public price from": "Offentligt pris från",
"Public transport": "Kollektivtrafik", "Public transport": "Kollektivtrafik",
"Queen bed": "Queen size-säng", "Queen bed": "Queen size-säng",
"Rate details": "Detaljer om rumspriset",
"Read more": "Läs mer", "Read more": "Läs mer",
"Read more & book a table": "Read more & book a table", "Read more & book a table": "Read more & book a table",
"Read more about the hotel": "Läs mer om hotellet", "Read more about the hotel": "Läs mer om hotellet",
@@ -405,6 +410,5 @@
"paying": "betalar", "paying": "betalar",
"uppercase letter": "stor bokstav", "uppercase letter": "stor bokstav",
"{amount} {currency}": "{amount} {currency}", "{amount} {currency}": "{amount} {currency}",
"{difference}{amount} {currency}": "{difference}{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}"
"{width} cm × {length} cm": "{width} cm × {length} cm"
} }

View File

@@ -1,6 +1,11 @@
import { cache } from "react" import { cache } from "react"
import { Lang } from "@/constants/languages" import { Lang } from "@/constants/languages"
import {
GetRoomsAvailabilityInput,
GetSelectedRoomAvailabilityInput,
HotelIncludeEnum,
} from "@/server/routers/hotels/input"
import { serverClient } from "../server" import { serverClient } from "../server"
@@ -50,15 +55,22 @@ export const getUserTracking = cache(async function getMemoizedUserTracking() {
return serverClient().user.tracking() return serverClient().user.tracking()
}) })
export const getHotelData = cache(async function getMemoizedHotelData( export const getHotelData = cache(async function getMemoizedHotelData({
hotelId: string, hotelId,
language: string, language,
isCardOnlyPayment,
include,
}: {
hotelId: string
language: string
isCardOnlyPayment?: boolean isCardOnlyPayment?: boolean
) { include?: HotelIncludeEnum[]
}) {
return serverClient().hotel.hotelData.get({ return serverClient().hotel.hotelData.get({
hotelId, hotelId,
language, language,
isCardOnlyPayment, isCardOnlyPayment,
include,
}) })
}) })
@@ -71,17 +83,9 @@ export const getRoomAvailability = cache(
children, children,
promotionCode, promotionCode,
rateCode, rateCode,
}: { }: GetRoomsAvailabilityInput) {
hotelId: string
adults: number
roomStayStartDate: string
roomStayEndDate: string
children?: string
promotionCode?: string
rateCode?: string
}) {
return serverClient().hotel.availability.rooms({ return serverClient().hotel.availability.rooms({
hotelId: parseInt(hotelId), hotelId,
adults, adults,
roomStayStartDate, roomStayStartDate,
roomStayEndDate, roomStayEndDate,
@@ -92,6 +96,14 @@ export const getRoomAvailability = cache(
} }
) )
export const getSelectedRoomAvailability = cache(
async function getMemoizedRoomAvailability(
args: GetSelectedRoomAvailabilityInput
) {
return serverClient().hotel.availability.room(args)
}
)
export const getFooter = cache(async function getMemoizedFooter() { export const getFooter = cache(async function getMemoizedFooter() {
return serverClient().contentstack.base.footer() return serverClient().contentstack.base.footer()
}) })

View File

@@ -87,12 +87,12 @@ const nextConfig = {
// value: undefined, // value: undefined,
// }, // },
// { // {
// key: "fromdate", // key: "fromDate",
// type: "query", // type: "query",
// value: undefined, // value: undefined,
// }, // },
// { // {
// key: "todate", // key: "toDate",
// type: "query", // type: "query",
// value: undefined, // value: undefined,
// }, // },

View File

@@ -29,17 +29,43 @@ export const getRoomsAvailabilityInputSchema = z.object({
rateCode: z.string().optional(), rateCode: z.string().optional(),
}) })
export const getSelectedRoomAvailabilityInputSchema = z.object({
hotelId: z.number(),
roomStayStartDate: z.string(),
roomStayEndDate: z.string(),
adults: z.number(),
children: z.string().optional(),
promotionCode: z.string().optional(),
reservationProfileType: z.string().optional().default(""),
attachedProfileId: z.string().optional().default(""),
rateCode: z.string(),
roomTypeCode: z.string(),
})
export type GetSelectedRoomAvailabilityInput = z.input<
typeof getSelectedRoomAvailabilityInputSchema
>
export type GetRoomsAvailabilityInput = z.input<
typeof getRoomsAvailabilityInputSchema
>
export const getRatesInputSchema = z.object({ export const getRatesInputSchema = z.object({
hotelId: z.string(), hotelId: z.string(),
}) })
export const getlHotelDataInputSchema = z.object({ export enum HotelIncludeEnum {
"RoomCategories",
"NearbyHotels",
"Restaurants",
"City",
}
export const getHotelDataInputSchema = z.object({
hotelId: z.string(), hotelId: z.string(),
language: z.string(), language: z.string(),
isCardOnlyPayment: z.boolean().optional(), isCardOnlyPayment: z.boolean().optional(),
include: z include: z.array(z.nativeEnum(HotelIncludeEnum)).optional(),
.array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"]))
.optional(),
}) })
export const getBreakfastPackageInput = z.object({ export const getBreakfastPackageInput = z.object({

View File

@@ -32,11 +32,12 @@ import {
} from "./schemas/packages" } from "./schemas/packages"
import { import {
getBreakfastPackageInput, getBreakfastPackageInput,
getHotelDataInputSchema,
getHotelInputSchema, getHotelInputSchema,
getHotelsAvailabilityInputSchema, getHotelsAvailabilityInputSchema,
getlHotelDataInputSchema,
getRatesInputSchema, getRatesInputSchema,
getRoomsAvailabilityInputSchema, getRoomsAvailabilityInputSchema,
getSelectedRoomAvailabilityInputSchema,
} from "./input" } from "./input"
import { import {
breakfastPackagesSchema, breakfastPackagesSchema,
@@ -93,6 +94,16 @@ const roomsAvailabilityFailCounter = meter.createCounter(
"trpc.hotel.availability.rooms-fail" "trpc.hotel.availability.rooms-fail"
) )
const selectedRoomAvailabilityCounter = meter.createCounter(
"trpc.hotel.availability.room"
)
const selectedRoomAvailabilitySuccessCounter = meter.createCounter(
"trpc.hotel.availability.room-success"
)
const selectedRoomAvailabilityFailCounter = meter.createCounter(
"trpc.hotel.availability.room-fail"
)
const breakfastPackagesCounter = meter.createCounter("trpc.package.breakfast") const breakfastPackagesCounter = meter.createCounter("trpc.package.breakfast")
const breakfastPackagesSuccessCounter = meter.createCounter( const breakfastPackagesSuccessCounter = meter.createCounter(
"trpc.package.breakfast-success" "trpc.package.breakfast-success"
@@ -545,6 +556,161 @@ export const hotelQueryRouter = router({
return validateAvailabilityData.data return validateAvailabilityData.data
}), }),
room: serviceProcedure
.input(getSelectedRoomAvailabilityInputSchema)
.query(async ({ input, ctx }) => {
const {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
promotionCode,
reservationProfileType,
attachedProfileId,
rateCode,
roomTypeCode,
} = input
const params: Record<string, string | number | undefined> = {
roomStayStartDate,
roomStayEndDate,
adults,
...(children && { children }),
promotionCode,
reservationProfileType,
attachedProfileId,
}
selectedRoomAvailabilityCounter.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
promotionCode,
reservationProfileType,
})
console.info(
"api.hotels.selectedRoomAvailability start",
JSON.stringify({ query: { hotelId, params } })
)
const apiResponseAvailability = await api.get(
api.endpoints.v1.Availability.hotel(hotelId.toString()),
{
headers: {
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
)
if (!apiResponseAvailability.ok) {
const text = await apiResponseAvailability.text()
selectedRoomAvailabilityFailCounter.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
promotionCode,
reservationProfileType,
error_type: "http_error",
error: JSON.stringify({
status: apiResponseAvailability.status,
statusText: apiResponseAvailability.statusText,
text,
}),
})
console.error(
"api.hotels.selectedRoomAvailability error",
JSON.stringify({
query: { hotelId, params },
error: {
status: apiResponseAvailability.status,
statusText: apiResponseAvailability.statusText,
text,
},
})
)
return null
}
const apiJsonAvailability = await apiResponseAvailability.json()
const validateAvailabilityData =
getRoomsAvailabilitySchema.safeParse(apiJsonAvailability)
if (!validateAvailabilityData.success) {
selectedRoomAvailabilityFailCounter.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
promotionCode,
reservationProfileType,
error_type: "validation_error",
error: JSON.stringify(validateAvailabilityData.error),
})
console.error(
"api.hotels.selectedRoomAvailability validation error",
JSON.stringify({
query: { hotelId, params },
error: validateAvailabilityData.error,
})
)
throw badRequestError()
}
const selectedRoom = validateAvailabilityData.data.roomConfigurations
.filter((room) => room.status === "Available")
.find((room) => room.roomTypeCode === roomTypeCode)
if (!selectedRoom) {
console.error("No matching room found")
return null
}
const memberRate = selectedRoom.products.find(
(rate) => rate.productType.member?.rateCode === rateCode
)?.productType.member
const publicRate = selectedRoom.products.find(
(rate) => rate.productType.public?.rateCode === rateCode
)?.productType.public
const mustBeGuaranteed =
validateAvailabilityData.data.rateDefinitions.filter(
(rate) => rate.rateCode === rateCode
)[0].mustBeGuaranteed
const cancellationText =
validateAvailabilityData.data.rateDefinitions.find(
(rate) => rate.rateCode === rateCode
)?.cancellationText ?? ""
selectedRoomAvailabilitySuccessCounter.add(1, {
hotelId,
roomStayStartDate,
roomStayEndDate,
adults,
children,
promotionCode,
reservationProfileType,
})
console.info(
"api.hotels.selectedRoomAvailability success",
JSON.stringify({
query: { hotelId, params: params },
})
)
return {
selectedRoom,
mustBeGuaranteed,
cancellationText,
memberRate,
publicRate,
}
}),
}), }),
rates: router({ rates: router({
get: publicProcedure get: publicProcedure
@@ -584,7 +750,7 @@ export const hotelQueryRouter = router({
}), }),
hotelData: router({ hotelData: router({
get: serviceProcedure get: serviceProcedure
.input(getlHotelDataInputSchema) .input(getHotelDataInputSchema)
.query(async ({ ctx, input }) => { .query(async ({ ctx, input }) => {
const { hotelId, language, include, isCardOnlyPayment } = input const { hotelId, language, include, isCardOnlyPayment } = input

View File

@@ -87,8 +87,11 @@ export const roomSchema = z
name: data.attributes.name, name: data.attributes.name,
occupancy: data.attributes.occupancy, occupancy: data.attributes.occupancy,
roomSize: data.attributes.roomSize, roomSize: data.attributes.roomSize,
roomTypes: data.attributes.roomTypes,
sortOrder: data.attributes.sortOrder, sortOrder: data.attributes.sortOrder,
type: data.type, type: data.type,
roomFacilities: data.attributes.roomFacilities, roomFacilities: data.attributes.roomFacilities,
} }
}) })
export type RoomType = Pick<z.output<typeof roomSchema>, "roomTypes" | "name">

View File

@@ -1,30 +1,33 @@
import { produce } from "immer" import { produce } from "immer"
import { ReadonlyURLSearchParams } from "next/navigation"
import { createContext, useContext } from "react" import { createContext, useContext } from "react"
import { create, useStore } from "zustand" import { create, useStore } from "zustand"
import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema"
import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema" import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema" import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema"
import { getQueryParamsForEnterDetails } from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { BreakfastPackage } from "@/types/components/enterDetails/breakfast" import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData"
import { DetailsSchema } from "@/types/components/enterDetails/details" import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek" import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
import { StepEnum } from "@/types/components/enterDetails/step" import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek"
import { BedTypeEnum } from "@/types/enums/bedType" import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import { BreakfastPackageEnum } from "@/types/enums/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast"
const SESSION_STORAGE_KEY = "enterDetails" const SESSION_STORAGE_KEY = "enterDetails"
interface EnterDetailsState { interface EnterDetailsState {
data: { userData: {
bedType: BedTypeEnum | undefined bedType: string | undefined
breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined
} & DetailsSchema } & DetailsSchema
roomData: BookingData
steps: StepEnum[] steps: StepEnum[]
currentStep: StepEnum currentStep: StepEnum
activeSidePeek: SidePeekEnum | null activeSidePeek: SidePeekEnum | null
isValid: Record<StepEnum, boolean> isValid: Record<StepEnum, boolean>
completeStep: (updatedData: Partial<EnterDetailsState["data"]>) => void completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void
navigate: ( navigate: (
step: StepEnum, step: StepEnum,
updatedData?: Record<string, string | boolean | BreakfastPackage> updatedData?: Record<string, string | boolean | BreakfastPackage>
@@ -34,13 +37,21 @@ interface EnterDetailsState {
closeSidePeek: () => void closeSidePeek: () => void
} }
export function initEditDetailsState(currentStep: StepEnum) { export function initEditDetailsState(
currentStep: StepEnum,
searchParams: ReadonlyURLSearchParams
) {
const isBrowser = typeof window !== "undefined" const isBrowser = typeof window !== "undefined"
const sessionData = isBrowser const sessionData = isBrowser
? sessionStorage.getItem(SESSION_STORAGE_KEY) ? sessionStorage.getItem(SESSION_STORAGE_KEY)
: null : null
const defaultData: EnterDetailsState["data"] = { let roomData: BookingData
if (searchParams?.size) {
roomData = getQueryParamsForEnterDetails(searchParams)
}
const defaultUserData: EnterDetailsState["userData"] = {
bedType: undefined, bedType: undefined,
breakfast: undefined, breakfast: undefined,
countryCode: "", countryCode: "",
@@ -54,14 +65,14 @@ export function initEditDetailsState(currentStep: StepEnum) {
termsAccepted: false, termsAccepted: false,
} }
let inputData = {} let inputUserData = {}
if (sessionData) { if (sessionData) {
inputData = JSON.parse(sessionData) inputUserData = JSON.parse(sessionData)
} }
const validPaths = [StepEnum.selectBed] const validPaths = [StepEnum.selectBed]
let initialData: EnterDetailsState["data"] = defaultData let initialData: EnterDetailsState["userData"] = defaultUserData
const isValid = { const isValid = {
[StepEnum.selectBed]: false, [StepEnum.selectBed]: false,
@@ -70,19 +81,19 @@ export function initEditDetailsState(currentStep: StepEnum) {
[StepEnum.payment]: false, [StepEnum.payment]: false,
} }
const validatedBedType = bedTypeSchema.safeParse(inputData) const validatedBedType = bedTypeSchema.safeParse(inputUserData)
if (validatedBedType.success) { if (validatedBedType.success) {
validPaths.push(StepEnum.breakfast) validPaths.push(StepEnum.breakfast)
initialData = { ...initialData, ...validatedBedType.data } initialData = { ...initialData, ...validatedBedType.data }
isValid[StepEnum.selectBed] = true isValid[StepEnum.selectBed] = true
} }
const validatedBreakfast = breakfastStoreSchema.safeParse(inputData) const validatedBreakfast = breakfastStoreSchema.safeParse(inputUserData)
if (validatedBreakfast.success) { if (validatedBreakfast.success) {
validPaths.push(StepEnum.details) validPaths.push(StepEnum.details)
initialData = { ...initialData, ...validatedBreakfast.data } initialData = { ...initialData, ...validatedBreakfast.data }
isValid[StepEnum.breakfast] = true isValid[StepEnum.breakfast] = true
} }
const validatedDetails = detailsSchema.safeParse(inputData) const validatedDetails = detailsSchema.safeParse(inputUserData)
if (validatedDetails.success) { if (validatedDetails.success) {
validPaths.push(StepEnum.payment) validPaths.push(StepEnum.payment)
initialData = { ...initialData, ...validatedDetails.data } initialData = { ...initialData, ...validatedDetails.data }
@@ -101,7 +112,8 @@ export function initEditDetailsState(currentStep: StepEnum) {
} }
return create<EnterDetailsState>()((set, get) => ({ return create<EnterDetailsState>()((set, get) => ({
data: initialData, userData: initialData,
roomData,
steps: Object.values(StepEnum), steps: Object.values(StepEnum),
setCurrentStep: (step) => set({ currentStep: step }), setCurrentStep: (step) => set({ currentStep: step }),
navigate: (step, updatedData) => navigate: (step, updatedData) =>
@@ -129,14 +141,17 @@ export function initEditDetailsState(currentStep: StepEnum) {
isValid, isValid,
completeStep: (updatedData) => completeStep: (updatedData) =>
set( set(
produce((state) => { produce((state: EnterDetailsState) => {
state.isValid[state.currentStep] = true state.isValid[state.currentStep] = true
const nextStep = const nextStep =
state.steps[state.steps.indexOf(state.currentStep) + 1] state.steps[state.steps.indexOf(state.currentStep) + 1]
state.data = { ...state.data, ...updatedData } // @ts-expect-error: ts has a hard time understanding that "false | true" equals "boolean"
state.userData = {
...state.userData,
...updatedData,
}
state.currentStep = nextStep state.currentStep = nextStep
get().navigate(nextStep, updatedData) get().navigate(nextStep, updatedData)
}) })

View File

@@ -2,4 +2,16 @@ import { z } from "zod"
import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema" import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema"
type BedType = {
description: string
size: {
min: number
max: number
}
value: string
}
export type BedTypeProps = {
bedTypes: BedType[]
}
export interface BedTypeSchema extends z.output<typeof bedTypeSchema> {} export interface BedTypeSchema extends z.output<typeof bedTypeSchema> {}

View File

@@ -0,0 +1,31 @@
interface Child {
bed: string
age: number
}
interface Room {
adults: number
roomtype?: string
ratecode?: string
child?: Child[]
}
export interface BookingData {
hotel: string
fromDate: string
toDate: string
room: Room[]
}
type Price = {
price?: string
currency?: string
}
export type RoomsData = {
roomType: string
localPrice: Price
euroPrice: Price
adults: number
children?: Child[]
cancellationText: string
}

View File

@@ -1,4 +1,4 @@
import { StepEnum } from "../../enterDetails/step" import { StepEnum } from "../enterDetails/step"
export interface SectionAccordionProps { export interface SectionAccordionProps {
header: string header: string

View File

@@ -7,8 +7,9 @@ export interface Child {
interface Room { interface Room {
adults: number adults: number
roomcode?: string roomtype?: string
ratecode?: string ratecode?: string
counterratecode?: string
child?: Child[] child?: Child[]
} }

8
utils/format.ts Normal file
View File

@@ -0,0 +1,8 @@
import { Lang } from "@/constants/languages"
/// Function to format numbers with space as thousands separator
export function formatNumber(num: number) {
// sv hardcoded to force space on thousands
const formatter = new Intl.NumberFormat(Lang.sv)
return formatter.format(num)
}