Merged in feat/SW-1261 (pull request #1263)

feat: only show member price when logged in

* feat: only show member price when logged in


Approved-by: Michael Zetterberg
This commit is contained in:
Simon.Emanuelsson
2025-02-07 08:51:50 +00:00
parent c0f5c0278b
commit c204532acc
27 changed files with 479 additions and 238 deletions

View File

@@ -55,7 +55,6 @@ export default async function StepPage({
// Deleting step to avoid double searchparams after rewrite // Deleting step to avoid double searchparams after rewrite
selectRoomParams.delete("step") selectRoomParams.delete("step")
const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams) const booking = convertSearchParamsToObj<SelectRateSearchParams>(searchParams)
const { const {
hotelId, hotelId,
rooms: [ rooms: [
@@ -153,8 +152,8 @@ export default async function StepPage({
} }
: undefined : undefined
const arrivalDate = new Date(searchParams.fromdate) const arrivalDate = new Date(fromDate)
const departureDate = new Date(searchParams.todate) const departureDate = new Date(toDate)
const hotelAttributes = hotelData?.hotel const hotelAttributes = hotelData?.hotel
const initialHotelsTrackingData: TrackingSDKHotelInfo = { const initialHotelsTrackingData: TrackingSDKHotelInfo = {

View File

@@ -2,10 +2,11 @@ import "@/app/globals.css"
import "@scandic-hotels/design-system/style.css" import "@scandic-hotels/design-system/style.css"
import Script from "next/script" import Script from "next/script"
import { SessionProvider } from "next-auth/react"
import TrpcProvider from "@/lib/trpc/Provider" import TrpcProvider from "@/lib/trpc/Provider"
import TokenRefresher from "@/components/Auth/TokenRefresher" import { SessionRefresher } from "@/components/Auth/TokenRefresher"
import CookieBotConsent from "@/components/CookieBot" import CookieBotConsent from "@/components/CookieBot"
import StorageCleaner from "@/components/HotelReservation/EnterDetails/StorageCleaner" import StorageCleaner from "@/components/HotelReservation/EnterDetails/StorageCleaner"
import { ToastHandler } from "@/components/TempDesignSystem/Toasts" import { ToastHandler } from "@/components/TempDesignSystem/Toasts"
@@ -56,20 +57,22 @@ export default async function RootLayout({
`}</Script> `}</Script>
</head> </head>
<body> <body>
<ServerIntlProvider intl={{ defaultLocale, locale, messages }}> <SessionProvider basePath="/api/web/auth">
<TrpcProvider> <ServerIntlProvider intl={{ defaultLocale, locale, messages }}>
<RouterTracking /> <TrpcProvider>
{sitewidealert} <RouterTracking />
{header} {sitewidealert}
{bookingwidget} {header}
{children} {bookingwidget}
{footer} {children}
<ToastHandler /> {footer}
<TokenRefresher /> <ToastHandler />
<StorageCleaner /> <SessionRefresher />
<CookieBotConsent /> <StorageCleaner />
</TrpcProvider> <CookieBotConsent />
</ServerIntlProvider> </TrpcProvider>
</ServerIntlProvider>
</SessionProvider>
</body> </body>
</html> </html>
) )

View File

@@ -15,12 +15,12 @@ import {
export default function TokenRefresher() { export default function TokenRefresher() {
return ( return (
<SessionProvider basePath="/api/web/auth"> <SessionProvider basePath="/api/web/auth">
<Refresher /> <SessionRefresher />
</SessionProvider> </SessionProvider>
) )
} }
function Refresher() { export function SessionRefresher() {
const session = useSession() const session = useSession()
const pathname = usePathname() const pathname = usePathname()
const searchParams = useSearchParams() const searchParams = useSearchParams()

View File

@@ -49,9 +49,6 @@ export default function Receipt({
<Body color="uiTextHighContrast">{room.name}</Body> <Body color="uiTextHighContrast">{room.name}</Body>
{booking.rateDefinition.isMemberRate ? ( {booking.rateDefinition.isMemberRate ? (
<div className={styles.memberPrice}> <div className={styles.memberPrice}>
<Body color="uiTextPlaceholder">
<s>{intl.formatMessage({ id: "N/A" })}</s>
</Body>
<Body color="red"> <Body color="red">
{formatPrice(intl, booking.roomPrice, booking.currencyCode)} {formatPrice(intl, booking.roomPrice, booking.currencyCode)}
</Body> </Body>

View File

@@ -33,8 +33,9 @@ import type { Lang } from "@/constants/languages"
function HotelCard({ function HotelCard({
hotel, hotel,
type = HotelCardListingTypeEnum.PageListing, isUserLoggedIn,
state = "default", state = "default",
type = HotelCardListingTypeEnum.PageListing,
}: HotelCardProps) { }: HotelCardProps) {
const params = useParams() const params = useParams()
const lang = params.lang as Lang const lang = params.lang as Lang
@@ -160,7 +161,7 @@ function HotelCard({
<NoPriceAvailableCard /> <NoPriceAvailableCard />
) : ( ) : (
<> <>
{price.public && ( {!isUserLoggedIn && price.public && (
<HotelPriceCard productTypePrices={price.public} /> <HotelPriceCard productTypePrices={price.public} />
)} )}
{price.member && ( {price.member && (

View File

@@ -1,3 +1,5 @@
"use client"
import { useSession } from "next-auth/react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { selectRate } from "@/constants/routes/hotelReservation" import { selectRate } from "@/constants/routes/hotelReservation"
@@ -5,7 +7,6 @@ import { selectRate } from "@/constants/routes/hotelReservation"
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
import Link from "@/components/TempDesignSystem/Link" import Link from "@/components/TempDesignSystem/Link"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption" import Caption from "@/components/TempDesignSystem/Text/Caption"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
@@ -31,6 +32,8 @@ export default function ListingHotelCardDialog({
setImageError, setImageError,
}: ListingHotelCardProps) { }: ListingHotelCardProps) {
const intl = useIntl() const intl = useIntl()
const { data: session } = useSession()
const isUserLoggedIn = !!session
const { const {
name, name,
publicPrice, publicPrice,
@@ -85,12 +88,14 @@ export default function ListingHotelCardDialog({
{intl.formatMessage({ id: "Per night from" })} {intl.formatMessage({ id: "Per night from" })}
</Caption> </Caption>
<div className={styles.listingPrices}> <div className={styles.listingPrices}>
{publicPrice && ( {publicPrice && !isUserLoggedIn && (
<Subtitle type="two"> <>
{publicPrice} {currency} <Subtitle type="two">
</Subtitle> {publicPrice} {currency}
</Subtitle>
{memberPrice && <Caption>/</Caption>}
</>
)} )}
{publicPrice && memberPrice && <Caption>/</Caption>}
{memberPrice && ( {memberPrice && (
<Subtitle type="two" color="red" className={styles.memberPrice}> <Subtitle type="two" color="red" className={styles.memberPrice}>
{intl.formatMessage( {intl.formatMessage(

View File

@@ -1,3 +1,5 @@
"use client"
import { useSession } from "next-auth/react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { selectRate } from "@/constants/routes/hotelReservation" import { selectRate } from "@/constants/routes/hotelReservation"
@@ -32,6 +34,8 @@ export default function StandaloneHotelCardDialog({
setImageError, setImageError,
}: StandaloneHotelCardProps) { }: StandaloneHotelCardProps) {
const intl = useIntl() const intl = useIntl()
const { data: session } = useSession()
const isUserLoggedIn = !!session
const { const {
name, name,
publicPrice, publicPrice,
@@ -86,7 +90,7 @@ export default function StandaloneHotelCardDialog({
<Caption type="bold"> <Caption type="bold">
{intl.formatMessage({ id: "From" })} {intl.formatMessage({ id: "From" })}
</Caption> </Caption>
{publicPrice && ( {publicPrice && !isUserLoggedIn && (
<Subtitle type="two"> <Subtitle type="two">
{intl.formatMessage( {intl.formatMessage(
{ id: "{price} {currency}" }, { id: "{price} {currency}" },

View File

@@ -1,5 +1,6 @@
"use client" "use client"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { useSession } from "next-auth/react"
import { useEffect, useMemo } from "react" import { useEffect, useMemo } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
@@ -26,6 +27,8 @@ export default function HotelCardListing({
hotelData, hotelData,
type = HotelCardListingTypeEnum.PageListing, type = HotelCardListingTypeEnum.PageListing,
}: HotelCardListingProps) { }: HotelCardListingProps) {
const { data: session } = useSession()
const isUserLoggedIn = !!session
const searchParams = useSearchParams() const searchParams = useSearchParams()
const activeFilters = useHotelFilterStore((state) => state.activeFilters) const activeFilters = useHotelFilterStore((state) => state.activeFilters)
const setResultCount = useHotelFilterStore((state) => state.setResultCount) const setResultCount = useHotelFilterStore((state) => state.setResultCount)
@@ -71,10 +74,11 @@ export default function HotelCardListing({
> >
<HotelCard <HotelCard
hotel={hotel} hotel={hotel}
type={type} isUserLoggedIn={isUserLoggedIn}
state={ state={
hotel.hotelData.name === activeHotelCard ? "active" : "default" hotel.hotelData.name === activeHotelCard ? "active" : "default"
} }
type={type}
/> />
</div> </div>
)) ))

View File

@@ -10,12 +10,12 @@ import type { FilterValues } from "@/types/components/hotelReservation/selectRat
import type { RoomSelectionPanelProps } from "@/types/components/hotelReservation/selectRate/roomSelection" import type { RoomSelectionPanelProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
export function RoomSelectionPanel({ export function RoomSelectionPanel({
roomCategories,
availablePackages, availablePackages,
selectedPackages,
hotelType,
defaultPackages, defaultPackages,
hotelType,
roomCategories,
roomListIndex, roomListIndex,
selectedPackages,
}: RoomSelectionPanelProps) { }: RoomSelectionPanelProps) {
const searchParams = useSearchParams() const searchParams = useSearchParams()
const { getRooms } = useRoomFilteringStore() const { getRooms } = useRoomFilteringStore()
@@ -42,12 +42,12 @@ export function RoomSelectionPanel({
/> />
{rooms && ( {rooms && (
<RoomTypeList <RoomTypeList
roomsAvailability={rooms}
roomCategories={roomCategories}
availablePackages={availablePackages} availablePackages={availablePackages}
selectedPackages={selectedPackages}
hotelType={hotelType} hotelType={hotelType}
roomCategories={roomCategories}
roomListIndex={roomListIndex} roomListIndex={roomListIndex}
roomsAvailability={rooms}
selectedPackages={selectedPackages}
/> />
)} )}
</> </>

View File

@@ -14,6 +14,7 @@ import styles from "./priceList.module.css"
import type { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" import type { PriceListProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
export default function PriceList({ export default function PriceList({
isUserLoggedIn,
publicPrice = {}, publicPrice = {},
memberPrice = {}, memberPrice = {},
petRoomPackage, petRoomPackage,
@@ -59,37 +60,41 @@ export default function PriceList({
return ( return (
<dl className={styles.priceList}> <dl className={styles.priceList}>
<div className={styles.priceRow}> {isUserLoggedIn ? null : (
<dt> <div className={styles.priceRow}>
<Caption <dt>
type="bold" <Caption
color={ type="bold"
totalPublicLocalPricePerNight ? "uiTextHighContrast" : "disabled" color={
} totalPublicLocalPricePerNight
> ? "uiTextHighContrast"
{intl.formatMessage({ id: "Standard price" })} : "disabled"
</Caption> }
</dt> >
<dd> {intl.formatMessage({ id: "Standard price" })}
{publicLocalPrice ? ( </Caption>
<div className={styles.price}> </dt>
<Subtitle type="two" color="uiTextHighContrast"> <dd>
{totalPublicLocalPricePerNight} {publicLocalPrice ? (
<div className={styles.price}>
<Subtitle type="two" color="uiTextHighContrast">
{totalPublicLocalPricePerNight}
</Subtitle>
<Body color="uiTextHighContrast" textTransform="bold">
{publicLocalPrice.currency}
<span className={styles.perNight}>
/{intl.formatMessage({ id: "night" })}
</span>
</Body>
</div>
) : (
<Subtitle type="two" color="baseTextDisabled">
{intl.formatMessage({ id: "N/A" })}
</Subtitle> </Subtitle>
<Body color="uiTextHighContrast" textTransform="bold"> )}
{publicLocalPrice.currency} </dd>
<span className={styles.perNight}> </div>
/{intl.formatMessage({ id: "night" })} )}
</span>
</Body>
</div>
) : (
<Subtitle type="two" color="baseTextDisabled">
{intl.formatMessage({ id: "N/A" })}
</Subtitle>
)}
</dd>
</div>
<div className={styles.priceRow}> <div className={styles.priceRow}>
<dt> <dt>
@@ -126,14 +131,22 @@ export default function PriceList({
</dt> </dt>
<dd> <dd>
<Caption color="uiTextMediumContrast"> <Caption color="uiTextMediumContrast">
{intl.formatMessage( {isUserLoggedIn
{ id: "{publicPrice}/{memberPrice} {currency}" }, ? intl.formatMessage(
{ { id: "{memberPrice} {currency}" },
publicPrice: totalPublicRequestedPricePerNight, {
memberPrice: totalMemberRequestedPricePerNight, memberPrice: totalMemberRequestedPricePerNight,
currency: publicRequestedPrice.currency, currency: publicRequestedPrice.currency,
} }
)} )
: intl.formatMessage(
{ id: "{publicPrice}/{memberPrice} {currency}" },
{
publicPrice: totalPublicRequestedPricePerNight,
memberPrice: totalMemberRequestedPricePerNight,
currency: publicRequestedPrice.currency,
}
)}
</Caption> </Caption>
</dd> </dd>
</div> </div>

View File

@@ -16,13 +16,14 @@ import styles from "./flexibilityOption.module.css"
import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption" import type { FlexibilityOptionProps } from "@/types/components/hotelReservation/selectRate/flexibilityOption"
export default function FlexibilityOption({ export default function FlexibilityOption({
product, handleSelect,
name, isSelected,
isUserLoggedIn,
paymentTerm, paymentTerm,
priceInformation, priceInformation,
petRoomPackage, petRoomPackage,
isSelected, product,
onSelect, title,
}: FlexibilityOptionProps) { }: FlexibilityOptionProps) {
const intl = useIntl() const intl = useIntl()
@@ -32,7 +33,7 @@ export default function FlexibilityOption({
<div className={styles.header}> <div className={styles.header}>
<InfoCircleIcon width={16} height={16} color="uiTextMediumContrast" /> <InfoCircleIcon width={16} height={16} color="uiTextMediumContrast" />
<div className={styles.priceType}> <div className={styles.priceType}>
<Caption>{name}</Caption> <Caption>{title}</Caption>
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption> <Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
</div> </div>
</div> </div>
@@ -47,6 +48,10 @@ export default function FlexibilityOption({
const { public: publicPrice, member: memberPrice } = product.productType const { public: publicPrice, member: memberPrice } = product.productType
function handleOnSelect() {
handleSelect(publicPrice.rateCode, title, paymentTerm)
}
return ( return (
<label> <label>
<input <input
@@ -54,7 +59,7 @@ export default function FlexibilityOption({
name={`rateCode-${product.productType.public.rateCode}`} name={`rateCode-${product.productType.public.rateCode}`}
value={publicPrice?.rateCode} value={publicPrice?.rateCode}
checked={isSelected} checked={isSelected}
onChange={onSelect} onChange={handleOnSelect}
/> />
<div className={styles.card}> <div className={styles.card}>
<div className={styles.header}> <div className={styles.header}>
@@ -68,7 +73,7 @@ export default function FlexibilityOption({
/> />
</Button> </Button>
} }
title={name} title={title}
subtitle={paymentTerm} subtitle={paymentTerm}
> >
<div className={styles.terms}> <div className={styles.terms}>
@@ -90,11 +95,12 @@ export default function FlexibilityOption({
</div> </div>
</Modal> </Modal>
<div className={styles.priceType}> <div className={styles.priceType}>
<Caption color="uiTextHighContrast">{name}</Caption> <Caption color="uiTextHighContrast">{title}</Caption>
<Caption color="uiTextPlaceholder">({paymentTerm})</Caption> <Caption color="uiTextPlaceholder">({paymentTerm})</Caption>
</div> </div>
</div> </div>
<PriceTable <PriceTable
isUserLoggedIn={isUserLoggedIn}
publicPrice={publicPrice} publicPrice={publicPrice}
memberPrice={memberPrice} memberPrice={memberPrice}
petRoomPackage={petRoomPackage} petRoomPackage={petRoomPackage}

View File

@@ -1,6 +1,7 @@
"use client" "use client"
import { useSearchParams } from "next/navigation" import { useSearchParams } from "next/navigation"
import { useSession } from "next-auth/react"
import { createElement, useCallback, useEffect, useMemo } from "react" import { createElement, useCallback, useEffect, useMemo } from "react"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
@@ -20,10 +21,42 @@ import { cardVariants } from "./cardVariants"
import styles from "./roomCard.module.css" import styles from "./roomCard.module.css"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard" import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { HotelTypeEnum } from "@/types/enums/hotelType" import { HotelTypeEnum } from "@/types/enums/hotelType"
import type { RateDefinition } from "@/types/trpc/routers/hotel/roomAvailability" import type { Product } from "@/types/trpc/routers/hotel/roomAvailability"
function getBreakfastMessage(
publicBreakfastIncluded: boolean,
memberBreakfastIncluded: boolean,
hotelType: string | undefined,
userIsLoggedIn: boolean,
msgs: Record<"included" | "noSelection" | "scandicgo" | "notIncluded", string>
) {
if (hotelType === HotelTypeEnum.ScandicGo) {
return msgs.scandicgo
}
if (userIsLoggedIn && memberBreakfastIncluded) {
return msgs.included
}
if (publicBreakfastIncluded && memberBreakfastIncluded) {
return msgs.included
}
/** selected and rate does not include breakfast */
if (false) {
return msgs.notIncluded
}
if (!publicBreakfastIncluded && !memberBreakfastIncluded) {
return msgs.notIncluded
}
return msgs.noSelection
}
export default function RoomCard({ export default function RoomCard({
hotelId, hotelId,
@@ -35,66 +68,72 @@ export default function RoomCard({
packages, packages,
roomListIndex, roomListIndex,
}: RoomCardProps) { }: RoomCardProps) {
const { data: session } = useSession()
const isUserLoggedIn = !!session
const intl = useIntl() const intl = useIntl()
const searchParams = useSearchParams() const searchParams = useSearchParams()
const { selectRate, selectedRates } = useRateSelectionStore() const { selectRate, selectedRates } = useRateSelectionStore((state) => ({
selectRate: state.selectRate,
selectedRates: state.selectedRates,
}))
const selectedRate = useRateSelectionStore( const selectedRate = useRateSelectionStore(
(state) => state.selectedRates[roomListIndex] (state) => state.selectedRates[roomListIndex]
) )
const classNames = cardVariants({
availability:
roomConfiguration.status === AvailabilityEnum.NotAvailable
? "noAvailability"
: "default",
})
const breakfastMessages = {
included: intl.formatMessage({ id: "Breakfast is included." }),
notIncluded: intl.formatMessage({
id: "Breakfast selection in next step.",
}),
noSelection: intl.formatMessage({ id: "Select a rate" }),
scandicgo: intl.formatMessage({
id: "Breakfast deal can be purchased at the hotel.",
}),
}
const breakfastMessage = getBreakfastMessage(
roomConfiguration.breakfastIncludedInAllRatesPublic,
roomConfiguration.breakfastIncludedInAllRatesMember,
hotelType,
isUserLoggedIn,
breakfastMessages
)
const rates = useMemo( const rates = useMemo(
() => ({ () => ({
saveRate: rateDefinitions.find( save: rateDefinitions.filter(
(rate) => rate.cancellationRule === "NotCancellable" (rate) => rate.cancellationRule === "NotCancellable"
), ),
changeRate: rateDefinitions.find( change: rateDefinitions.filter(
(rate) => rate.cancellationRule === "Changeable" (rate) => rate.cancellationRule === "Changeable"
), ),
flexRate: rateDefinitions.find( flex: rateDefinitions.filter(
(rate) => rate.cancellationRule === "CancellableBefore6PM" (rate) => rate.cancellationRule === "CancellableBefore6PM"
), ),
}), }),
[rateDefinitions] [rateDefinitions]
) )
function findProductForRate(rate: RateDefinition | undefined) {
return rate
? roomConfiguration.products.find(
(product) =>
product.productType.public?.rateCode === rate.rateCode ||
product.productType.member?.rateCode === rate.rateCode
)
: undefined
}
function getRateDefinitionForRate(rate: RateDefinition | undefined) {
return rateDefinitions.find((def) => def.rateCode === rate?.rateCode)
}
const getBreakfastMessage = (rate: RateDefinition | undefined) => {
if (hotelType === HotelTypeEnum.ScandicGo) {
return intl.formatMessage({
id: "Breakfast deal can be purchased at the hotel.",
})
}
return getRateDefinitionForRate(rate)?.breakfastIncluded
? intl.formatMessage({ id: "Breakfast is included." })
: intl.formatMessage({ id: "Breakfast selection in next step." })
}
const petRoomPackage = const petRoomPackage =
(selectedPackages?.includes(RoomPackageCodeEnum.PET_ROOM) && (selectedPackages?.includes(RoomPackageCodeEnum.PET_ROOM) &&
packages?.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) || packages?.find((pkg) => pkg.code === RoomPackageCodeEnum.PET_ROOM)) ||
undefined undefined
const selectedRoom = roomCategories.find((roomCategory) => const selectedRoom = roomCategories.find((roomCategory) =>
roomCategory.roomTypes.some( roomCategory.roomTypes.find(
(roomType) => roomType.code === roomConfiguration.roomTypeCode (roomType) => roomType.code === roomConfiguration.roomTypeCode
) )
) )
const { name, roomSize, totalOccupancy, images } = selectedRoom || {} const { name, roomSize, totalOccupancy, images } = selectedRoom || {}
const galleryImages = mapApiImagesToGalleryImages(images || [])
const freeCancelation = intl.formatMessage({ id: "Free cancellation" }) const freeCancelation = intl.formatMessage({ id: "Free cancellation" })
const nonRefundable = intl.formatMessage({ id: "Non-refundable" }) const nonRefundable = intl.formatMessage({ id: "Non-refundable" })
@@ -102,27 +141,74 @@ export default function RoomCard({
const payLater = intl.formatMessage({ id: "Pay later" }) const payLater = intl.formatMessage({ id: "Pay later" })
const payNow = intl.formatMessage({ id: "Pay now" }) const payNow = intl.formatMessage({ id: "Pay now" })
const rateKey = useCallback( function handleRateSelection(
(key: string) => { rateCode: string,
rateName: string,
paymentTerm: string
) {
if (
selectedRates[roomListIndex]?.publicRateCode === rateCode &&
selectedRates[roomListIndex]?.roomTypeCode ===
roomConfiguration.roomTypeCode
) {
selectRate(roomListIndex, undefined)
} else {
selectRate(roomListIndex, {
publicRateCode: rateCode,
roomTypeCode: roomConfiguration.roomTypeCode,
name: rateName,
paymentTerm: paymentTerm,
})
}
}
const getRateInfo = useCallback(
(product: Product) => {
const publicRate = Object.keys(rates).find((k) =>
rates[k as keyof typeof rates].find(
(a) => a.rateCode === product.productType.public.rateCode
)
)
let memberRate
if (product.productType.member) {
memberRate = Object.keys(rates).find((k) =>
rates[k as keyof typeof rates].find(
(a) => a.rateCode === product.productType.member!.rateCode
)
)
}
if (!publicRate || !memberRate) {
throw new Error("We should never make it here without rateCodes")
}
const key = isUserLoggedIn ? memberRate : publicRate
switch (key) { switch (key) {
case "flexRate": case "change":
return freeCancelation return {
case "saveRate": isFlex: false,
return nonRefundable title: freeBooking,
}
case "flex":
return {
isFlex: true,
title: freeCancelation,
}
case "save":
return {
isFlex: false,
title: nonRefundable,
}
default: default:
return freeBooking throw new Error(
`Unknown key for rate, should be "change", "flex" or "save", but got ${key}`
)
} }
}, },
[freeCancelation, freeBooking, nonRefundable] [freeBooking, freeCancelation, isUserLoggedIn, nonRefundable, rates]
) )
const classNames = cardVariants({
availability:
roomConfiguration.status === "NotAvailable"
? "noAvailability"
: "default",
})
// Handle URL-based preselection // Handle URL-based preselection
useEffect(() => { useEffect(() => {
const ratecodeSearchParam = searchParams.get( const ratecodeSearchParam = searchParams.get(
@@ -138,55 +224,34 @@ export default function RoomCard({
const existingSelection = selectedRates[roomListIndex] const existingSelection = selectedRates[roomListIndex]
if (existingSelection) return if (existingSelection) return
const matchingRate = Object.entries(rates).find( const matchingRate = roomConfiguration.products.find(
([_, rate]) => (product) =>
rate?.rateCode === ratecodeSearchParam && product.productType.public.rateCode === ratecodeSearchParam &&
roomConfiguration.roomTypeCode === roomtypeSearchParam roomConfiguration.roomTypeCode === roomtypeSearchParam
) )
if (matchingRate) { if (matchingRate) {
const [key, rate] = matchingRate const rateInfo = getRateInfo(matchingRate)
selectRate(roomListIndex, { selectRate(roomListIndex, {
publicRateCode: rate?.rateCode ?? "", publicRateCode: matchingRate.productType.public.rateCode,
roomTypeCode: roomConfiguration.roomTypeCode, roomTypeCode: roomConfiguration.roomTypeCode,
name: rateKey(key), name: rateInfo.title,
paymentTerm: key === "flexRate" ? payLater : payNow, paymentTerm: rateInfo.isFlex ? payLater : payNow,
}) })
} }
}, [ }, [
searchParams, searchParams,
roomListIndex, roomListIndex,
rates, rates,
roomConfiguration.products,
roomConfiguration.roomTypeCode, roomConfiguration.roomTypeCode,
payLater, payLater,
payNow, payNow,
selectRate, selectRate,
selectedRates, selectedRates,
rateKey, getRateInfo,
]) ])
const handleRateSelection = (
rateCode: string,
rateName: string,
paymentTerm: string
) => {
if (
selectedRates[roomListIndex]?.publicRateCode === rateCode &&
selectedRates[roomListIndex]?.roomTypeCode ===
roomConfiguration.roomTypeCode
) {
selectRate(roomListIndex, undefined)
} else {
selectRate(roomListIndex, {
publicRateCode: rateCode,
roomTypeCode: roomConfiguration.roomTypeCode,
name: rateName,
paymentTerm: paymentTerm,
})
}
}
const galleryImages = mapApiImagesToGalleryImages(images || [])
return ( return (
<li className={classNames}> <li className={classNames}>
<div> <div>
@@ -274,11 +339,14 @@ export default function RoomCard({
*/} */}
</div> </div>
</div> </div>
<div className={styles.container}> <div className={styles.container}>
<Caption color="uiTextHighContrast" type="bold"> {roomConfiguration.status === AvailabilityEnum.Available ? (
{getBreakfastMessage(rates.flexRate)} <Caption color="uiTextHighContrast" type="bold">
</Caption> {breakfastMessage}
{roomConfiguration.status === "NotAvailable" ? ( </Caption>
) : null}
{roomConfiguration.status === AvailabilityEnum.NotAvailable ? (
<div className={styles.noRoomsContainer}> <div className={styles.noRoomsContainer}>
<div className={styles.noRooms}> <div className={styles.noRooms}>
<ErrorCircleIcon color="red" width={16} /> <ErrorCircleIcon color="red" width={16} />
@@ -290,29 +358,26 @@ export default function RoomCard({
</div> </div>
</div> </div>
) : ( ) : (
Object.entries(rates).map(([key, rate]) => ( roomConfiguration.products.map((product) => {
<FlexibilityOption const rate = getRateInfo(product)
key={`${roomListIndex}-${rate?.rateCode ?? key}-${selectedRate?.roomTypeCode ?? `${roomListIndex}-unselected`}`} return (
name={rateKey(key)} <FlexibilityOption
value={key.toLowerCase()} key={product.productType.public.rateCode}
paymentTerm={key === "flexRate" ? payLater : payNow} handleSelect={handleRateSelection}
product={findProductForRate(rate)} isSelected={
priceInformation={getRateDefinitionForRate(rate)?.generalTerms} selectedRate?.publicRateCode ===
roomTypeCode={roomConfiguration.roomTypeCode} product.productType.public.rateCode &&
petRoomPackage={petRoomPackage} selectedRate?.roomTypeCode === roomConfiguration.roomTypeCode
isSelected={ }
selectedRate?.publicRateCode === rate?.rateCode && isUserLoggedIn={isUserLoggedIn}
selectedRate?.roomTypeCode === roomConfiguration.roomTypeCode paymentTerm={rate.isFlex ? payLater : payNow}
} petRoomPackage={petRoomPackage}
onSelect={() => product={product}
handleRateSelection( roomTypeCode={roomConfiguration.roomTypeCode}
rate?.rateCode ?? "", title={rate.title}
rateKey(key), />
key === "flexRate" ? payLater : payNow )
) })
}
/>
))
)} )}
</div> </div>
</li> </li>

View File

@@ -7,12 +7,12 @@ import styles from "./roomSelection.module.css"
import type { RoomTypeListProps } from "@/types/components/hotelReservation/selectRate/roomSelection" import type { RoomTypeListProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
export default function RoomTypeList({ export default function RoomTypeList({
roomsAvailability,
roomCategories,
availablePackages, availablePackages,
selectedPackages,
hotelType, hotelType,
roomCategories,
roomListIndex, roomListIndex,
roomsAvailability,
selectedPackages,
}: RoomTypeListProps) { }: RoomTypeListProps) {
const { roomConfigurations, rateDefinitions } = roomsAvailability const { roomConfigurations, rateDefinitions } = roomsAvailability
@@ -21,15 +21,15 @@ export default function RoomTypeList({
<ul className={styles.roomList}> <ul className={styles.roomList}>
{roomConfigurations.map((roomConfiguration) => ( {roomConfigurations.map((roomConfiguration) => (
<RoomCard <RoomCard
key={roomConfiguration.roomTypeCode}
hotelId={roomsAvailability.hotelId.toString()} hotelId={roomsAvailability.hotelId.toString()}
hotelType={hotelType} hotelType={hotelType}
rateDefinitions={rateDefinitions}
roomConfiguration={roomConfiguration}
roomCategories={roomCategories}
selectedPackages={selectedPackages}
packages={availablePackages} packages={availablePackages}
key={roomConfiguration.roomTypeCode} rateDefinitions={rateDefinitions}
roomCategories={roomCategories}
roomConfiguration={roomConfiguration}
roomListIndex={roomListIndex} roomListIndex={roomListIndex}
selectedPackages={selectedPackages}
/> />
))} ))}
</ul> </ul>

View File

@@ -232,12 +232,12 @@ export default function Rooms({
</div> </div>
<div className={styles.roomSelectionPanel}> <div className={styles.roomSelectionPanel}>
<RoomSelectionPanel <RoomSelectionPanel
roomCategories={roomCategories}
availablePackages={availablePackages} availablePackages={availablePackages}
selectedPackages={selectedPackagesByRoom[index]}
hotelType={hotelType}
defaultPackages={defaultPackages} defaultPackages={defaultPackages}
hotelType={hotelType}
roomCategories={roomCategories}
roomListIndex={index} roomListIndex={index}
selectedPackages={selectedPackagesByRoom[index]}
/> />
</div> </div>
</div> </div>
@@ -246,12 +246,12 @@ export default function Rooms({
}) })
) : ( ) : (
<RoomSelectionPanel <RoomSelectionPanel
roomCategories={roomCategories}
availablePackages={availablePackages} availablePackages={availablePackages}
selectedPackages={selectedPackagesByRoom[0]}
hotelType={hotelType}
defaultPackages={defaultPackages} defaultPackages={defaultPackages}
hotelType={hotelType}
roomCategories={roomCategories}
roomListIndex={0} roomListIndex={0}
selectedPackages={selectedPackagesByRoom[0]}
/> />
)} )}

View File

@@ -659,6 +659,7 @@
"{difference}{amount} {currency}": "{difference}{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}",
"{distanceInKm} km": "{distanceInKm} km", "{distanceInKm} km": "{distanceInKm} km",
"{lowest} to {highest} persons": "{lowest} bis {highest} Personen", "{lowest} to {highest} persons": "{lowest} bis {highest} Personen",
"{memberPrice} {currency}": "{memberPrice} {currency}",
"{min} to {max} characters": "{min} til {max} tegn", "{min} to {max} characters": "{min} til {max} tegn",
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} tilgængelig", "{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} tilgængelig",
"{number} km to city center": "{number} km til centrum", "{number} km to city center": "{number} km til centrum",

View File

@@ -659,6 +659,7 @@
"{difference}{amount} {currency}": "{difference}{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}",
"{distanceInKm} km": "{distanceInKm} km", "{distanceInKm} km": "{distanceInKm} km",
"{lowest} to {highest} persons": "{lowest} til {highest} personer", "{lowest} to {highest} persons": "{lowest} til {highest} personer",
"{memberPrice} {currency}": "{memberPrice} {currency}",
"{min} to {max} characters": "{min} zu {max} figuren", "{min} to {max} characters": "{min} zu {max} figuren",
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} verfügbar", "{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} verfügbar",
"{number} km to city center": "{number} km zum Stadtzentrum", "{number} km to city center": "{number} km zum Stadtzentrum",

View File

@@ -660,6 +660,7 @@
"{difference}{amount} {currency}": "{difference}{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}",
"{distanceInKm} km": "{distanceInKm} km", "{distanceInKm} km": "{distanceInKm} km",
"{lowest} to {highest} persons": "{lowest} to {highest} persons", "{lowest} to {highest} persons": "{lowest} to {highest} persons",
"{memberPrice} {currency}": "{memberPrice} {currency}",
"{min} to {max} characters": "{min} to {max} characters", "{min} to {max} characters": "{min} to {max} characters",
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} available", "{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} available",
"{number} km to city center": "{number} km to city center", "{number} km to city center": "{number} km to city center",

View File

@@ -659,6 +659,7 @@
"{difference}{amount} {currency}": "{difference}{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}",
"{distanceInKm} km": "{distanceInKm} km", "{distanceInKm} km": "{distanceInKm} km",
"{lowest} to {highest} persons": "{lowest} - {highest} henkilöä", "{lowest} to {highest} persons": "{lowest} - {highest} henkilöä",
"{memberPrice} {currency}": "{memberPrice} {currency}",
"{min} to {max} characters": "{min} to {max} hahmoja", "{min} to {max} characters": "{min} to {max} hahmoja",
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} saatavilla", "{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} saatavilla",
"{number} km to city center": "{number} km Etäisyys kaupunkiin", "{number} km to city center": "{number} km Etäisyys kaupunkiin",

View File

@@ -26,6 +26,7 @@
"Address: {address}": "Adresse: {address}", "Address: {address}": "Adresse: {address}",
"Adults": "Voksne", "Adults": "Voksne",
"Age": "Alder", "Age": "Alder",
"{memberPrice} {currency}": "{memberPrice} {currency}",
"Airport": "Flyplass", "Airport": "Flyplass",
"All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.", "All our breakfast buffets offer gluten free, vegan, and allergy-friendly options.": "Alle våre frokostbufféer tilbyr glutenfrie, veganske og allergivennlige alternativer.",
"Allergy-friendly room": "Allergirom", "Allergy-friendly room": "Allergirom",

View File

@@ -659,6 +659,7 @@
"{difference}{amount} {currency}": "{difference}{amount} {currency}", "{difference}{amount} {currency}": "{difference}{amount} {currency}",
"{distanceInKm} km": "{distanceInKm} km", "{distanceInKm} km": "{distanceInKm} km",
"{lowest} to {highest} persons": "{lowest} till {highest} personer", "{lowest} to {highest} persons": "{lowest} till {highest} personer",
"{memberPrice} {currency}": "{memberPrice} {currency}",
"{min} to {max} characters": "{min} till {max} tecken", "{min} to {max} characters": "{min} till {max} tecken",
"{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} tillgängliga", "{numberOfRooms, plural, one {# room type} other {# room types}} available": "{numberOfRooms, plural, one {# room type} other {# room types}} tillgängliga",
"{number} km to city center": "{number} km till centrum", "{number} km to city center": "{number} km till centrum",

View File

@@ -25,6 +25,10 @@ import type {
Restaurant, Restaurant,
Room, Room,
} from "@/types/hotel" } from "@/types/hotel"
import type {
Product,
RateDefinition,
} from "@/types/trpc/routers/hotel/roomAvailability"
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html // NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
export const hotelSchema = z export const hotelSchema = z
@@ -91,6 +95,30 @@ export const hotelsAvailabilitySchema = z.object({
), ),
}) })
function everyRateHasBreakfastIncluded(
product: Product,
rateDefinitions: RateDefinition[],
userType: "member" | "public"
) {
const rateDefinition = rateDefinitions.find(
(rd) => rd.rateCode === product.productType[userType]?.rateCode
)
if (!rateDefinition) {
return false
}
return rateDefinition.breakfastIncluded
}
/**
* This is used for custom sorting further down
* to guarantee correct order of rates
*/
const cancellationRules = {
CancellableBefore6PM: 2,
Changeable: 1,
NotCancellable: 0,
} as const
export const roomsAvailabilitySchema = z export const roomsAvailabilitySchema = z
.object({ .object({
data: z.object({ data: z.object({
@@ -107,7 +135,52 @@ export const roomsAvailabilitySchema = z
type: z.string().optional(), type: z.string().optional(),
}), }),
}) })
.transform((o) => o.data.attributes) .transform((o) => {
const cancellationRuleLookup = o.data.attributes.rateDefinitions.reduce(
(acc, val) => {
// @ts-expect-error - index of cancellationRule TS
acc[val.rateCode] = cancellationRules[val.cancellationRule]
return acc
},
{}
)
o.data.attributes.roomConfigurations =
o.data.attributes.roomConfigurations.map((room) => {
if (room.products.length) {
room.breakfastIncludedInAllRatesMember = room.products.every(
(product) =>
everyRateHasBreakfastIncluded(
product,
o.data.attributes.rateDefinitions,
"member"
)
)
room.breakfastIncludedInAllRatesPublic = room.products.every(
(product) =>
everyRateHasBreakfastIncluded(
product,
o.data.attributes.rateDefinitions,
"public"
)
)
}
// CancellationRule is the same for public and member per product
// Sorting to guarantee order based on rate
room.products = room.products.sort(
(a, b) =>
// @ts-expect-error - index
cancellationRuleLookup[a.productType.public.rateCode] -
// @ts-expect-error - index
cancellationRuleLookup[b.productType.public.rateCode]
)
return room
})
return o.data.attributes
})
export const ratesSchema = z.array(rateSchema) export const ratesSchema = z.array(rateSchema)

View File

@@ -1,25 +1,81 @@
import deepmerge from "deepmerge"
import { z } from "zod" import { z } from "zod"
import { productSchema } from "./product" import { productSchema } from "./product"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter" import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
export const roomConfigurationSchema = z.object({ export const roomConfigurationSchema = z
features: z .object({
.array( breakfastIncludedInAllRatesMember: z.boolean().default(false),
z.object({ breakfastIncludedInAllRatesPublic: z.boolean().default(false),
inventory: z.number(), features: z
code: z.enum([ .array(
RoomPackageCodeEnum.PET_ROOM, z.object({
RoomPackageCodeEnum.ALLERGY_ROOM, inventory: z.number(),
RoomPackageCodeEnum.ACCESSIBILITY_ROOM, code: z.enum([
]), RoomPackageCodeEnum.PET_ROOM,
}) RoomPackageCodeEnum.ALLERGY_ROOM,
) RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
.default([]), ]),
products: z.array(productSchema).default([]), })
roomsLeft: z.number(), )
roomType: z.string(), .default([]),
roomTypeCode: z.string(), products: z.array(productSchema).default([]),
status: z.string(), roomsLeft: z.number(),
}) roomType: z.string(),
roomTypeCode: z.string(),
status: z.string(),
})
.transform((data) => {
if (data.products.length) {
const someProductsMissAtLeastOneRateCode = data.products.some(
({ productType }) =>
!productType.public.rateCode || !productType.member?.rateCode
)
if (someProductsMissAtLeastOneRateCode) {
data.products = data.products.map((product) => {
if (
product.productType.public.rateCode &&
product.productType.member?.rateCode
) {
return product
}
/**
* Reset both rateCodes if one is missing to show `No prices available` for the same reason as
* mentioned above.
*
* TODO: (Maybe) notify somewhere that this happened
*/
return deepmerge(product, {
productType: {
member: {
rateCode: "",
},
public: {
rateCode: "",
},
},
})
})
}
/**
* When all products miss at least one rateCode (member or public), we change the status to NotAvailable
* since we cannot as of now (31 january) guarantee the flow with missing rateCodes.
*
* TODO: (Maybe) notify somewhere that this happened
*/
const allProductsMissAtLeastOneRateCode = data.products.every(
({ productType }) =>
!productType.public.rateCode || !productType.member?.rateCode
)
if (allProductsMissAtLeastOneRateCode) {
data.status = AvailabilityEnum.NotAvailable
}
}
return data
})

View File

@@ -2,6 +2,7 @@ import { create } from "zustand"
import { filterDuplicateRoomTypesByLowestPrice } from "@/components/HotelReservation/SelectRate/Rooms/utils" import { filterDuplicateRoomTypesByLowestPrice } from "@/components/HotelReservation/SelectRate/Rooms/utils"
import { AvailabilityEnum } from "@/types/components/hotelReservation/selectHotel/selectHotel"
import type { import type {
FilterValues, FilterValues,
RoomPackageCodeEnum, RoomPackageCodeEnum,
@@ -43,7 +44,7 @@ export const useRoomFilteringStore = create<RoomFilteringState>((set, get) => ({
notAvailable: RoomConfiguration[] notAvailable: RoomConfiguration[]
}>( }>(
(acc, curr) => { (acc, curr) => {
if (curr.status === "NotAvailable") if (curr?.status === AvailabilityEnum.NotAvailable)
return { ...acc, notAvailable: [...acc.notAvailable, curr] } return { ...acc, notAvailable: [...acc.notAvailable, curr] }
return { ...acc, available: [...acc.available, curr] } return { ...acc, available: [...acc.available, curr] }
}, },
@@ -83,7 +84,7 @@ export const useRoomFilteringStore = create<RoomFilteringState>((set, get) => ({
return state.visibleRooms.filter((room) => return state.visibleRooms.filter((room) =>
selectedPackages.every((filteredPackage) => selectedPackages.every((filteredPackage) =>
room.features.some((feature) => feature.code === filteredPackage) room?.features.some((feature) => feature.code === filteredPackage)
) )
) )
}, },

View File

@@ -5,6 +5,7 @@ import {
export type HotelCardProps = { export type HotelCardProps = {
hotel: HotelData hotel: HotelData
isUserLoggedIn: boolean
type?: HotelCardListingTypeEnum type?: HotelCardListingTypeEnum
state?: "default" | "active" state?: "default" | "active"
} }

View File

@@ -14,18 +14,23 @@ type ProductPrice = z.output<typeof productTypePriceSchema>
export type RoomPriceSchema = z.output<typeof priceSchema> export type RoomPriceSchema = z.output<typeof priceSchema>
export type FlexibilityOptionProps = { export type FlexibilityOptionProps = {
product: Product | undefined handleSelect: (
name: string rateCode: string,
value: string rateName: string,
paymentTerm: string paymentTerm: string
priceInformation?: Array<string> ) => void
roomTypeCode: RoomConfiguration["roomTypeCode"]
petRoomPackage: RoomPackage | undefined
isSelected: boolean isSelected: boolean
onSelect: () => void isUserLoggedIn: boolean
paymentTerm: string
petRoomPackage: RoomPackage | undefined
priceInformation?: Array<string>
product: Product | undefined
roomTypeCode: RoomConfiguration["roomTypeCode"]
title: string
} }
export interface PriceListProps { export interface PriceListProps {
isUserLoggedIn: boolean
publicPrice?: ProductPrice | Record<string, never> publicPrice?: ProductPrice | Record<string, never>
memberPrice?: ProductPrice | Record<string, never> memberPrice?: ProductPrice | Record<string, never>
petRoomPackage?: RoomPackage petRoomPackage?: RoomPackage

View File

@@ -7,12 +7,12 @@ import type {
} from "./roomFilter" } from "./roomFilter"
export interface RoomTypeListProps { export interface RoomTypeListProps {
roomsAvailability: RoomsAvailability
roomCategories: Room[]
availablePackages: RoomPackages | undefined availablePackages: RoomPackages | undefined
selectedPackages: RoomPackageCodes[]
hotelType: string | undefined hotelType: string | undefined
roomCategories: Room[]
roomListIndex: number roomListIndex: number
roomsAvailability: RoomsAvailability
selectedPackages: RoomPackageCodes[]
} }
export interface SelectRateProps { export interface SelectRateProps {
@@ -24,10 +24,10 @@ export interface SelectRateProps {
} }
export interface RoomSelectionPanelProps { export interface RoomSelectionPanelProps {
roomCategories: Room[]
availablePackages: RoomPackages availablePackages: RoomPackages
selectedPackages: RoomPackageCodes[]
hotelType: string | undefined
defaultPackages: DefaultFilterOptions[] defaultPackages: DefaultFilterOptions[]
hotelType: string | undefined
roomCategories: Room[]
roomListIndex: number roomListIndex: number
selectedPackages: RoomPackageCodes[]
} }

View File

@@ -1,3 +1,5 @@
import "server-only"
import type { Session } from "next-auth" import type { Session } from "next-auth"
export function isValidSession(session: Session | null) { export function isValidSession(session: Session | null) {