feat: save search params from select-rate to store

This commit is contained in:
Christel Westerberg
2024-10-22 16:19:15 +02:00
parent b5dce01fd3
commit 85fdefb5ac
28 changed files with 332 additions and 195 deletions

View File

@@ -7,7 +7,7 @@ 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({
@@ -19,19 +19,18 @@ 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
} }>) {
>) {
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.summary}>
<Summary /> <Summary isMember={false} />
</aside> </aside>
</div> </div>
{sidePeek} {sidePeek}

View File

@@ -17,7 +17,7 @@ import SectionAccordion from "@/components/HotelReservation/EnterDetails/Section
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils" import getHotelReservationQueryParams 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 type { LangParams, PageArgs } from "@/types/params" import type { LangParams, PageArgs } from "@/types/params"
export function preload() { export function preload() {

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

@@ -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,18 +14,17 @@ 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 { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
import { BedTypeEnum } from "@/types/enums/bedType"
export default function BedType() { export default function BedType() {
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
? { ? {
bedType, bedType,
} }
: undefined, : undefined,
criteriaMode: "all", criteriaMode: "all",
mode: "all", mode: "all",

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) {
@@ -76,21 +76,21 @@ export default function Breakfast({ packages }: BreakfastProps) {
subtitle={ subtitle={
pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
? intl.formatMessage<React.ReactNode>( ? intl.formatMessage<React.ReactNode>(
{ id: "breakfast.price.free" }, { id: "breakfast.price.free" },
{ {
amount: pkg.originalPrice, amount: pkg.originalPrice,
currency: pkg.currency, currency: pkg.currency,
free: (str) => <Highlight>{str}</Highlight>, free: (str) => <Highlight>{str}</Highlight>,
strikethrough: (str) => <s>{str}</s>, strikethrough: (str) => <s>{str}</s>,
} }
) )
: intl.formatMessage( : intl.formatMessage(
{ id: "breakfast.price" }, { id: "breakfast.price" },
{ {
amount: pkg.packagePrice, amount: pkg.packagePrice,
currency: pkg.currency, currency: pkg.currency,
} }
) )
} }
text={intl.formatMessage({ text={intl.formatMessage({
id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.", id: "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.",

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

@@ -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,154 +1,206 @@
"use client"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import { trpc } from "@/lib/trpc/client"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { ArrowRightIcon } from "@/components/Icons" import { ArrowRightIcon } from "@/components/Icons"
import LoadingSpinner from "@/components/LoadingSpinner"
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 = [
{
adults: 1,
type: "Cozy cabin",
},
]
export default async function Summary() { export default function Summary({ isMember }: { isMember: boolean }) {
const intl = await getIntl() const intl = useIntl()
const lang = getLang() const lang = useLang()
const fromDate = dt().locale(lang).format("ddd, D MMM") const { fromDate, toDate, rooms, hotel, bedType, breakfast } =
const toDate = dt().add(1, "day").locale(lang).format("ddd, D MMM") useEnterDetailsStore((state) => ({
const diff = dt(toDate).diff(fromDate, "days") fromDate: state.roomData.fromdate,
toDate: state.roomData.todate,
rooms: state.roomData.room,
hotel: state.roomData.hotel,
bedType: state.userData.bedType,
breakfast: state.userData.breakfast,
}))
const totalAdults = rooms.reduce((total, room) => total + room.adults, 0) const totalAdults = rooms.reduce((total, room) => total + room.adults, 0)
const adults = intl.formatMessage( const {
{ id: "booking.adults" }, data: availabilityData,
{ totalAdults: totalAdults } isLoading,
error,
} = trpc.hotel.availability.rooms.useQuery(
{
hotelId: parseInt(hotel),
adults: totalAdults,
roomStayStartDate: dt(fromDate).format("YYYY-MM-DD"),
roomStayEndDate: dt(toDate).format("YYYY-MM-DD"),
},
{ enabled: !!hotel && !!fromDate && !!toDate }
) )
const diff = dt(toDate).diff(fromDate, "days")
const nights = intl.formatMessage( const nights = intl.formatMessage(
{ id: "booking.nights" }, { id: "booking.nights" },
{ totalNights: diff } { totalNights: diff }
) )
const addOns = [ if (isLoading) {
{ return <LoadingSpinner />
price: intl.formatMessage({ id: "Included" }), }
title: intl.formatMessage({ id: "King bed" }), const populatedRooms = rooms
}, .map((room) => {
{ const chosenRoom = availabilityData?.roomConfigurations.find(
price: intl.formatMessage({ id: "Included" }), (availRoom) => room.roomtypecode === availRoom.roomTypeCode
title: intl.formatMessage({ id: "Breakfast buffet" }), )
}, const cancellationText = availabilityData?.rateDefinitions.find(
] (rate) => rate.rateCode === room.ratecode
)?.cancellationText
const mappedRooms = Array.from( if (chosenRoom) {
rooms const memberPrice = chosenRoom.products.find(
.reduce((acc, room) => { (rate) => rate.productType.member?.rateCode === room.ratecode
const currentRoom = acc.get(room.type) )?.productType.member?.localPrice.pricePerStay
acc.set(room.type, { const publicPrice = chosenRoom.products.find(
total: currentRoom ? currentRoom.total + 1 : 1, (rate) => rate.productType.public?.rateCode === room.ratecode
type: room.type, )?.productType.public?.localPrice.pricePerStay
})
return acc return {
}, new Map()) roomType: chosenRoom.roomType,
.values() memberPrice: memberPrice && formatNumber(parseInt(memberPrice)),
) publicPrice: publicPrice && formatNumber(parseInt(publicPrice)),
adults: room.adults,
children: room.child,
cancellationText,
}
}
})
.filter((room): room is RoomsData => room !== undefined)
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 className={styles.entry}> {populatedRooms.map((room, idx) => (
<Caption color="uiTextMediumContrast"> <RoomBreakdown key={idx} room={room} isMember={isMember} />
{`${nights}, ${adults}`}
</Caption>
<Caption color="uiTextHighContrast">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "4536", currency: "SEK" }
)}
</Caption>
</div>
{addOns.map((addOn) => (
<div className={styles.entry} key={addOn.title}>
<Caption color="uiTextMediumContrast">{addOn.title}</Caption>
<Caption color="uiTextHighContrast">{addOn.price}</Caption>
</div>
))} ))}
{bedType ? (
<div className={styles.entry}>
<Body color="textHighContrast">{bedType}</Body>
<Caption color="red">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "0", currency: "SEK" }
)}
</Caption>
</div>
) : null}
{breakfast ? (
<div className={styles.entry}>
<Body color="textHighContrast">{breakfast}</Body>
<Caption color="red">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "0", currency: "SEK" }
)}
</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">
<Body textTransform="bold"> {intl.formatMessage({ id: "Total price (incl VAT)" })}
{intl.formatMessage({ id: "Total incl VAT" })} </Body>
</Body> <Body textTransform="bold">
<Body textTransform="bold"> {intl.formatMessage(
{intl.formatMessage( { id: "{amount} {currency}" },
{ id: "{amount} {currency}" }, { amount: "4686", currency: "SEK" }
{ amount: "4686", currency: "SEK" } )}
)} </Body>
</Body>
</div>
<div className={styles.entry}>
<Caption color="uiTextMediumContrast">
{intl.formatMessage({ id: "Approx." })}
</Caption>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "455", currency: "EUR" }
)}
</Caption>
</div>
</div> </div>
<div> <div className={styles.entry}>
<div className={styles.entry}> <Caption color="uiTextMediumContrast">
<Body color="red" textTransform="bold"> {intl.formatMessage({ id: "Approx." })}
{intl.formatMessage({ id: "Member price" })} </Caption>
</Body> <Caption color="uiTextMediumContrast">
<Body color="red" textTransform="bold"> {intl.formatMessage(
{intl.formatMessage( { id: "{amount} {currency}" },
{ id: "{amount} {currency}" }, { amount: "455", currency: "EUR" }
{ amount: "4219", currency: "SEK" } )}
)} </Caption>
</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>
</div>
</div> </div>
</div> </div>
</section> </section>
) )
} }
function RoomBreakdown({
room,
isMember,
}: {
room: RoomsData
isMember: boolean
}) {
const intl = useIntl()
let color: "uiTextHighContrast" | "red" = "uiTextHighContrast"
let price = room.publicPrice
if (isMember) {
color = "red"
price = room.memberPrice
}
return (
<div>
<div className={styles.entry}>
<Body color="textHighContrast">{room.roomType}</Body>
<Caption color={color}>
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: price, currency: "SEK" }
)}
</Caption>
</div>
<Caption color="uiTextMediumContrast">
{intl.formatMessage(
{ id: "booking.adults" },
{ 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>
)
}

View File

@@ -22,7 +22,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 {

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,7 @@ const config = {
textHighContrast: styles.textHighContrast, textHighContrast: styles.textHighContrast,
white: styles.white, white: styles.white,
peach50: styles.peach50, peach50: styles.peach50,
peach80: styles.peach80, baseTextMediumContrast: styles.baseTextMediumContrast,
uiTextHighContrast: styles.uiTextHighContrast, uiTextHighContrast: styles.uiTextHighContrast,
uiTextMediumContrast: styles.uiTextMediumContrast, uiTextMediumContrast: styles.uiTextMediumContrast,
uiTextPlaceholder: styles.uiTextPlaceholder, uiTextPlaceholder: styles.uiTextPlaceholder,

View File

@@ -313,8 +313,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",

View File

@@ -312,8 +312,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",

View File

@@ -326,10 +326,10 @@
"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",
"Total price (incl VAT)": "Total price (incl VAT)",
"Tourist": "Tourist", "Tourist": "Tourist",
"Transaction date": "Transaction date", "Transaction date": "Transaction date",
"Transactions": "Transactions", "Transactions": "Transactions",

View File

@@ -314,8 +314,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",

View File

@@ -1,30 +1,34 @@
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 getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { BreakfastPackage } from "@/types/components/enterDetails/breakfast" import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
import { DetailsSchema } from "@/types/components/enterDetails/details" import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek"
import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek" import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import { StepEnum } from "@/types/components/enterDetails/step"
import { BedTypeEnum } from "@/types/enums/bedType" import { BedTypeEnum } from "@/types/enums/bedType"
import { BreakfastPackageEnum } from "@/types/enums/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import type { BookingData } from "@/types/components/hotelReservation/enterDetails/bookingData"
import type { DetailsSchema } from "@/types/components/hotelReservation/enterDetails/details"
const SESSION_STORAGE_KEY = "enterDetails" const SESSION_STORAGE_KEY = "enterDetails"
interface EnterDetailsState { interface EnterDetailsState {
data: { userData: {
bedType: BedTypeEnum | undefined bedType: BedTypeEnum | 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 +38,60 @@ interface EnterDetailsState {
closeSidePeek: () => void closeSidePeek: () => void
} }
export function initEditDetailsState(currentStep: StepEnum) { function getUpdatedValue<T>(
searchParams: URLSearchParams,
key: string,
defaultValue: T
): T {
const value = searchParams.get(key)
if (value === null) return defaultValue
if (typeof defaultValue === "number")
return parseInt(value, 10) as unknown as T
if (typeof defaultValue === "boolean")
return (value === "true") as unknown as T
if (defaultValue instanceof Date) return new Date(value) as unknown as T
return value as unknown as T
}
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"] = { const today = new Date()
const tomorrow = new Date()
tomorrow.setDate(today.getDate() + 1)
let roomData: BookingData
if (searchParams?.size) {
roomData = getHotelReservationQueryParams(searchParams)
roomData.room = roomData.room.map((room, index) => ({
...room,
adults: getUpdatedValue(
searchParams,
`room[${index}].adults`,
room.adults
),
roomtypecode: getUpdatedValue(
searchParams,
`room[${index}].roomtypecode`,
room.roomtypecode
),
ratecode: getUpdatedValue(
searchParams,
`room[${index}].ratecode`,
room.ratecode
),
}))
}
const defaultUserData: EnterDetailsState["userData"] = {
bedType: undefined, bedType: undefined,
breakfast: undefined, breakfast: undefined,
countryCode: "", countryCode: "",
@@ -54,14 +105,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 +121,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 +152,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 +181,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

@@ -0,0 +1,27 @@
interface Child {
bed: string
age: number
}
interface Room {
adults: number
roomtypecode: string
ratecode: string
child: Child[]
}
export interface BookingData {
hotel: string
fromdate: string
todate: string
room: Room[]
}
export type RoomsData = {
roomType: string
memberPrice: string | undefined
publicPrice: string | undefined
adults: number
children: Child[]
cancellationText: string | undefined
}

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

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)
}