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 { StepEnum } from "@/types/components/enterDetails/step"
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import type { LangParams, LayoutArgs } from "@/types/params"
export default async function StepLayout({
@@ -19,19 +19,18 @@ export default async function StepLayout({
LayoutArgs<LangParams & { step: StepEnum }> & {
hotelHeader: React.ReactNode
sidePeek: React.ReactNode
}
>) {
}>) {
setLang(params.lang)
preload()
return (
<EnterDetailsProvider step={params.step}>
<EnterDetailsProvider step={params.step} >
<main className={styles.layout}>
{hotelHeader}
<div className={styles.content}>
<SelectedRoom />
{children}
<aside className={styles.summary}>
<Summary />
<Summary isMember={false} />
</aside>
</div>
{sidePeek}

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,18 +14,17 @@ import { bedTypeSchema } from "./schema"
import styles from "./bedOptions.module.css"
import type { BedTypeSchema } from "@/types/components/enterDetails/bedType"
import { BedTypeEnum } from "@/types/enums/bedType"
import type { BedTypeSchema } from "@/types/components/hotelReservation/enterDetails/bedType"
export default function BedType() {
const intl = useIntl()
const bedType = useEnterDetailsStore((state) => state.data.bedType)
const bedType = useEnterDetailsStore((state) => state.userData.bedType)
const methods = useForm<BedTypeSchema>({
defaultValues: bedType
? {
bedType,
}
bedType,
}
: undefined,
criteriaMode: "all",
mode: "all",

View File

@@ -17,13 +17,13 @@ import styles from "./breakfast.module.css"
import type {
BreakfastFormSchema,
BreakfastProps,
} from "@/types/components/enterDetails/breakfast"
} from "@/types/components/hotelReservation/enterDetails/breakfast"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
export default function Breakfast({ packages }: BreakfastProps) {
const intl = useIntl()
const breakfast = useEnterDetailsStore((state) => state.data.breakfast)
const breakfast = useEnterDetailsStore((state) => state.userData.breakfast)
let defaultValues = undefined
if (breakfast === BreakfastPackageEnum.NO_BREAKFAST) {
@@ -76,21 +76,21 @@ export default function Breakfast({ packages }: BreakfastProps) {
subtitle={
pkg.code === BreakfastPackageEnum.FREE_MEMBER_BREAKFAST
? intl.formatMessage<React.ReactNode>(
{ id: "breakfast.price.free" },
{
amount: pkg.originalPrice,
currency: pkg.currency,
free: (str) => <Highlight>{str}</Highlight>,
strikethrough: (str) => <s>{str}</s>,
}
)
{ id: "breakfast.price.free" },
{
amount: pkg.originalPrice,
currency: pkg.currency,
free: (str) => <Highlight>{str}</Highlight>,
strikethrough: (str) => <s>{str}</s>,
}
)
: intl.formatMessage(
{ id: "breakfast.price" },
{
amount: pkg.packagePrice,
currency: pkg.currency,
}
)
{ id: "breakfast.price" },
{
amount: pkg.packagePrice,
currency: pkg.currency,
}
)
}
text={intl.formatMessage({
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 {
DetailsProps,
DetailsSchema,
} from "@/types/components/enterDetails/details"
} from "@/types/components/hotelReservation/enterDetails/details"
const formID = "enter-details"
export default function Details({ user }: DetailsProps) {
const intl = useIntl()
const initialData = useEnterDetailsStore((state) => ({
countryCode: state.data.countryCode,
email: state.data.email,
firstName: state.data.firstName,
lastName: state.data.lastName,
phoneNumber: state.data.phoneNumber,
join: state.data.join,
dateOfBirth: state.data.dateOfBirth,
zipCode: state.data.zipCode,
termsAccepted: state.data.termsAccepted,
countryCode: state.userData.countryCode,
email: state.userData.email,
firstName: state.userData.firstName,
lastName: state.userData.lastName,
phoneNumber: state.userData.phoneNumber,
join: state.userData.join,
dateOfBirth: state.userData.dateOfBirth,
zipCode: state.userData.zipCode,
termsAccepted: state.userData.termsAccepted,
}))
const methods = useForm<DetailsSchema>({

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
import { ChevronRightSmallIcon } from "@/components/Icons"
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() {
const intl = useIntl()

View File

@@ -1,154 +1,206 @@
"use client"
import { useIntl } from "react-intl"
import { dt } from "@/lib/dt"
import { trpc } from "@/lib/trpc/client"
import { useEnterDetailsStore } from "@/stores/enter-details"
import { ArrowRightIcon } from "@/components/Icons"
import LoadingSpinner from "@/components/LoadingSpinner"
import Divider from "@/components/TempDesignSystem/Divider"
import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import ToggleSidePeek from "./ToggleSidePeek"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import useLang from "@/hooks/useLang"
import { formatNumber } from "@/utils/format"
import styles from "./summary.module.css"
// TEMP
const rooms = [
{
adults: 1,
type: "Cozy cabin",
},
]
import { RoomsData } from "@/types/components/hotelReservation/enterDetails/bookingData"
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")
export default function Summary({ isMember }: { isMember: boolean }) {
const intl = useIntl()
const lang = useLang()
const { fromDate, toDate, rooms, hotel, bedType, breakfast } =
useEnterDetailsStore((state) => ({
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 adults = intl.formatMessage(
{ id: "booking.adults" },
{ totalAdults: totalAdults }
const {
data: availabilityData,
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(
{ id: "booking.nights" },
{ totalNights: diff }
)
const addOns = [
{
price: intl.formatMessage({ id: "Included" }),
title: intl.formatMessage({ id: "King bed" }),
},
{
price: intl.formatMessage({ id: "Included" }),
title: intl.formatMessage({ id: "Breakfast buffet" }),
},
]
if (isLoading) {
return <LoadingSpinner />
}
const populatedRooms = rooms
.map((room) => {
const chosenRoom = availabilityData?.roomConfigurations.find(
(availRoom) => room.roomtypecode === availRoom.roomTypeCode
)
const cancellationText = availabilityData?.rateDefinitions.find(
(rate) => rate.rateCode === room.ratecode
)?.cancellationText
const mappedRooms = Array.from(
rooms
.reduce((acc, room) => {
const currentRoom = acc.get(room.type)
acc.set(room.type, {
total: currentRoom ? currentRoom.total + 1 : 1,
type: room.type,
})
return acc
}, new Map())
.values()
)
if (chosenRoom) {
const memberPrice = chosenRoom.products.find(
(rate) => rate.productType.member?.rateCode === room.ratecode
)?.productType.member?.localPrice.pricePerStay
const publicPrice = chosenRoom.products.find(
(rate) => rate.productType.public?.rateCode === room.ratecode
)?.productType.public?.localPrice.pricePerStay
return {
roomType: chosenRoom.roomType,
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 (
<section className={styles.summary}>
<header>
<Body textTransform="bold">
{mappedRooms.map(
(room, idx) =>
`${room.total} x ${room.type}${mappedRooms.length > 1 && idx + 1 !== mappedRooms.length ? ", " : ""}`
)}
<Subtitle type="two">{intl.formatMessage({ id: "Summary" })}</Subtitle>
<Body className={styles.date} color="baseTextMediumContrast">
{dt(fromDate).locale(lang).format("ddd, D MMM")}
<ArrowRightIcon color="peach80" height={15} width={15} />
{dt(toDate).locale(lang).format("ddd, D MMM")} ({nights})
</Body>
<Body className={styles.date} color="textMediumContrast">
{fromDate}
<ArrowRightIcon color="uiTextMediumContrast" height={15} width={15} />
{toDate}
</Body>
<ToggleSidePeek />
</header>
<Divider color="primaryLightSubtle" />
<div className={styles.addOns}>
<div className={styles.entry}>
<Caption color="uiTextMediumContrast">
{`${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>
{populatedRooms.map((room, idx) => (
<RoomBreakdown key={idx} room={room} isMember={isMember} />
))}
{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>
<Divider color="primaryLightSubtle" />
<div className={styles.total}>
<div>
<div className={styles.entry}>
<Body textTransform="bold">
{intl.formatMessage({ id: "Total incl VAT" })}
</Body>
<Body textTransform="bold">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "4686", 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: "455", currency: "EUR" }
)}
</Caption>
</div>
<div className={styles.entry}>
<Body textTransform="bold">
{intl.formatMessage({ id: "Total price (incl VAT)" })}
</Body>
<Body textTransform="bold">
{intl.formatMessage(
{ id: "{amount} {currency}" },
{ amount: "4686", currency: "SEK" }
)}
</Body>
</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>
</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>
</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 {
display: flex;
flex-direction: column;
gap: var(--Spacing-x1);
gap: var(--Spacing-x-one-and-half);
}
.entry {

View File

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

View File

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

View File

@@ -313,8 +313,8 @@
"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.",
"Total Points": "Samlet antal point",
"Total incl VAT": "Inkl. moms",
"Total price": "Samlet pris",
"Total price (incl VAT)": "Samlet pris (inkl. moms)",
"Tourist": "Turist",
"Transaction date": "Overførselsdato",
"Transactions": "Transaktioner",

View File

@@ -312,8 +312,8 @@
"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.",
"Total Points": "Gesamtpunktzahl",
"Total incl VAT": "Gesamt inkl. MwSt.",
"Total price": "Gesamtpreis",
"Total price (incl VAT)": "Gesamtpreis (inkl. MwSt.)",
"Tourist": "Tourist",
"Transaction date": "Transaktionsdatum",
"Transactions": "Transaktionen",

View File

@@ -326,10 +326,10 @@
"There are no transactions to display": "There are no transactions to display",
"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.",
"Total Points": "Total Points",
"Total cost": "Total cost",
"Total incl VAT": "Total incl VAT",
"Total price": "Total price",
"Total Points": "Total Points",
"Total price (incl VAT)": "Total price (incl VAT)",
"Tourist": "Tourist",
"Transaction date": "Transaction date",
"Transactions": "Transactions",

View File

@@ -314,8 +314,8 @@
"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ä.",
"Total Points": "Kokonaispisteet",
"Total incl VAT": "Yhteensä sis. alv",
"Total price": "Kokonaishinta",
"Total price (incl VAT)": "Kokonaishinta (sis. ALV)",
"Tourist": "Turisti",
"Transaction date": "Tapahtuman päivämäärä",
"Transactions": "Tapahtumat",

View File

@@ -1,30 +1,34 @@
import { produce } from "immer"
import { ReadonlyURLSearchParams } from "next/navigation"
import { createContext, useContext } from "react"
import { create, useStore } from "zustand"
import { bedTypeSchema } from "@/components/HotelReservation/EnterDetails/BedType/schema"
import { breakfastStoreSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
import { detailsSchema } from "@/components/HotelReservation/EnterDetails/Details/schema"
import getHotelReservationQueryParams from "@/components/HotelReservation/SelectRate/RoomSelection/utils"
import { BreakfastPackage } from "@/types/components/enterDetails/breakfast"
import { DetailsSchema } from "@/types/components/enterDetails/details"
import { SidePeekEnum } from "@/types/components/enterDetails/sidePeek"
import { StepEnum } from "@/types/components/enterDetails/step"
import { BreakfastPackage } from "@/types/components/hotelReservation/enterDetails/breakfast"
import { SidePeekEnum } from "@/types/components/hotelReservation/enterDetails/sidePeek"
import { StepEnum } from "@/types/components/hotelReservation/enterDetails/step"
import { BedTypeEnum } from "@/types/enums/bedType"
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"
interface EnterDetailsState {
data: {
userData: {
bedType: BedTypeEnum | undefined
breakfast: BreakfastPackage | BreakfastPackageEnum.NO_BREAKFAST | undefined
} & DetailsSchema
roomData: BookingData
steps: StepEnum[]
currentStep: StepEnum
activeSidePeek: SidePeekEnum | null
isValid: Record<StepEnum, boolean>
completeStep: (updatedData: Partial<EnterDetailsState["data"]>) => void
completeStep: (updatedData: Partial<EnterDetailsState["userData"]>) => void
navigate: (
step: StepEnum,
updatedData?: Record<string, string | boolean | BreakfastPackage>
@@ -34,13 +38,60 @@ interface EnterDetailsState {
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 sessionData = isBrowser
? sessionStorage.getItem(SESSION_STORAGE_KEY)
: 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,
breakfast: undefined,
countryCode: "",
@@ -54,14 +105,14 @@ export function initEditDetailsState(currentStep: StepEnum) {
termsAccepted: false,
}
let inputData = {}
let inputUserData = {}
if (sessionData) {
inputData = JSON.parse(sessionData)
inputUserData = JSON.parse(sessionData)
}
const validPaths = [StepEnum.selectBed]
let initialData: EnterDetailsState["data"] = defaultData
let initialData: EnterDetailsState["userData"] = defaultUserData
const isValid = {
[StepEnum.selectBed]: false,
@@ -70,19 +121,19 @@ export function initEditDetailsState(currentStep: StepEnum) {
[StepEnum.payment]: false,
}
const validatedBedType = bedTypeSchema.safeParse(inputData)
const validatedBedType = bedTypeSchema.safeParse(inputUserData)
if (validatedBedType.success) {
validPaths.push(StepEnum.breakfast)
initialData = { ...initialData, ...validatedBedType.data }
isValid[StepEnum.selectBed] = true
}
const validatedBreakfast = breakfastStoreSchema.safeParse(inputData)
const validatedBreakfast = breakfastStoreSchema.safeParse(inputUserData)
if (validatedBreakfast.success) {
validPaths.push(StepEnum.details)
initialData = { ...initialData, ...validatedBreakfast.data }
isValid[StepEnum.breakfast] = true
}
const validatedDetails = detailsSchema.safeParse(inputData)
const validatedDetails = detailsSchema.safeParse(inputUserData)
if (validatedDetails.success) {
validPaths.push(StepEnum.payment)
initialData = { ...initialData, ...validatedDetails.data }
@@ -101,7 +152,8 @@ export function initEditDetailsState(currentStep: StepEnum) {
}
return create<EnterDetailsState>()((set, get) => ({
data: initialData,
userData: initialData,
roomData,
steps: Object.values(StepEnum),
setCurrentStep: (step) => set({ currentStep: step }),
navigate: (step, updatedData) =>
@@ -129,14 +181,17 @@ export function initEditDetailsState(currentStep: StepEnum) {
isValid,
completeStep: (updatedData) =>
set(
produce((state) => {
produce((state: EnterDetailsState) => {
state.isValid[state.currentStep] = true
const nextStep =
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
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 {
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)
}