fix: clean up hotel and its typings

This commit is contained in:
Simon Emanuelsson
2024-12-17 16:17:25 +01:00
parent ec74af8814
commit 13a164242f
110 changed files with 1931 additions and 1559 deletions

View File

@@ -12,6 +12,8 @@ export default async function BookingConfirmationPage({
setLang(params.lang)
void getBookingConfirmation(searchParams.confirmationNumber)
return (
<BookingConfirmation confirmationNumber={searchParams.confirmationNumber} />
<BookingConfirmation
confirmationNumber={searchParams.confirmationNumber}
/>
)
}

View File

@@ -1,4 +1,4 @@
import { getHotelData } from "@/lib/trpc/memoizedRequests"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import SidePeek from "@/components/HotelReservation/SidePeek"
@@ -12,9 +12,10 @@ export default async function HotelSidePeek({
return <SidePeek hotel={null} />
}
const hotel = await getHotelData({
const hotel = await getHotel({
hotelId: searchParams.hotel,
language: params.lang,
isCardOnlyPayment: false,
})
return <SidePeek hotel={hotel} />

View File

@@ -1,4 +1,4 @@
import { getHotelData } from "@/lib/trpc/memoizedRequests"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server"
import { getLang } from "@/i18n/serverContext"
@@ -73,15 +73,16 @@ async function enhanceHotels(hotels: {
const language = getLang()
const hotelFetchers = hotels.availability.map(async (hotel) => {
const hotelData = await getHotelData({
const hotelData = await getHotel({
hotelId: hotel.hotelId.toString(),
isCardOnlyPayment: false,
language,
})
if (!hotelData) return { hotelData: null, price: hotel.productType }
return {
hotelData: hotelData.data.attributes,
hotelData: hotelData.hotel,
price: hotel.productType,
}
})

View File

@@ -2,7 +2,7 @@ import { differenceInCalendarDays, format, isWeekend } from "date-fns"
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { getHotelData } from "@/lib/trpc/memoizedRequests"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import HotelInfoCard from "@/components/HotelReservation/SelectRate/HotelInfoCard"
import { RoomsContainer } from "@/components/HotelReservation/SelectRate/Rooms/RoomsContainer"
@@ -28,13 +28,16 @@ export default async function SelectRatePage({
}: PageArgs<LangParams & { section: string }, SelectRateSearchParams>) {
setLang(params.lang)
const searchDetails = await getHotelSearchDetails({ searchParams })
if (!searchDetails) return notFound()
const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } =
searchDetails
if (!searchDetails) {
return notFound()
}
const { hotel, adultsInRoom, childrenInRoom, selectHotelParams } = searchDetails
if (!hotel) return notFound()
if (!hotel) {
return notFound()
}
const hotelData = await getHotelData({
const hotelData = await getHotel({
hotelId: hotel.id,
language: params.lang,
})
@@ -72,9 +75,9 @@ export default async function SelectRatePage({
leadTime: differenceInCalendarDays(arrivalDate, new Date()),
searchType: "hotel",
bookingTypeofDay: isWeekend(arrivalDate) ? "weekend" : "weekday",
country: hotelData?.data?.attributes.address.country,
country: hotelData?.hotel.address.country,
hotelID: hotel?.id,
region: hotelData?.data?.attributes.address.city,
region: hotelData?.hotel.address.city,
}
const hotelId = +hotel.id

View File

@@ -16,8 +16,8 @@ import {
type TrackingSDKUserData,
} from "@/types/components/tracking"
import type { Packages } from "@/types/requests/packages"
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
import type { Lang } from "@/constants/languages"
import type { RoomConfiguration } from "@/server/routers/hotels/output"
type Props = {
initialHotelsTrackingData: TrackingSDKHotelInfo
@@ -135,7 +135,7 @@ export default function EnterDetailsTracking(props: Props) {
roomPrice: roomPrice.perStay.local.price,
discount: roomRate.memberRate
? roomRate.publicRate.localPrice.pricePerStay -
roomRate.memberRate.localPrice.pricePerStay
roomRate.memberRate.localPrice.pricePerStay
: 0,
analyticsrateCode: getAnalyticsRateCode(roomRate.publicRate.rateCode),
ancillaries: breakfastAncillary ? [breakfastAncillary] : [],

View File

@@ -4,7 +4,7 @@ import { Suspense } from "react"
import {
getBreakfastPackages,
getHotelData,
getHotel,
getPackages,
getProfileSafely,
getSelectedRoomAvailability,
@@ -106,19 +106,19 @@ export default async function StepPage({
const packages = packageCodes
? await getPackages({
adults,
children: childrenInRoom?.length,
endDate: toDate,
hotelId,
packageCodes,
startDate: fromDate,
})
adults,
children: childrenInRoom?.length,
endDate: toDate,
hotelId,
packageCodes,
startDate: fromDate,
})
: null
const roomAvailability = await getSelectedRoomAvailability(
selectedRoomAvailabilityInput
)
const hotelData = await getHotelData({
const hotelData = await getHotel({
hotelId,
isCardOnlyPayment: roomAvailability?.mustBeGuaranteed,
language: lang,
@@ -153,14 +153,14 @@ export default async function StepPage({
const memberPrice = roomAvailability.memberRate
? {
price: roomAvailability.memberRate.localPrice.pricePerStay,
currency: roomAvailability.memberRate.localPrice.currency,
}
price: roomAvailability.memberRate.localPrice.pricePerStay,
currency: roomAvailability.memberRate.localPrice.currency,
}
: undefined
const arrivalDate = new Date(fromDate)
const departureDate = new Date(toDate)
const hotelAttributes = hotelData?.data.attributes
const arrivalDate = new Date(searchParams.fromDate)
const departureDate = new Date(searchParams.toDate)
const hotelAttributes = hotelData?.hotel
const initialHotelsTrackingData: TrackingSDKHotelInfo = {
searchTerm: searchParams.city,
@@ -207,7 +207,7 @@ export default async function StepPage({
searchParamsStr={selectRoomParams.toString()}
step={searchParams.step}
user={user}
vat={hotelData.data.attributes.vat}
vat={hotelAttributes.vat}
>
<main>
<HotelHeader hotelData={hotelData} />
@@ -262,11 +262,11 @@ export default async function StepPage({
user={user}
roomPrice={roomPrice}
otherPaymentOptions={
hotelData.data.attributes.merchantInformationData
hotelData.hotel.merchantInformationData
.alternatePaymentOptions
}
supportedCards={
hotelData.data.attributes.merchantInformationData.cards
hotelData.hotel.merchantInformationData.cards
}
mustBeGuaranteed={mustBeGuaranteed}
/>

View File

@@ -1,5 +1,5 @@
import { env } from "@/env/server"
import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests"
import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests"
import BookingWidget, { preload } from "@/components/BookingWidget"
import { getLang } from "@/i18n/serverContext"
@@ -21,7 +21,7 @@ export default async function BookingWidgetPage({
if (params.contentType === PageContentTypeEnum.hotelPage) {
const hotelPageData = await getHotelPage()
const hotelData = await getHotelData({
const hotelData = await getHotel({
hotelId: hotelPageData?.hotel_page_id || "",
language: getLang(),
})

View File

@@ -29,8 +29,8 @@ export default async function HotelListing({
</Title>
{hotels.map(({ data, url }) => (
<HotelListingItem
key={data.name}
hotel={data}
key={data.hotel.name}
hotel={data.hotel}
contentType={contentType}
url={url}
/>

View File

@@ -1,14 +1,15 @@
import type {
Hotel,
HotelAddress,
HotelData,
HotelContent,
HotelLocation,
HotelTripAdvisor,
} from "@/types/hotel"
export type IntroSectionProps = {
hotelName: HotelData["data"]["attributes"]["name"]
hotelDescription: HotelData["data"]["attributes"]["hotelContent"]["texts"]["descriptions"]["short"]
location: HotelLocation
address: HotelAddress
hotelDescription: HotelContent["texts"]["descriptions"]["short"]
hotelName: Hotel["name"]
location: HotelLocation
tripAdvisor: HotelTripAdvisor
}

View File

@@ -10,9 +10,9 @@ import {
} from "@/types/components/hotelPage/sidepeek/parking"
export default async function ParkingPrices({
pricing,
currency,
freeParking,
pricing,
}: ParkingPricesProps) {
const intl = await getIntl()
const day = intl.formatMessage({ id: "Price per day" })

View File

@@ -53,8 +53,8 @@ export default async function ParkingAmenity({
</Caption>
<Divider color="baseSurfaceSubtleHover" />
<ParkingPrices
pricing={data.pricing.localCurrency.ordinary}
currency={data.pricing.localCurrency.currency}
pricing={data.pricing.localCurrency?.ordinary}
currency={data.pricing.localCurrency?.currency}
freeParking={data.pricing.freeParking}
/>
</div>
@@ -64,8 +64,8 @@ export default async function ParkingAmenity({
</Caption>
<Divider color="baseSurfaceSubtleHover" />
<ParkingPrices
pricing={data.pricing.localCurrency.weekend}
currency={data.pricing.localCurrency.currency}
pricing={data.pricing.localCurrency?.weekend}
currency={data.pricing.localCurrency?.currency}
freeParking={data.pricing.freeParking}
/>
</div>

View File

@@ -16,8 +16,8 @@ export default async function RestaurantBarItem({
restaurant,
}: RestaurantBarItemProps) {
const intl = await getIntl()
const { name, openingDetails, menus, content } = restaurant
const { bookTableUrl, images } = restaurant.content
const { bookTableUrl, name, openingDetails, content, menus } = restaurant
const { images } = restaurant.content
const visibleImages = restaurant.content.images.slice(0, 2)
const imageWidth = images.length === 2 ? 240 : 496

View File

@@ -2,7 +2,7 @@ import { notFound } from "next/navigation"
import { Suspense } from "react"
import { env } from "@/env/server"
import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests"
import { getHotel, getHotelPage } from "@/lib/trpc/memoizedRequests"
import AccordionSection from "@/components/Blocks/Accordion"
import Breadcrumbs from "@/components/Breadcrumbs"
@@ -41,23 +41,27 @@ import styles from "./hotelPage.module.css"
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
import type { HotelPageProps } from "@/types/components/hotelPage/hotelPage"
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
import type { AdditionalData, Facility } from "@/types/hotel"
import type { Facility } from "@/types/hotel"
import { PageContentTypeEnum } from "@/types/requests/contentType"
export default async function HotelPage({ hotelId }: HotelPageProps) {
const lang = getLang()
const [hotelPageData, hotelData] = await Promise.all([
getHotelPage(),
getHotelData({ hotelId, language: lang }),
getHotel({
hotelId,
isCardOnlyPayment: false,
language: lang,
}),
])
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
if (!hotelData?.data || !hotelPageData) {
if (!hotelData?.hotel || !hotelPageData) {
return notFound()
}
const jsonSchema = generateHotelSchema(hotelData.data.attributes)
const jsonSchema = generateHotelSchema(hotelData.hotel)
const { faq, content, tabValues } = hotelPageData
const {
name,
@@ -73,12 +77,9 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
location,
ratings,
parking,
} = hotelData.data.attributes
const roomCategories = hotelData.included.rooms || []
const restaurants = hotelData.included.restaurants || []
const additionalData =
hotelData.included.additionalData || ({} as AdditionalData)
} = hotelData.hotel
const restaurants = hotelData.restaurants
const roomCategories = hotelData.roomCategories
const {
healthAndWellness,
restaurantImages,
@@ -87,7 +88,7 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
gallery,
hotelParking,
displayWebPage,
} = additionalData
} = hotelData.additionalData
const images = gallery?.smallerImages
const description = hotelContent.texts.descriptions.medium

View File

@@ -22,8 +22,9 @@ export default async function BookingConfirmation({
confirmationNumber,
}: BookingConfirmationProps) {
const lang = getLang()
const { booking, hotel, room } =
await getBookingConfirmation(confirmationNumber)
const { booking, hotel, room } = await getBookingConfirmation(
confirmationNumber
)
const arrivalDate = new Date(booking.checkInDate)
const departureDate = new Date(booking.checkOutDate)

View File

@@ -1,6 +1,6 @@
import { z } from "zod"
import { breakfastPackageSchema } from "@/server/routers/hotels/output"
import { breakfastPackageSchema } from "@/server/routers/hotels/schemas/packages"
export const breakfastStoreSchema = z.object({
breakfast: breakfastPackageSchema.or(z.literal(false)),

View File

@@ -10,10 +10,10 @@ import styles from "./header.module.css"
import type { HotelHeaderProps } from "@/types/components/hotelReservation/enterDetails/hotelHeader"
export default async function HotelHeader({ hotelData }: HotelHeaderProps) {
export default async function HotelHeader({
hotelData: { hotel },
}: HotelHeaderProps) {
const intl = await getIntl()
const hotel = hotelData.data.attributes
const image = hotel.hotelContent?.images
const addressStr = `${hotel.address.streetAddress}, ${hotel.address.city}`

View File

@@ -32,7 +32,7 @@ import {
} from "@/types/components/tracking"
function isValidHotelData(hotel: NullableHotelData): hotel is HotelData {
return hotel !== null && hotel !== undefined
return hotel != null
}
export async function SelectHotelMapContainer({

View File

@@ -1,6 +1,6 @@
import { Suspense } from "react"
import { getHotelData } from "@/lib/trpc/memoizedRequests"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
import ImageGallery from "@/components/ImageGallery"
@@ -18,55 +18,44 @@ import { NoRoomsAlert } from "./NoRoomsAlert"
import styles from "./hotelInfoCard.module.css"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Lang } from "@/constants/languages"
type Props = {
hotelId: number
lang: Lang
fromDate: Date
toDate: Date
adultCount: number
childArray?: Child[]
}
import type { HotelInfoCardProps } from "@/types/components/hotelReservation/selectRate/hotelInfoCard"
export default async function HotelInfoCard({
hotelId,
lang,
...props
}: Props) {
const hotelData = await getHotelData({
}: HotelInfoCardProps) {
const hotelData = await getHotel({
hotelId: hotelId.toString(),
isCardOnlyPayment: false,
language: lang,
})
const hotelAttributes = hotelData?.data.attributes
const hotel = hotelData?.hotel
const intl = await getIntl()
const sortedFacilities = hotelAttributes?.detailedFacilities
const sortedFacilities = hotel?.detailedFacilities
.sort((a, b) => b.sortOrder - a.sortOrder)
.slice(0, 5)
return (
<article className={styles.container}>
{hotelAttributes && (
{hotel && (
<section className={styles.wrapper}>
<div className={styles.imageWrapper}>
<ImageGallery
title={hotelAttributes.name}
images={hotelAttributes.galleryImages}
title={hotel.name}
images={hotel.galleryImages}
fill
/>
{hotelAttributes.ratings?.tripAdvisor && (
<TripAdvisorChip
rating={hotelAttributes.ratings.tripAdvisor.rating}
/>
{hotel.ratings?.tripAdvisor && (
<TripAdvisorChip rating={hotel.ratings.tripAdvisor.rating} />
)}
</div>
<div className={styles.hotelContent}>
<div className={styles.hotelInformation}>
<Title as="h2" textTransform="uppercase">
{hotelAttributes.name}
{hotel.name}
</Title>
<div className={styles.hotelAddressDescription}>
<Caption color="uiTextMediumContrast">
@@ -75,16 +64,16 @@ export default async function HotelInfoCard({
id: "{address}, {city} ∙ {distanceToCityCenterInKm} km to city center",
},
{
address: hotelAttributes.address.streetAddress,
city: hotelAttributes.address.city,
distanceToCityCenterInKm: getSingleDecimal(
hotelAttributes.location.distanceToCentre / 1000
address: hotel.address.streetAddress,
city: hotel.address.city,
distanceToCityCentreInKm: getSingleDecimal(
hotel.location.distanceToCentre / 1000
),
}
)}
</Caption>
<Body color="uiTextHighContrast">
{hotelAttributes.hotelContent.texts.descriptions.medium}
{hotel.hotelContent.texts.descriptions.medium}
</Body>
</div>
</div>
@@ -111,15 +100,15 @@ export default async function HotelInfoCard({
</div>
<ReadMore
label={intl.formatMessage({ id: "Show all amenities" })}
hotelId={hotelAttributes.operaId}
hotel={hotelAttributes}
hotelId={hotel.operaId}
hotel={hotel}
showCTA={false}
/>
</div>
</div>
</section>
)}
{hotelAttributes?.specialAlerts.map((alert) => {
{hotel?.specialAlerts.map((alert) => {
return (
<div className={styles.hotelAlert} key={`wrapper_${alert.id}`}>
<Alert

View File

@@ -22,7 +22,7 @@ import styles from "./roomCard.module.css"
import type { RoomCardProps } from "@/types/components/hotelReservation/selectRate/roomCard"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { HotelTypeEnum } from "@/types/enums/hotelType"
import type { RateDefinition } from "@/server/routers/hotels/output"
import type { RateDefinition } from "@/types/trpc/routers/hotel/roomAvailability"
export default function RoomCard({
hotelId,

View File

@@ -1,6 +1,6 @@
import { dt } from "@/lib/dt"
import {
getHotelData,
getHotel,
getPackages,
getRoomsAvailability,
} from "@/lib/trpc/memoizedRequests"
@@ -13,33 +13,28 @@ import { generateChildrenString } from "../../utils"
import Rooms from "."
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { Lang } from "@/constants/languages"
export type Props = {
hotelId: number
fromDate: Date
toDate: Date
adultCount: number
childArray?: Child[]
lang: Lang
}
import type { RoomsContainerProps } from "@/types/components/hotelReservation/selectRate/roomsContainer"
export async function RoomsContainer({
hotelId,
fromDate,
toDate,
adultCount,
childArray,
fromDate,
hotelId,
lang,
}: Props) {
toDate,
}: RoomsContainerProps) {
const session = await auth()
const isUserLoggedIn = isValidSession(session)
const fromDateString = dt(fromDate).format("YYYY-MM-DD")
const toDateString = dt(toDate).format("YYYY-MM-DD")
const hotelDataPromise = safeTry(
getHotelData({ hotelId: hotelId.toString(), language: lang })
getHotel({
hotelId: hotelId.toString(),
isCardOnlyPayment: false,
language: lang,
})
)
const packagesPromise = safeTry(
@@ -94,10 +89,10 @@ export async function RoomsContainer({
return (
<Rooms
availablePackages={packages ?? []}
roomsAvailability={roomsAvailability}
roomCategories={hotelData?.included.rooms ?? []}
hotelType={hotelData?.data.attributes?.hotelType}
hotelType={hotelData?.hotel.hotelType}
isUserLoggedIn={isUserLoggedIn}
roomsAvailability={roomsAvailability}
roomCategories={hotelData?.roomCategories ?? []}
/>
)
}

View File

@@ -25,14 +25,14 @@ import {
} from "@/types/components/hotelReservation/selectRate/roomFilter"
import type { SelectRateProps } from "@/types/components/hotelReservation/selectRate/roomSelection"
import type { SelectRateSearchParams } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { RoomConfiguration } from "@/server/routers/hotels/output"
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
export default function Rooms({
roomsAvailability,
roomCategories = [],
availablePackages,
hotelType,
isUserLoggedIn,
roomsAvailability,
roomCategories = [],
}: SelectRateProps) {
const router = useRouter()
const pathname = usePathname()

View File

@@ -1,5 +1,4 @@
import type { RoomParam } from "@/types/components/hotelReservation/selectRate/section"
import type { RoomConfiguration } from "@/server/routers/hotels/output"
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
/**
* Get the lowest priced room for each room type that appears more than once.
@@ -64,16 +63,16 @@ export function filterDuplicateRoomTypesByLowestPrice(
if (
!previousLowest ||
currentRequestedPrice <
Math.min(
Number(
previousLowest.products[0].productType.public.requestedPrice
?.pricePerNight
) ?? Infinity,
Number(
previousLowest.products[0].productType.member?.requestedPrice
?.pricePerNight
) ?? Infinity
) ||
Math.min(
Number(
previousLowest.products[0].productType.public.requestedPrice
?.pricePerNight
) ?? Infinity,
Number(
previousLowest.products[0].productType.member?.requestedPrice
?.pricePerNight
) ?? Infinity
) ||
(currentRequestedPrice ===
Math.min(
Number(
@@ -86,16 +85,16 @@ export function filterDuplicateRoomTypesByLowestPrice(
) ?? Infinity
) &&
currentLocalPrice <
Math.min(
Number(
previousLowest.products[0].productType.public.localPrice
?.pricePerNight
) ?? Infinity,
Number(
previousLowest.products[0].productType.member?.localPrice
?.pricePerNight
) ?? Infinity
))
Math.min(
Number(
previousLowest.products[0].productType.public.localPrice
?.pricePerNight
) ?? Infinity,
Number(
previousLowest.products[0].productType.member?.localPrice
?.pricePerNight
) ?? Infinity
))
) {
roomMap.set(roomType, room)
}

View File

@@ -7,13 +7,11 @@ import HotelSidePeek from "@/components/SidePeeks/HotelSidePeek"
import RoomSidePeek from "@/components/SidePeeks/RoomSidePeek"
import useLang from "@/hooks/useLang"
import type { HotelData } from "@/types/hotel"
import type { HotelReservationSidePeekProps } from "@/types/components/hotelReservation/sidePeek"
export default function HotelReservationSidePeek({
hotel,
}: {
hotel: HotelData | null
}) {
}: HotelReservationSidePeekProps) {
const activeSidePeek = useSidePeekStore((state) => state.activeSidePeek)
const hotelId = useSidePeekStore((state) => state.hotelId)
const roomTypeCode = useSidePeekStore((state) => state.roomTypeCode)
@@ -21,7 +19,7 @@ export default function HotelReservationSidePeek({
const close = useSidePeekStore((state) => state.closeSidePeek)
const lang = useLang()
const { data: hotelData } = trpc.hotel.hotelData.get.useQuery(
const { data: hotelData } = trpc.hotel.get.useQuery(
{
hotelId: hotelId ?? "",
language: lang,
@@ -32,7 +30,7 @@ export default function HotelReservationSidePeek({
}
)
const selectedRoom = hotelData?.included.rooms?.find((room) =>
const selectedRoom = hotelData?.roomCategories.find((room) =>
room.roomTypes.some((type) => type.code === roomTypeCode)
)
@@ -41,8 +39,8 @@ export default function HotelReservationSidePeek({
<>
{hotelData && (
<HotelSidePeek
hotel={hotelData.data?.attributes}
additionalHotelData={hotelData.included.additionalData}
additionalHotelData={hotelData.additionalData}
hotel={hotelData.hotel}
activeSidePeek={activeSidePeek}
close={close}
showCTA={showCTA}

View File

@@ -2,7 +2,7 @@ import { cva } from "class-variance-authority"
import styles from "./poi.module.css"
import { PointOfInterestGroupEnum } from "@/types/hotel"
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
export const poiVariants = cva(styles.icon, {
variants: {

View File

@@ -1,5 +1,5 @@
import { IconName } from "@/types/components/icon"
import { PointOfInterestGroupEnum } from "@/types/hotel"
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
export function getIconByPoiGroupAndCategory(
group: PointOfInterestGroupEnum,

View File

@@ -6,12 +6,15 @@ import type {
BreackfastPackagesInput,
PackagesInput,
} from "@/types/requests/packages"
import type {
CityCoordinatesInput,
HotelInput,
} from "@/types/trpc/routers/hotel/hotel"
import type { Lang } from "@/constants/languages"
import type {
GetHotelsInput,
GetRoomsAvailabilityInput,
GetSelectedRoomAvailabilityInput,
HotelDataInput,
} from "@/server/routers/hotels/input"
import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input"
@@ -35,9 +38,9 @@ export const getProfileSafely = cache(
export const getSavedPaymentCardsSafely = cache(
async function getMemoizedSavedPaymentCardsSafely(
args: GetSavedPaymentCardsInput
input: GetSavedPaymentCardsInput
) {
return serverClient().user.safePaymentCards(args)
return serverClient().user.safePaymentCards(input)
}
)
@@ -69,10 +72,13 @@ export const getHotels = cache(async function getMemoizedHotels(
return serverClient().hotel.hotels.get(input)
})
export const getHotelData = cache(async function getMemoizedHotelData(
input: HotelDataInput
export const getHotel = cache(async function getMemoizedHotelData(
input: HotelInput
) {
return serverClient().hotel.hotelData.get(input)
if (!input.isCardOnlyPayment) {
input.isCardOnlyPayment = false
}
return serverClient().hotel.get(input)
})
export const getHotelPage = cache(async function getMemoizedHotelPage() {
@@ -87,9 +93,9 @@ export const getRoomsAvailability = cache(
export const getSelectedRoomAvailability = cache(
function getMemoizedSelectedRoomAvailability(
args: GetSelectedRoomAvailabilityInput
input: GetSelectedRoomAvailabilityInput
) {
return serverClient().hotel.availability.room(args)
return serverClient().hotel.availability.room(input)
}
)
@@ -129,29 +135,26 @@ export const getSiteConfig = cache(async function getMemoizedSiteConfig() {
return serverClient().contentstack.base.siteConfig()
})
export const getBreakfastPackages = cache(function getMemoizedBreakfastPackages(
input: BreackfastPackagesInput
) {
return serverClient().hotel.packages.breakfast(input)
})
export const getBreakfastPackages = cache(
async function getMemoizedBreakfastPackages(input: BreackfastPackagesInput) {
return serverClient().hotel.packages.breakfast(input)
}
)
export const getPackages = cache(function getMemoizedPackages(
export const getPackages = cache(async function getMemoizedPackages(
input: PackagesInput
) {
return serverClient().hotel.packages.get(input)
})
export const getBookingConfirmation = cache(
function getMemoizedBookingConfirmation(confirmationNumber: string) {
async function getMemoizedBookingConfirmation(confirmationNumber: string) {
return serverClient().booking.confirmation({ confirmationNumber })
}
)
export const getCityCoordinates = cache(
async function getMemoizedCityCoordinates(input: {
city: string
hotel: { address: string | undefined }
}) {
async function getMemoizedCityCoordinates(input: CityCoordinatesInput) {
return serverClient().hotel.map.city(input)
}
)

View File

@@ -5,7 +5,7 @@ import { dt } from "@/lib/dt"
import { badRequestError, serverErrorByStatus } from "@/server/errors/trpc"
import { router, serviceProcedure } from "@/server/trpc"
import { getHotelData } from "../hotels/query"
import { getHotel } from "../hotels/query"
import { bookingConfirmationInput, getBookingStatusInput } from "./input"
import { bookingConfirmationSchema, createBookingSchema } from "./output"
import { getBookedHotelRoom } from "./utils"
@@ -84,8 +84,12 @@ export const bookingQueryRouter = router({
throw badRequestError()
}
const hotelData = await getHotelData(
{ hotelId: booking.data.hotelId, language: ctx.lang },
const hotelData = await getHotel(
{
hotelId: booking.data.hotelId,
isCardOnlyPayment: false,
language: ctx.lang,
},
ctx.serviceToken
)
@@ -123,14 +127,12 @@ export const bookingQueryRouter = router({
* Add hotels check in and out times to booking check in and out date
* as that is date only (YYYY-MM-DD)
*/
const checkInTime =
hotelData.data.attributes.hotelFacts.checkin.checkInTime
const checkInTime = hotelData.hotel.hotelFacts.checkin.checkInTime
const [checkInHour, checkInMinute] = checkInTime.split(":")
const checkIn = dt(booking.data.checkInDate)
.set("hour", Number(checkInHour))
.set("minute", Number(checkInMinute))
const checkOutTime =
hotelData.data.attributes.hotelFacts.checkin.checkOutTime
const checkOutTime = hotelData.hotel.hotelFacts.checkin.checkOutTime
const [checkOutHour, checkOutMinute] = checkOutTime.split(":")
const checkOut = dt(booking.data.checkOutDate)
.set("hour", Number(checkOutHour))
@@ -140,13 +142,10 @@ export const bookingQueryRouter = router({
booking.data.checkOutDate = checkOut.toDate()
return {
...hotelData,
booking: booking.data,
hotel: {
...hotelData.data.attributes,
included: hotelData.included,
},
room: getBookedHotelRoom(
hotelData.included.rooms,
hotelData.roomCategories,
booking.data.roomTypeCode
),
}

View File

@@ -1,8 +1,8 @@
import type { RoomData } from "@/types/hotel"
import type { Room } from "@/types/hotel"
import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmation"
export function getBookedHotelRoom(
rooms: RoomData[] | undefined,
rooms: Room[] | undefined,
roomTypeCode: BookingConfirmation["booking"]["roomTypeCode"]
) {
if (!rooms?.length || !roomTypeCode) {

View File

@@ -1,6 +1,6 @@
import { z } from "zod"
import { hotelAttributesSchema } from "../../hotels/output"
import { attributesSchema as hotelAttributesSchema } from "../../hotels/schemas/hotel"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { getDescription, getImage, getTitle } from "./utils"

View File

@@ -15,7 +15,7 @@ import { contentStackUidWithServiceProcedure, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag"
import { getHotelData } from "../../hotels/query"
import { getHotel } from "../../hotels/query"
import { metadataSchema } from "./output"
import { affix } from "./utils"
@@ -171,15 +171,19 @@ export const metadataQueryRouter = router({
}>(GetHotelPageMetadata, variables)
const hotelPageData = hotelPageResponse.hotel_page
const hotelData = hotelPageData.hotel_page_id
? await getHotelData(
{ hotelId: hotelPageData.hotel_page_id, language: ctx.lang },
? await getHotel(
{
hotelId: hotelPageData.hotel_page_id,
isCardOnlyPayment: false,
language: ctx.lang,
},
ctx.serviceToken
)
: null
return getTransformedMetadata({
...hotelPageData,
hotelData: hotelData?.data.attributes,
hotelData: hotelData?.hotel,
})
default:
return null

View File

@@ -1,9 +1,11 @@
import { z } from "zod"
import { Lang } from "@/constants/languages"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { Country } from "@/types/enums/country"
export const getHotelsAvailabilityInputSchema = z.object({
export const hotelsAvailabilityInputSchema = z.object({
cityId: z.string(),
roomStayStartDate: z.string(),
roomStayEndDate: z.string(),
@@ -21,7 +23,7 @@ export const getHotelsByHotelIdsAvailabilityInputSchema = z.object({
bookingCode: z.string().optional().default(""),
})
export const getRoomsAvailabilityInputSchema = z.object({
export const roomsAvailabilityInputSchema = z.object({
hotelId: z.number(),
roomStayStartDate: z.string(),
roomStayEndDate: z.string(),
@@ -31,7 +33,7 @@ export const getRoomsAvailabilityInputSchema = z.object({
rateCode: z.string().optional(),
})
export const getSelectedRoomAvailabilityInputSchema = z.object({
export const selectedRoomAvailabilityInputSchema = z.object({
hotelId: z.string(),
roomStayStartDate: z.string(),
roomStayEndDate: z.string(),
@@ -44,25 +46,23 @@ export const getSelectedRoomAvailabilityInputSchema = z.object({
})
export type GetSelectedRoomAvailabilityInput = z.input<
typeof getSelectedRoomAvailabilityInputSchema
typeof selectedRoomAvailabilityInputSchema
>
export type GetRoomsAvailabilityInput = z.input<
typeof getRoomsAvailabilityInputSchema
typeof roomsAvailabilityInputSchema
>
export const getRatesInputSchema = z.object({
export const ratesInputSchema = z.object({
hotelId: z.string(),
})
export const getHotelDataInputSchema = z.object({
export const hotelInputSchema = z.object({
hotelId: z.string(),
language: z.string(),
isCardOnlyPayment: z.boolean().optional(),
isCardOnlyPayment: z.boolean().default(false),
language: z.nativeEnum(Lang),
})
export type HotelDataInput = z.input<typeof getHotelDataInputSchema>
export const getHotelsInput = z.object({
locationFilter: z
.object({
@@ -73,13 +73,13 @@ export const getHotelsInput = z.object({
.nullable(),
hotelsToInclude: z.array(z.string()),
})
export interface GetHotelsInput extends z.infer<typeof getHotelsInput> {}
export interface GetHotelsInput extends z.infer<typeof getHotelsInput> { }
export const nearbyHotelIdsInput = z.object({
hotelId: z.string(),
})
export const getBreakfastPackageInputSchema = z.object({
export const breakfastPackageInputSchema = z.object({
adults: z.number().min(1, { message: "at least one adult is required" }),
fromDate: z
.string()
@@ -92,7 +92,7 @@ export const getBreakfastPackageInputSchema = z.object({
.pipe(z.coerce.date()),
})
export const getRoomPackagesInputSchema = z.object({
export const roomPackagesInputSchema = z.object({
hotelId: z.string(),
startDate: z.string(),
endDate: z.string(),
@@ -100,7 +100,7 @@ export const getRoomPackagesInputSchema = z.object({
children: z.number().optional().default(0),
packageCodes: z.array(z.string()).optional().default([]),
})
export const getCityCoordinatesInputSchema = z.object({
export const cityCoordinatesInputSchema = z.object({
city: z.string(),
hotel: z.object({
address: z.string().optional(),

View File

@@ -0,0 +1,65 @@
import { metrics as opentelemetryMetrics } from "@opentelemetry/api"
const meter = opentelemetryMetrics.getMeter("trpc.hotels")
export const metrics = {
additionalData: {
counter: meter.createCounter("trpc.hotels.additionalData"),
fail: meter.createCounter("trpc.hotels.additionalData-fail"),
success: meter.createCounter("trpc.hotels.additionalData-success"),
},
breakfastPackage: {
counter: meter.createCounter("trpc.package.breakfast"),
fail: meter.createCounter("trpc.package.breakfast-fail"),
success: meter.createCounter("trpc.package.breakfast-success"),
},
hotel: {
counter: meter.createCounter("trpc.hotel.get"),
fail: meter.createCounter("trpc.hotel.get-fail"),
success: meter.createCounter("trpc.hotel.get-success"),
},
hotels: {
counter: meter.createCounter("trpc.hotel.hotels.get"),
fail: meter.createCounter("trpc.hotel.hotels.get-fail"),
success: meter.createCounter("trpc.hotel.hotels.get-success"),
},
hotelIds: {
counter: meter.createCounter("trpc.hotel.hotel-ids.get"),
fail: meter.createCounter("trpc.hotel.hotel-ids.get-fail"),
success: meter.createCounter("trpc.hotel.hotel-ids.get-success"),
},
hotelsAvailability: {
counter: meter.createCounter("trpc.hotel.availability.hotels"),
fail: meter.createCounter("trpc.hotel.availability.hotels-fail"),
success: meter.createCounter("trpc.hotel.availability.hotels-success"),
},
hotelsByHotelIdAvailability: {
counter: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id"),
fail: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id-fail"),
success: meter.createCounter("trpc.hotel.availability.hotels-by-hotel-id-success"),
},
meetingRooms: {
counter: meter.createCounter("trpc.hotels.meetingRooms"),
fail: meter.createCounter("trpc.hotels.meetingRooms-fail"),
success: meter.createCounter("trpc.hotels.meetingRooms-success"),
},
nearbyHotelIds: {
counter: meter.createCounter("trpc.hotel.nearby-hotel-ids.get"),
fail: meter.createCounter("trpc.hotel.nearby-hotel-ids.get-fail"),
success: meter.createCounter("trpc.hotel.nearby-hotel-ids.get-success"),
},
packages: {
counter: meter.createCounter("trpc.hotel.packages.get"),
fail: meter.createCounter("trpc.hotel.packages.get-fail"),
success: meter.createCounter("trpc.hotel.packages.get-success"),
},
roomAvailability: {
counter: meter.createCounter("trpc.hotel.availability.rooms"),
fail: meter.createCounter("trpc.hotel.availability.rooms-fail"),
success: meter.createCounter("trpc.hotel.availability.rooms-success"),
},
selectedRoomAvailability: {
counter: meter.createCounter("trpc.hotel.availability.room"),
fail: meter.createCounter("trpc.hotel.availability.room-fail"),
success: meter.createCounter("trpc.hotel.availability.room-success"),
},
}

View File

@@ -1,668 +1,120 @@
import { z } from "zod"
import { ChildBedTypeEnum, type PaymentMethodEnum } from "@/constants/booking"
import { toLang } from "@/server/utils"
import { additionalDataSchema } from "./schemas/additionalData"
import { imageSchema } from "./schemas/image"
import { restaurantSchema } from "./schemas/restaurants"
import { roomSchema } from "./schemas/room"
import { specialAlertsSchema } from "./schemas/specialAlerts"
import { getPoiGroupByCategoryName } from "./utils"
import { occupancySchema } from "./schemas/availability/occupancy"
import { productTypeSchema } from "./schemas/availability/productType"
import { citySchema } from "./schemas/city"
import {
attributesSchema,
includesSchema,
relationshipsSchema as hotelRelationshipsSchema,
} from "./schemas/hotel"
import { locationCitySchema } from "./schemas/location/city"
import { locationHotelSchema } from "./schemas/location/hotel"
import { breakfastPackageSchema, packageSchema } from "./schemas/packages"
import { rateSchema } from "./schemas/rate"
import { relationshipsSchema } from "./schemas/relationships"
import { roomConfigurationSchema } from "./schemas/roomAvailability/configuration"
import { rateDefinitionSchema } from "./schemas/roomAvailability/rateDefinition"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { FacilityEnum } from "@/types/enums/facilities"
import { PackageTypeEnum } from "@/types/enums/packages"
import type { AdditionalData, RestaurantData, RoomData } from "@/types/hotel"
const ratingsSchema = z
.object({
tripAdvisor: z.object({
numberOfReviews: z.number(),
rating: z.number(),
ratingImageUrl: z.string(),
webUrl: z.string(),
awards: z.array(
z.object({
displayName: z.string(),
images: z.object({
small: z.string(),
medium: z.string(),
large: z.string(),
}),
})
),
reviews: z
.object({
widgetHtmlTagId: z.string(),
widgetScriptEmbedUrlIframe: z.string(),
widgetScriptEmbedUrlJavaScript: z.string(),
})
.optional(),
}),
})
.optional()
const addressSchema = z.object({
streetAddress: z.string(),
city: z.string(),
zipCode: z.string(),
country: z.string(),
})
const contactInformationSchema = z.object({
phoneNumber: z.string(),
faxNumber: z.string().optional(),
email: z.string(),
websiteUrl: z.string(),
})
export const checkinSchema = z.object({
checkInTime: z.string(),
checkOutTime: z.string(),
onlineCheckOutAvailableFrom: z.string().nullable().optional(),
onlineCheckout: z.boolean(),
})
const ecoLabelsSchema = z.object({
euEcoLabel: z.boolean(),
greenGlobeLabel: z.boolean(),
nordicEcoLabel: z.boolean(),
svanenEcoLabelCertificateNumber: z.string().optional(),
})
const interiorSchema = z.object({
numberOfBeds: z.number(),
numberOfCribs: z.number(),
numberOfFloors: z.number(),
numberOfRooms: z.object({
connected: z.number(),
forAllergics: z.number().optional(),
forDisabled: z.number(),
nonSmoking: z.number(),
pet: z.number(),
withExtraBeds: z.number(),
total: z.number(),
}),
})
const receptionHoursSchema = z.object({
alwaysOpen: z.boolean(),
isClosed: z.boolean(),
openingTime: z.string().optional(),
closingTime: z.string().optional(),
})
const locationSchema = z.object({
distanceToCentre: z.number(),
latitude: z.number(),
longitude: z.number(),
})
const hotelContentSchema = z.object({
images: imageSchema,
texts: z.object({
facilityInformation: z.string().optional(),
surroundingInformation: z.string(),
descriptions: z.object({
short: z.string(),
medium: z.string(),
}),
meetingDescription: z
.object({
short: z.string().optional(),
medium: z.string().optional(),
})
.optional(),
}),
restaurantsOverviewPage: z.object({
restaurantsOverviewPageLinkText: z.string().optional(),
restaurantsOverviewPageLink: z.string().optional(),
restaurantsContentDescriptionShort: z.string().optional(),
restaurantsContentDescriptionMedium: z.string().optional(),
}), // TODO remove, use new /additionalData endpoint
})
const detailedFacilitySchema = z.object({
id: z.number(),
name: z.string(),
public: z.boolean(),
sortOrder: z.number(),
filter: z.string().optional(),
icon: z.string().optional(),
})
export const facilitySchema = z.object({
headingText: z.string().default(""),
heroImages: z.array(imageSchema),
}) // TODO remove, use new /additionalData endpoint
export const gallerySchema = z.object({
heroImages: z.array(imageSchema),
smallerImages: z.array(imageSchema),
}) // TODO remove, use new /additionalData endpoint
const healthFacilitySchema = z.object({
type: z.string(),
content: z.object({
images: z.array(imageSchema),
texts: z.object({
facilityInformation: z.string().optional(),
surroundingInformation: z.string().optional(),
descriptions: z.object({
short: z.string(),
medium: z.string(),
}),
}),
}),
openingDetails: z.object({
useManualOpeningHours: z.boolean(),
manualOpeningHours: z.string().optional(),
openingHours: z.object({
ordinary: z.object({
alwaysOpen: z.boolean(),
isClosed: z.boolean(),
openingTime: z.string().optional(),
closingTime: z.string().optional(),
sortOrder: z.number().optional(),
}),
weekends: z.object({
alwaysOpen: z.boolean(),
isClosed: z.boolean(),
openingTime: z.string().optional(),
closingTime: z.string().optional(),
sortOrder: z.number().optional(),
}),
}),
}),
details: z.array(
z.object({
name: z.string(),
type: z.string(),
value: z.string().optional(),
})
),
})
const rewardNightSchema = z.object({
points: z.number(),
campaign: z.object({
start: z.string(),
end: z.string(),
points: z.number(),
}),
})
export const pointOfInterestSchema = z
.object({
name: z.string().optional(),
distance: z.number().optional(),
category: z.object({
name: z.string().optional(),
group: z.string().optional(),
}),
location: locationSchema.optional(),
isHighlighted: z.boolean().optional(),
})
.transform((poi) => ({
name: poi.name,
distance: poi.distance,
categoryName: poi.category.name,
group: getPoiGroupByCategoryName(poi.category.name),
coordinates: {
lat: poi.location?.latitude ?? 0,
lng: poi.location?.longitude ?? 0,
},
}))
const parkingPricingSchema = z.object({
freeParking: z.boolean(),
paymentType: z.string().optional(),
localCurrency: z.object({
currency: z.string().default("N/A"),
range: z.object({
min: z.number().optional(),
max: z.number().optional(),
}),
ordinary: z
.array(
z.object({
period: z.string().optional(),
amount: z.number().optional(),
startTime: z.string().optional(),
endTime: z.string().optional(),
})
)
.optional(),
weekend: z
.array(
z.object({
period: z.string().optional(),
amount: z.number().optional(),
startTime: z.string().optional(),
endTime: z.string().optional(),
})
)
.optional(),
}),
requestedCurrency: z
.object({
currency: z.string().default("N/A"),
range: z
.object({
min: z.number().optional(),
max: z.number().optional(),
})
.optional(),
ordinary: z
.array(
z.object({
period: z.string().optional(),
amount: z.number().optional(),
startTime: z.string().optional(),
endTime: z.string().optional(),
})
)
.optional(),
weekend: z
.array(
z.object({
period: z.string().optional(),
amount: z.number().optional(),
startTime: z.string().optional(),
endTime: z.string().optional(),
})
)
.optional(),
})
.optional(),
})
export const parkingSchema = z.object({
type: z.string().optional(),
name: z.string().optional(),
address: z.string().optional(),
numberOfParkingSpots: z.number().optional(),
numberOfChargingSpaces: z.number().optional(),
distanceToHotel: z.number().optional(),
canMakeReservation: z.boolean(),
externalParkingUrl: z.string().optional(),
pricing: parkingPricingSchema,
})
const socialMediaSchema = z.object({
instagram: z.string().optional(),
facebook: z.string().optional(),
})
const relationshipsSchema = z.object({
restaurants: z.object({
links: z.object({
related: z.string(),
}),
}),
nearbyHotels: z.object({
links: z.object({
related: z.string(),
}),
}),
roomCategories: z.object({
links: z.object({
related: z.string(),
}),
}),
meetingRooms: z.object({
links: z.object({
related: z.string(),
}),
}),
})
const merchantInformationSchema = z.object({
webMerchantId: z.string(),
cards: z.record(z.string(), z.boolean()).transform((val) => {
return Object.entries(val)
.filter(([_, enabled]) => enabled)
.map(([key]) => key)
.filter((key): key is PaymentMethodEnum => !!key)
}),
alternatePaymentOptions: z
.record(z.string(), z.boolean())
.transform((val) => {
return Object.entries(val)
.filter(([_, enabled]) => enabled)
.map(([key]) => key)
.filter((key): key is PaymentMethodEnum => !!key)
}),
})
const hotelFacilityDetailSchema = z
.object({
description: z.string(),
heading: z.string(),
})
.optional()
/** Possibly more values */
const hotelFacilityDetailsSchema = z.object({
breakfast: hotelFacilityDetailSchema,
checkout: hotelFacilityDetailSchema,
gym: hotelFacilityDetailSchema,
internet: hotelFacilityDetailSchema,
laundry: hotelFacilityDetailSchema,
luggage: hotelFacilityDetailSchema,
shop: hotelFacilityDetailSchema,
telephone: hotelFacilityDetailSchema,
})
const hotelInformationSchema = z
.object({
description: z.string(),
heading: z.string(),
link: z.string().optional(),
})
.optional()
const hotelInformationsSchema = z.object({
accessibility: hotelInformationSchema,
safety: hotelInformationSchema,
sustainability: hotelInformationSchema,
})
const hotelFactsSchema = z.object({
checkin: checkinSchema,
ecoLabels: ecoLabelsSchema,
hotelFacilityDetail: hotelFacilityDetailsSchema.default({}),
hotelInformation: hotelInformationsSchema.default({}),
interior: interiorSchema,
receptionHours: receptionHoursSchema,
yearBuilt: z.string(),
})
type DetailedFacility = { id: FacilityEnum } & z.infer<
typeof detailedFacilitySchema
>
export const hotelAttributesSchema = z.object({
address: addressSchema,
cityId: z.string(),
cityName: z.string(),
conferencesAndMeetings: facilitySchema.optional(), // TODO remove, use new /additionalData endpoint
contactInformation: contactInformationSchema,
detailedFacilities: z.array(detailedFacilitySchema).transform(
(facilities) =>
facilities
// Filter away facilities with ID:s that we don't recognize
.filter(
(f) => f.id !== undefined && f.id !== null && f.id in FacilityEnum
)
.sort((a, b) => b.sortOrder - a.sortOrder) as DetailedFacility[]
),
gallery: gallerySchema.optional(), // TODO remove, use new /additionalData endpoint
galleryImages: z.array(imageSchema).optional(),
healthFacilities: z.array(healthFacilitySchema),
hotelContent: hotelContentSchema,
hotelFacts: hotelFactsSchema,
hotelType: z.string().optional(),
isActive: z.boolean(),
isPublished: z.boolean(),
keywords: z.array(z.string()),
location: locationSchema,
merchantInformationData: merchantInformationSchema,
name: z.string(),
operaId: z.string(),
parking: z.array(parkingSchema),
pointsOfInterest: z
.array(pointOfInterestSchema)
.transform((pois) =>
pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
),
ratings: ratingsSchema,
rewardNight: rewardNightSchema,
restaurantImages: facilitySchema.optional(), // TODO remove, use new /additionalData endpoint
socialMedia: socialMediaSchema,
specialAlerts: specialAlertsSchema,
vat: z.number(),
})
const includedSchema = z
.array(z.union([roomSchema, restaurantSchema, additionalDataSchema]))
.transform((data) => {
const rooms = data.filter((d) => d.type === "roomcategories") as RoomData[]
const restaurants = data.filter(
(d) => d.type === "restaurants"
) as RestaurantData[]
const additionalData = data.filter(
(d) => d.type === "additionalData"
) as AdditionalData[]
return {
rooms,
restaurants,
additionalData,
}
})
import type { AdditionalData, City, NearbyHotel, Restaurant, Room } from "@/types/hotel"
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
export const getHotelDataSchema = z.object({
data: z.object({
id: z.string(),
type: z.literal("hotels"), // No enum here but the standard return appears to be "hotels".
language: z.string().transform((val) => {
const lang = toLang(val)
if (!lang) {
throw new Error("Invalid language")
}
return lang
export const hotelSchema = z
.object({
data: z.object({
attributes: attributesSchema,
id: z.string(),
language: z.string().transform((val) => {
const lang = toLang(val)
if (!lang) {
throw new Error("Invalid language")
}
return lang
}),
relationships: hotelRelationshipsSchema,
type: z.literal("hotels"), // No enum here but the standard return appears to be "hotels".
}),
attributes: hotelAttributesSchema,
relationships: relationshipsSchema,
}),
// NOTE: We can pass an "include" param to the hotel API to retrieve
// additional data for an individual hotel.
included: includedSchema.optional().transform((incl) => {
// NOTE: We can pass an "include" param to the hotel API to retrieve
// additional data for an individual hotel.
included: includesSchema,
})
.transform(({ data: { attributes, ...data }, included }) => {
const additionalData = included.find((inc): inc is AdditionalData => inc!.type === "additionalData")
const cities = included.filter((inc): inc is City => inc!.type === "cities")
const nearbyHotels = included.filter(
(inc): inc is NearbyHotel => inc!.type === "hotels"
)
const restaurants = included.filter(
(inc): inc is Restaurant => inc!.type === "restaurants"
)
const roomCategories = included.filter(
(inc): inc is Room => inc!.type === "roomcategories"
)
return {
restaurants: incl?.restaurants,
rooms: incl?.rooms,
additionalData: incl?.additionalData[0],
}
}),
})
export const childrenSchema = z.object({
age: z.number(),
bedType: z.nativeEnum(ChildBedTypeEnum),
})
const occupancySchema = z.object({
adults: z.number(),
children: z.array(childrenSchema).default([]),
})
const linksSchema = z.object({
links: z.array(
z.object({
url: z.string().url(),
type: z.string(),
})
),
})
export const priceSchema = z.object({
pricePerNight: z.coerce.number(),
pricePerStay: z.coerce.number(),
currency: z.string().default("N/A"),
})
export const productTypePriceSchema = z.object({
rateCode: z.string(),
rateType: z.string().optional(),
localPrice: priceSchema,
requestedPrice: priceSchema.optional(),
})
const productSchema = z.object({
productType: z.object({
public: productTypePriceSchema.default({
rateCode: "",
rateType: "",
localPrice: {
currency: "N/A",
pricePerNight: 0,
pricePerStay: 0,
additionalData,
cities,
hotel: {
...data,
...attributes,
},
requestedPrice: undefined,
}),
member: productTypePriceSchema.optional(),
}),
})
nearbyHotels,
restaurants,
roomCategories,
}
})
const hotelsAvailabilitySchema = z.object({
export const hotelsAvailabilitySchema = z.object({
data: z.array(
z.object({
attributes: z.object({
checkInDate: z.string(),
checkOutDate: z.string(),
occupancy: occupancySchema,
status: z.string(),
hotelId: z.number(),
productType: z
.object({
public: productTypePriceSchema.optional(),
member: productTypePriceSchema.optional(),
})
.optional(),
occupancy: occupancySchema,
productType: productTypeSchema,
status: z.string(),
}),
relationships: linksSchema.optional(),
relationships: relationshipsSchema.optional(),
type: z.string().optional(),
})
),
})
export const getHotelsAvailabilitySchema = hotelsAvailabilitySchema
export type HotelsAvailability = z.infer<typeof hotelsAvailabilitySchema>
export type ProductType =
HotelsAvailability["data"][number]["attributes"]["productType"]
export type ProductTypePrices = z.infer<typeof productTypePriceSchema>
export type HotelsAvailabilityItem =
HotelsAvailability["data"][number]["attributes"]
const roomConfigurationSchema = z.object({
status: z.string(),
roomTypeCode: z.string(),
roomType: z.string(),
roomsLeft: z.number(),
features: z
.array(
z.object({
inventory: z.number(),
code: z.enum([
RoomPackageCodeEnum.PET_ROOM,
RoomPackageCodeEnum.ALLERGY_ROOM,
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
]),
})
)
.default([]),
products: z.array(productSchema).default([]),
})
const rateDefinitionSchema = z.object({
title: z.string(),
breakfastIncluded: z.boolean(),
rateType: z.string().optional(),
rateCode: z.string(),
generalTerms: z.array(z.string()),
cancellationRule: z.string(),
cancellationText: z.string(),
mustBeGuaranteed: z.boolean(),
})
const roomsAvailabilitySchema = z
export const roomsAvailabilitySchema = z
.object({
data: z.object({
attributes: z.object({
checkInDate: z.string(),
checkOutDate: z.string(),
occupancy: occupancySchema.optional(),
hotelId: z.number(),
bookingCode: z.string().optional(),
roomConfigurations: z.array(roomConfigurationSchema),
rateDefinitions: z.array(rateDefinitionSchema),
mustBeGuaranteed: z.boolean().optional(),
occupancy: occupancySchema.optional(),
rateDefinitions: z.array(rateDefinitionSchema),
roomConfigurations: z.array(roomConfigurationSchema),
}),
relationships: linksSchema.optional(),
relationships: relationshipsSchema.optional(),
type: z.string().optional(),
}),
})
.transform((o) => o.data.attributes)
export const getRoomsAvailabilitySchema = roomsAvailabilitySchema
export type RoomsAvailability = z.infer<typeof roomsAvailabilitySchema>
export type RoomConfiguration = z.infer<typeof roomConfigurationSchema>
export type Product = z.infer<typeof productSchema>
export type RateDefinition = z.infer<typeof rateDefinitionSchema>
export const ratesSchema = z.array(rateSchema)
const flexibilityPrice = z.object({
standard: z.number(),
member: z.number(),
})
const rate = z.object({
id: z.number(),
name: z.string(),
description: z.string(),
size: z.string(),
imageSrc: z.string(),
breakfastIncluded: z.boolean(),
prices: z.object({
currency: z.string(),
nonRefundable: flexibilityPrice,
freeRebooking: flexibilityPrice,
freeCancellation: flexibilityPrice,
}),
})
export const getRatesSchema = z.array(rate)
export type Rate = z.infer<typeof rate>
const hotelFilter = z.object({
roomFacilities: z.array(z.string()),
hotelFacilities: z.array(z.string()),
hotelSurroundings: z.array(z.string()),
})
export const getFiltersSchema = hotelFilter
export type HotelFilter = z.infer<typeof hotelFilter>
export const apiCitiesByCountrySchema = z.object({
export const citiesByCountrySchema = z.object({
data: z.array(
z
.object({
attributes: z.object({
cityIdentifier: z.string().optional(),
name: z.string(),
keywords: z.array(z.string()).optional(),
timeZoneId: z.string().optional(),
ianaTimeZoneId: z.string().optional(),
isPublished: z.boolean().optional().default(false),
}),
id: z.string(),
type: z.literal("cities"),
})
.transform((data) => {
return {
...data.attributes,
id: data.id,
type: data.type,
}
})
citySchema.transform((data) => {
return {
...data.attributes,
id: data.id,
type: data.type,
}
})
),
})
export interface CitiesByCountry
extends z.output<typeof apiCitiesByCountrySchema> {}
export type CitiesGroupedByCountry = Record<string, CitiesByCountry["data"]>
export const apiCountriesSchema = z.object({
export const countriesSchema = z.object({
data: z
.array(
z.object({
@@ -689,35 +141,9 @@ export const apiCountriesSchema = z.object({
}),
})
export interface Countries extends z.output<typeof apiCountriesSchema> {}
export const apiLocationCitySchema = z.object({
attributes: z.object({
cityIdentifier: z.string().optional(),
keyWords: z.array(z.string()).optional(),
name: z.string().optional().default(""),
}),
country: z.string().optional().default(""),
id: z.string().optional().default(""),
type: z.literal("cities"),
})
export const apiCitySchema = z
export const citiesSchema = z
.object({
data: z.array(
z.object({
attributes: z.object({
cityIdentifier: z.string().optional(),
name: z.string(),
keywords: z.array(z.string()),
timeZoneId: z.string().optional(),
ianaTimeZoneId: z.string().optional(),
isPublished: z.boolean().optional().default(false),
}),
id: z.string().optional(),
type: z.literal("cities"),
})
),
data: z.array(citySchema),
})
.transform(({ data }) => {
if (data.length) {
@@ -731,46 +157,11 @@ export const apiCitySchema = z
return null
})
export const apiLocationHotelSchema = z.object({
attributes: z.object({
distanceToCentre: z.number().optional(),
images: z
.object({
large: z.string().optional(),
medium: z.string().optional(),
small: z.string().optional(),
tiny: z.string().optional(),
})
.optional(),
keyWords: z.array(z.string()).optional(),
name: z.string().optional().default(""),
operaId: z.string().optional(),
}),
id: z.string().optional().default(""),
relationships: z
.object({
city: z
.object({
links: z
.object({
related: z.string().optional(),
})
.optional(),
})
.optional(),
})
.optional(),
type: z.literal("hotels"),
})
export const apiLocationsSchema = z.object({
export const locationsSchema = z.object({
data: z
.array(
z
.discriminatedUnion("type", [
apiLocationCitySchema,
apiLocationHotelSchema,
])
.discriminatedUnion("type", [locationCitySchema, locationHotelSchema])
.transform((location) => {
if (location.type === "cities") {
return {
@@ -813,20 +204,6 @@ export const apiLocationsSchema = z.object({
),
})
export const packagePriceSchema = z.object({
currency: z.string().default("N/A"),
price: z.string(),
totalPrice: z.string(),
})
export const breakfastPackageSchema = z.object({
code: z.string(),
description: z.string(),
localPrice: packagePriceSchema,
requestedPrice: packagePriceSchema,
packageType: z.literal(PackageTypeEnum.BreakfastAdult),
})
export const breakfastPackagesSchema = z
.object({
data: z.object({
@@ -838,38 +215,23 @@ export const breakfastPackagesSchema = z
}),
})
.transform(({ data }) =>
data.attributes.packages.filter((pkg) => pkg.code.match(/^(BRF\d+)$/gm))
data.attributes.packages.filter((pkg) => pkg.code?.match(/^(BRF\d+)$/gm))
)
export const packagesSchema = z.object({
code: z.nativeEnum(RoomPackageCodeEnum),
itemCode: z.string().optional(),
description: z.string(),
localPrice: packagePriceSchema,
requestedPrice: packagePriceSchema,
inventories: z.array(
z.object({
date: z.string(),
total: z.number(),
available: z.number(),
})
),
})
export const getRoomPackagesSchema = z
export const packagesSchema = z
.object({
data: z
.object({
attributes: z.object({
hotelId: z.number(),
packages: z.array(packagesSchema).optional().default([]),
packages: z.array(packageSchema).default([]),
}),
relationships: z
.object({
links: z.array(
z.object({
url: z.string(),
type: z.string(),
url: z.string(),
})
),
})
@@ -878,7 +240,7 @@ export const getRoomPackagesSchema = z
})
.optional(),
})
.transform((data) => data.data?.attributes?.packages ?? [])
.transform(({ data }) => data?.attributes.packages)
export const getHotelIdsByCityIdSchema = z
.object({
@@ -900,50 +262,4 @@ export const getNearbyHotelIdsSchema = z
})
),
})
.transform((data) => data.data.map((hotel) => hotel.id))
export const getMeetingRoomsSchema = z.object({
data: z.array(
z.object({
attributes: z.object({
name: z.string(),
email: z.string().optional(),
phoneNumber: z.string(),
size: z.number(),
doorWidth: z.number(),
doorHeight: z.number(),
length: z.number(),
width: z.number(),
height: z.number(),
floorNumber: z.number(),
content: z.object({
images: z.array(imageSchema),
texts: z.object({
facilityInformation: z.string().optional(),
surroundingInformation: z.string().optional(),
descriptions: z.object({
short: z.string().optional(),
medium: z.string().optional(),
}),
meetingDescription: z
.object({
short: z.string().optional(),
medium: z.string().optional(),
})
.optional(),
}),
}),
seatings: z.array(
z.object({
type: z.string(),
capacity: z.number(),
})
),
lighting: z.string(),
sortOrder: z.number().optional(),
}),
id: z.string(),
type: z.string(),
})
),
})
.transform((data) => data.data.map((hotel) => hotel.id))

File diff suppressed because it is too large Load Diff

View File

@@ -60,8 +60,13 @@ export const additionalDataSchema = z
}),
type: z.literal("additionalData"),
})
.transform(({ attributes, type }) => ({
...attributes,
type,
id: attributes.id,
}))
export function transformAdditionalData(
data: z.output<typeof additionalDataSchema>
) {
return {
...data.attributes,
id: data.attributes.id,
type: data.type
}
}

View File

@@ -0,0 +1,13 @@
import { z } from "zod"
import { ChildBedTypeEnum } from "@/constants/booking"
export const childrenSchema = z.object({
age: z.number(),
bedType: z.nativeEnum(ChildBedTypeEnum),
})
export const occupancySchema = z.object({
adults: z.number(),
children: z.array(childrenSchema),
})

View File

@@ -0,0 +1,10 @@
import { z } from "zod"
import { productTypePriceSchema } from "../productTypePrice"
export const productTypeSchema = z
.object({
public: productTypePriceSchema.optional(),
member: productTypePriceSchema.optional(),
})
.optional()

View File

@@ -0,0 +1,14 @@
import { z } from "zod"
export const citySchema = z.object({
attributes: z.object({
cityIdentifier: z.string().default(""),
ianaTimeZoneId: z.string().default(""),
isPublished: z.boolean().default(false),
keywords: z.array(z.string()).default([]),
name: z.string(),
timeZoneId: z.string().default(""),
}),
id: z.string(),
type: z.literal("cities"),
})

View File

@@ -0,0 +1,73 @@
import { z } from "zod"
import { addressSchema } from "./hotel/address"
import { contactInformationSchema } from "./hotel/contactInformation"
import { hotelContentSchema } from "./hotel/content"
import { detailedFacilitiesSchema } from "./hotel/detailedFacility"
import { hotelFactsSchema } from "./hotel/facts"
import { gallerySchema } from "./hotel/gallery"
import { healthFacilitySchema } from "./hotel/healthFacilities"
import { includeSchema } from "./hotel/include/include"
import { locationSchema } from "./hotel/location"
import { merchantInformationSchema } from "./hotel/merchantInformation"
import { parkingSchema } from "./hotel/parking"
import { pointOfInterestsSchema } from "./hotel/poi"
import { ratingsSchema } from "./hotel/rating"
import { rewardNightSchema } from "./hotel/rewardNight"
import { socialMediaSchema } from "./hotel/socialMedia"
import { specialAlertsSchema } from "./hotel/specialAlerts"
import { specialNeedGroupSchema } from "./hotel/specialNeedGroups"
import { imageSchema } from "./image"
import { facilitySchema } from "./additionalData"
export const attributesSchema = z.object({
accessibilityElevatorPitchText: z.string().optional(),
address: addressSchema,
cityId: z.string(),
cityName: z.string(),
conferencesAndMeetings: facilitySchema.optional(),
contactInformation: contactInformationSchema,
detailedFacilities: detailedFacilitiesSchema,
gallery: gallerySchema.optional(),
galleryImages: z.array(imageSchema).optional(),
healthAndWellness: facilitySchema.optional(),
healthFacilities: z.array(healthFacilitySchema),
hotelContent: hotelContentSchema,
hotelFacts: hotelFactsSchema,
hotelRoomElevatorPitchText: z.string().optional(),
hotelType: z.string().optional(),
isActive: z.boolean(),
isPublished: z.boolean(),
keywords: z.array(z.string()),
location: locationSchema,
merchantInformationData: merchantInformationSchema,
name: z.string(),
operaId: z.string(),
parking: z.array(parkingSchema),
pointsOfInterest: pointOfInterestsSchema,
ratings: ratingsSchema,
rewardNight: rewardNightSchema,
restaurantImages: facilitySchema.optional(),
socialMedia: socialMediaSchema,
specialAlerts: specialAlertsSchema,
specialNeedGroups: z.array(specialNeedGroupSchema),
vat: z.number(),
})
export const includesSchema = z
.array(includeSchema)
.default([])
.transform((data) => data.filter((item) => !!item))
const relationshipSchema = z.object({
links: z.object({
related: z.string(),
}),
})
export const relationshipsSchema = z.object({
meetingRooms: relationshipSchema,
nearbyHotels: relationshipSchema,
restaurants: relationshipSchema,
roomCategories: relationshipSchema,
})

View File

@@ -0,0 +1,8 @@
import { z } from "zod"
export const addressSchema = z.object({
city: z.string(),
country: z.string(),
streetAddress: z.string(),
zipCode: z.string(),
})

View File

@@ -0,0 +1,8 @@
import { z } from "zod"
export const contactInformationSchema = z.object({
email: z.string(),
faxNumber: z.string().optional(),
phoneNumber: z.string(),
websiteUrl: z.string(),
})

View File

@@ -0,0 +1,40 @@
import { z } from "zod"
import { imageSchema } from "../image"
export const hotelContentSchema = z.object({
images: imageSchema.default({
metaData: {
altText: "default image",
altText_En: "default image",
copyRight: "default image",
title: "default image",
},
imageSizes: {
large: "https://placehold.co/1280x720",
medium: "https://placehold.co/1280x720",
small: "https://placehold.co/1280x720",
tiny: "https://placehold.co/1280x720",
},
}),
restaurantsOverviewPage: z.object({
restaurantsContentDescriptionMedium: z.string().optional(),
restaurantsContentDescriptionShort: z.string().optional(),
restaurantsOverviewPageLink: z.string().optional(),
restaurantsOverviewPageLinkText: z.string().optional(),
}),
texts: z.object({
descriptions: z.object({
medium: z.string(),
short: z.string(),
}),
facilityInformation: z.string().optional(),
meetingDescription: z
.object({
medium: z.string().optional(),
short: z.string().optional(),
})
.optional(),
surroundingInformation: z.string(),
}),
})

View File

@@ -0,0 +1,18 @@
import { z } from "zod"
import { FacilityEnum } from "@/types/enums/facilities"
const detailedFacilitySchema = z.object({
filter: z.string().optional(),
icon: z.string().optional(),
id: z.nativeEnum(FacilityEnum),
name: z.string(),
public: z.boolean(),
sortOrder: z.number(),
})
export const detailedFacilitiesSchema = z
.array(detailedFacilitySchema)
.transform((facilities) =>
facilities.sort((a, b) => b.sortOrder - a.sortOrder)
)

View File

@@ -0,0 +1,80 @@
import { z } from "zod"
const ecoLabelsSchema = z.object({
euEcoLabel: z.boolean(),
greenGlobeLabel: z.boolean(),
nordicEcoLabel: z.boolean(),
svanenEcoLabelCertificateNumber: z.string().optional(),
})
export const checkinSchema = z.object({
checkInTime: z.string(),
checkOutTime: z.string(),
onlineCheckout: z.boolean(),
onlineCheckOutAvailableFrom: z.string().nullable().optional(),
})
const hotelFacilityDetailSchema = z
.object({
description: z.string(),
heading: z.string(),
})
.optional()
/** Possibly more values */
const hotelFacilityDetailsSchema = z.object({
breakfast: hotelFacilityDetailSchema,
checkout: hotelFacilityDetailSchema,
gym: hotelFacilityDetailSchema,
internet: hotelFacilityDetailSchema,
laundry: hotelFacilityDetailSchema,
luggage: hotelFacilityDetailSchema,
shop: hotelFacilityDetailSchema,
telephone: hotelFacilityDetailSchema,
})
const hotelInformationSchema = z
.object({
description: z.string(),
heading: z.string(),
link: z.string().optional(),
})
.optional()
const hotelInformationsSchema = z.object({
accessibility: hotelInformationSchema,
safety: hotelInformationSchema,
sustainability: hotelInformationSchema,
})
const interiorSchema = z.object({
numberOfBeds: z.number(),
numberOfCribs: z.number(),
numberOfFloors: z.number(),
numberOfRooms: z.object({
connected: z.number(),
forAllergics: z.number().optional(),
forDisabled: z.number(),
nonSmoking: z.number(),
pet: z.number(),
withExtraBeds: z.number(),
total: z.number(),
}),
})
const receptionHoursSchema = z.object({
alwaysOpen: z.boolean(),
isClosed: z.boolean(),
openingTime: z.string().optional(),
closingTime: z.string().optional(),
})
export const hotelFactsSchema = z.object({
checkin: checkinSchema,
ecoLabels: ecoLabelsSchema,
hotelFacilityDetail: hotelFacilityDetailsSchema.default({}),
hotelInformation: hotelInformationsSchema.default({}),
interior: interiorSchema,
receptionHours: receptionHoursSchema,
yearBuilt: z.string(),
})

View File

@@ -0,0 +1,8 @@
import { z } from "zod"
import { imageSchema } from "../image"
export const gallerySchema = z.object({
heroImages: z.array(imageSchema),
smallerImages: z.array(imageSchema),
})

View File

@@ -0,0 +1,41 @@
import { z } from "zod"
import { imageSchema } from "../image"
const healthFacilitiesOpenHoursSchema = z.object({
alwaysOpen: z.boolean(),
closingTime: z.string().optional(),
isClosed: z.boolean(),
openingTime: z.string().optional(),
sortOrder: z.number().optional(),
})
export const healthFacilitySchema = z.object({
content: z.object({
images: z.array(imageSchema),
texts: z.object({
descriptions: z.object({
short: z.string(),
medium: z.string(),
}),
facilityInformation: z.string().optional(),
surroundingInformation: z.string().optional(),
}),
}),
details: z.array(
z.object({
name: z.string(),
type: z.string(),
value: z.string().optional(),
})
),
openingDetails: z.object({
manualOpeningHours: z.string().optional(),
openingHours: z.object({
ordinary: healthFacilitiesOpenHoursSchema,
weekends: healthFacilitiesOpenHoursSchema,
}),
useManualOpeningHours: z.boolean(),
}),
type: z.string(),
})

View File

@@ -0,0 +1,37 @@
import { z } from "zod"
import { citySchema } from "@/server/routers/hotels/schemas/city"
import { nearbyHotelsSchema } from "@/server/routers/hotels/schemas/hotel/include/nearbyHotels"
import { restaurantsSchema } from "@/server/routers/hotels/schemas/hotel/include/restaurants"
import {
roomCategoriesSchema,
transformRoomCategories,
} from "@/server/routers/hotels/schemas/hotel/include/roomCategories"
import { additionalDataSchema, transformAdditionalData } from "../../additionalData"
export const includeSchema = z
.discriminatedUnion("type", [
additionalDataSchema,
citySchema,
nearbyHotelsSchema,
restaurantsSchema,
roomCategoriesSchema,
])
.transform((data) => {
switch (data.type) {
case "additionalData":
return transformAdditionalData(data)
case "cities":
case "hotels":
case "restaurants":
return {
...data.attributes,
id: data.id,
type: data.type,
}
case "roomcategories":
return transformRoomCategories(data)
default:
return null
}
})

View File

@@ -0,0 +1,41 @@
import { z } from "zod"
import { attributesSchema } from "@/server/routers/hotels/schemas/hotel"
export const nearbyHotelsSchema = z.object({
attributes: z.lazy(() =>
z
.object({
displayWebPage: z
.object({
healthGym: z.boolean().default(false),
meetingRoom: z.boolean().default(false),
parking: z.boolean().default(false),
specialNeeds: z.boolean().default(false),
})
.default({
healthGym: false,
meetingRoom: false,
parking: false,
specialNeeds: false,
}),
})
.merge(
attributesSchema.pick({
address: true,
cityId: true,
cityName: true,
detailedFacilities: true,
hotelContent: true,
isActive: true,
isPublished: true,
location: true,
name: true,
operaId: true,
ratings: true,
})
)
),
id: z.string(),
type: z.literal("hotels"),
})

View File

@@ -0,0 +1,72 @@
import { z } from "zod"
import { imageSchema } from "@/server/routers/hotels/schemas/image"
import { CurrencyEnum } from "@/types/enums/currency"
const contentSchema = z.object({
images: z.array(imageSchema).default([]),
texts: z.object({
descriptions: z.object({
medium: z.string().default(""),
short: z.string().default(""),
}),
}),
})
const externalBreakfastSchema = z.object({
isAvailable: z.boolean().default(false),
localPriceForExternalGuests: z.object({
amount: z.number().default(0),
currency: z.nativeEnum(CurrencyEnum).default(CurrencyEnum.SEK),
}),
})
const menuItemSchema = z.object({
name: z.string(),
url: z.string().url(),
})
const daySchema = z.object({
alwaysOpen: z.boolean().default(false),
closingTime: z.string().default(""),
isClosed: z.boolean().default(false),
openingTime: z.string().default(""),
sortOrder: z.number().int().default(0),
})
const openingDetailsSchema = z.object({
alternateOpeningHours: z.object({
isActive: z.boolean().default(false),
}),
openingHours: z.object({
friday: daySchema,
isActive: z.boolean().default(false),
monday: daySchema,
name: z.string().default(""),
saturday: daySchema,
sunday: daySchema,
thursday: daySchema,
tuesday: daySchema,
wednesday: daySchema,
}),
})
export const restaurantsSchema = z.object({
attributes: z.object({
bookTableUrl: z.string().default(""),
content: contentSchema,
// When using .email().default("") is not sufficent
// so .optional also needs to be chained
email: z.string().email().optional(),
externalBreakfast: externalBreakfastSchema,
isPublished: z.boolean().default(false),
menus: z.array(menuItemSchema).default([]),
name: z.string().default(""),
openingDetails: z.array(openingDetailsSchema).default([]),
restaurantPage: z.boolean().default(false),
specialAlerts: z.array(z.object({})).default([]),
}),
id: z.string(),
type: z.literal("restaurants"),
})

View File

@@ -0,0 +1,97 @@
import { z } from "zod"
import { imageMetaDataSchema, imageSizesSchema } from "../../image"
const minMaxSchema = z.object({
max: z.number(),
min: z.number(),
})
const bedTypeSchema = z.object({
description: z.string().default(""),
type: z.string(),
widthRange: minMaxSchema,
})
const occupancySchema = z.object({
adults: z.number(),
children: z.number(),
total: z.number(),
})
const roomContentSchema = z.object({
images: z.array(
z.object({
imageSizes: imageSizesSchema,
metaData: imageMetaDataSchema,
})
),
texts: z.object({
descriptions: z.object({
medium: z.string().optional(),
short: z.string().optional(),
}),
}),
})
const roomTypesSchema = z.object({
code: z.string(),
description: z.string(),
fixedExtraBed: bedTypeSchema,
isLackingCribs: z.boolean(),
isLackingExtraBeds: z.boolean(),
mainBed: bedTypeSchema,
name: z.string(),
occupancy: occupancySchema,
roomCount: z.number(),
roomSize: minMaxSchema,
})
const roomFacilitiesSchema = z.object({
availableInAllRooms: z.boolean(),
icon: z.string().optional(),
isUniqueSellingPoint: z.boolean(),
name: z.string(),
sortOrder: z.number(),
})
export const roomCategoriesSchema = z.object({
attributes: z.object({
content: roomContentSchema,
name: z.string(),
occupancy: minMaxSchema,
roomFacilities: z.array(roomFacilitiesSchema),
roomSize: minMaxSchema,
roomTypes: z.array(roomTypesSchema),
sortOrder: z.number(),
}),
id: z.string(),
type: z.literal("roomcategories"),
})
export function transformRoomCategories(
data: z.output<typeof roomCategoriesSchema>
) {
return {
descriptions: data.attributes.content.texts.descriptions,
id: data.id,
images: data.attributes.content.images,
name: data.attributes.name,
occupancy: data.attributes.occupancy,
roomFacilities: data.attributes.roomFacilities,
roomSize: data.attributes.roomSize,
roomTypes: data.attributes.roomTypes,
sortOrder: data.attributes.sortOrder,
type: data.type,
totalOccupancy:
data.attributes.occupancy.min === data.attributes.occupancy.max
? {
max: data.attributes.occupancy.max,
range: `${data.attributes.occupancy.max}`,
}
: {
max: data.attributes.occupancy.max,
range: `${data.attributes.occupancy.min}-${data.attributes.occupancy.max}`,
},
}
}

View File

@@ -0,0 +1,7 @@
import { z } from "zod"
export const locationSchema = z.object({
distanceToCentre: z.number(),
latitude: z.number(),
longitude: z.number(),
})

View File

@@ -0,0 +1,21 @@
import { z } from "zod"
import type { PaymentMethodEnum } from "@/constants/booking"
export const merchantInformationSchema = z.object({
alternatePaymentOptions: z
.record(z.string(), z.boolean())
.transform((val) => {
return Object.entries(val)
.filter(([_, enabled]) => enabled)
.map(([key]) => key)
.filter((key): key is PaymentMethodEnum => !!key)
}),
cards: z.record(z.string(), z.boolean()).transform((val) => {
return Object.entries(val)
.filter(([_, enabled]) => enabled)
.map(([key]) => key)
.filter((key): key is PaymentMethodEnum => !!key)
}),
webMerchantId: z.string(),
})

View File

@@ -0,0 +1,41 @@
import { z } from "zod"
const periodSchema = z.object({
amount: z.number().optional(),
endTime: z.string().optional(),
period: z.string().optional(),
startTime: z.string().optional(),
})
const currencySchema = z
.object({
currency: z.string().optional(),
ordinary: z.array(periodSchema).optional(),
range: z
.object({
min: z.number().optional(),
max: z.number().optional(),
})
.optional(),
weekend: z.array(periodSchema).optional(),
})
.optional()
const pricingSchema = z.object({
freeParking: z.boolean(),
localCurrency: currencySchema,
paymentType: z.string().optional(),
requestedCurrency: currencySchema,
})
export const parkingSchema = z.object({
address: z.string().optional(),
canMakeReservation: z.boolean(),
distanceToHotel: z.number().optional(),
externalParkingUrl: z.string().optional(),
name: z.string().optional(),
numberOfChargingSpaces: z.number().optional(),
numberOfParkingSpots: z.number().optional(),
pricing: pricingSchema,
type: z.string().optional(),
})

View File

@@ -0,0 +1,32 @@
import { z } from "zod"
import { getPoiGroupByCategoryName } from "../../utils"
import { locationSchema } from "./location"
export const pointOfInterestSchema = z
.object({
category: z.object({
name: z.string().optional(),
group: z.string().optional(),
}),
distance: z.number().optional(),
isHighlighted: z.boolean().optional(),
location: locationSchema.optional(),
name: z.string().optional(),
})
.transform((poi) => ({
categoryName: poi.category.name,
coordinates: {
lat: poi.location?.latitude ?? 0,
lng: poi.location?.longitude ?? 0,
},
distance: poi.distance,
group: getPoiGroupByCategoryName(poi.category.name),
name: poi.name,
}))
export const pointOfInterestsSchema = z
.array(pointOfInterestSchema)
.transform((pois) =>
pois.sort((a, b) => (a.distance ?? 0) - (b.distance ?? 0))
)

View File

@@ -0,0 +1,31 @@
import { z } from "zod"
const awardSchema = z.object({
displayName: z.string(),
images: z.object({
large: z.string(),
medium: z.string(),
small: z.string(),
}),
})
const reviewsSchema = z
.object({
widgetHtmlTagId: z.string(),
widgetScriptEmbedUrlIframe: z.string(),
widgetScriptEmbedUrlJavaScript: z.string(),
})
.optional()
export const ratingsSchema = z
.object({
tripAdvisor: z.object({
awards: z.array(awardSchema),
numberOfReviews: z.number(),
rating: z.number(),
ratingImageUrl: z.string(),
reviews: reviewsSchema,
webUrl: z.string(),
}),
})
.optional()

View File

@@ -0,0 +1,10 @@
import { z } from "zod"
export const rewardNightSchema = z.object({
campaign: z.object({
end: z.string(),
points: z.number(),
start: z.string(),
}),
points: z.number(),
})

View File

@@ -0,0 +1,6 @@
import { z } from "zod"
export const socialMediaSchema = z.object({
facebook: z.string().optional(),
instagram: z.string().optional(),
})

View File

@@ -0,0 +1,35 @@
import { z } from "zod"
import { dt } from "@/lib/dt"
import { AlertTypeEnum } from "@/types/enums/alert"
const specialAlertSchema = z.object({
description: z.string().optional(),
displayInBookingFlow: z.boolean(),
endDate: z.string().optional(),
startDate: z.string().optional(),
title: z.string().optional(),
type: z.string(),
})
export const specialAlertsSchema = z
.array(specialAlertSchema)
.transform((data) => {
const now = dt().utc().format("YYYY-MM-DD")
const filteredAlerts = data.filter((alert) => {
const shouldShowNow =
alert.startDate && alert.endDate
? alert.startDate <= now && alert.endDate >= now
: true
const hasText = alert.description || alert.title
return shouldShowNow && hasText
})
return filteredAlerts.map((alert, idx) => ({
heading: alert.title || null,
id: `alert-${alert.type}-${idx}`,
text: alert.description || null,
type: AlertTypeEnum.Info,
}))
})
.default([])

View File

@@ -0,0 +1,11 @@
import { z } from "zod"
const specialNeedSchema = z.object({
details: z.string(),
name: z.string(),
})
export const specialNeedGroupSchema = z.object({
name: z.string(),
specialNeeds: z.array(specialNeedSchema),
})

View File

@@ -0,0 +1,7 @@
import { z } from "zod"
export const hotelFilterSchema = z.object({
hotelFacilities: z.array(z.string()),
hotelSurroundings: z.array(z.string()),
roomFacilities: z.array(z.string()),
})

View File

@@ -1,17 +1,17 @@
import { z } from "zod"
const imageSizesSchema = z.object({
tiny: z.string(),
small: z.string(),
medium: z.string(),
export const imageSizesSchema = z.object({
large: z.string(),
medium: z.string(),
small: z.string(),
tiny: z.string(),
})
const imageMetaDataSchema = z.object({
title: z.string(),
export const imageMetaDataSchema = z.object({
altText: z.string(),
altText_En: z.string(),
copyRight: z.string(),
title: z.string(),
})
const DEFAULT_IMAGE_OBJ = {

View File

@@ -0,0 +1,12 @@
import { z } from "zod"
export const locationCitySchema = z.object({
attributes: z.object({
cityIdentifier: z.string().optional(),
keyWords: z.array(z.string()).optional(),
name: z.string().optional().default(""),
}),
country: z.string().optional().default(""),
id: z.string().optional().default(""),
type: z.literal("cities"),
})

View File

@@ -0,0 +1,33 @@
import { z } from "zod"
export const locationHotelSchema = z.object({
attributes: z.object({
distanceToCentre: z.number().optional(),
images: z
.object({
large: z.string().optional(),
medium: z.string().optional(),
small: z.string().optional(),
tiny: z.string().optional(),
})
.optional(),
keyWords: z.array(z.string()).optional(),
name: z.string().optional().default(""),
operaId: z.string().optional(),
}),
id: z.string().optional().default(""),
relationships: z
.object({
city: z
.object({
links: z
.object({
related: z.string().optional(),
})
.optional(),
})
.optional(),
})
.optional(),
type: z.literal("hotels"),
})

View File

@@ -0,0 +1,50 @@
import { z } from "zod"
import { imageSchema } from "./image"
export const meetingRoomsSchema = z
.object({
data: z.array(
z.object({
attributes: z.object({
name: z.string(),
email: z.string().optional(),
phoneNumber: z.string(),
size: z.number(),
doorWidth: z.number(),
doorHeight: z.number(),
length: z.number(),
width: z.number(),
height: z.number(),
floorNumber: z.number(),
content: z.object({
images: z.array(imageSchema),
texts: z.object({
facilityInformation: z.string().optional(),
surroundingInformation: z.string().optional(),
descriptions: z.object({
short: z.string().optional(),
medium: z.string().optional(),
}),
meetingDescription: z
.object({
short: z.string().optional(),
medium: z.string().optional(),
})
.optional(),
}),
}),
seatings: z.array(
z.object({
type: z.string(),
capacity: z.number(),
})
),
lighting: z.string(),
sortOrder: z.number().optional(),
}),
id: z.string(),
type: z.string(),
})
),
})

View File

@@ -1,16 +1,9 @@
import { z } from "zod"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { PackageTypeEnum } from "@/types/enums/packages"
export const getRoomPackagesInputSchema = z.object({
hotelId: z.string(),
startDate: z.string(),
endDate: z.string(),
adults: z.number(),
children: z.number().optional().default(0),
packageCodes: z.array(z.string()).optional().default([]),
})
// TODO: Remove optional and default when the API change has been deployed
export const packagePriceSchema = z
.object({
currency: z.string().default("N/A"),
@@ -22,40 +15,27 @@ export const packagePriceSchema = z
currency: "N/A",
price: "0",
totalPrice: "0",
}) // TODO: Remove optional and default when the API change has been deployed
})
export const packagesSchema = z.object({
const inventorySchema = z.object({
date: z.string(),
total: z.number(),
available: z.number(),
})
export const packageSchema = z.object({
code: z.nativeEnum(RoomPackageCodeEnum),
description: z.string(),
inventories: z.array(inventorySchema),
itemCode: z.string().default(""),
localPrice: packagePriceSchema,
requestedPrice: packagePriceSchema,
})
export const breakfastPackageSchema = z.object({
code: z.string(),
description: z.string(),
localPrice: packagePriceSchema,
requestedPrice: packagePriceSchema,
inventories: z.array(
z.object({
date: z.string(),
total: z.number(),
available: z.number(),
})
),
packageType: z.literal(PackageTypeEnum.BreakfastAdult),
})
export const getRoomPackagesSchema = z
.object({
data: z.object({
attributes: z.object({
hotelId: z.number(),
packages: z.array(packagesSchema).default([]),
}),
relationships: z
.object({
links: z.array(
z.object({
url: z.string(),
type: z.string(),
})
),
})
.optional(),
type: z.string(),
}),
})
.transform((data) => data.data.attributes.packages)

View File

@@ -0,0 +1,16 @@
import { z } from "zod"
import { CurrencyEnum } from "@/types/enums/currency"
export const priceSchema = z.object({
currency: z.nativeEnum(CurrencyEnum),
pricePerNight: z.coerce.number(),
pricePerStay: z.coerce.number(),
})
export const productTypePriceSchema = z.object({
localPrice: priceSchema,
rateCode: z.string(),
rateType: z.string().optional(),
requestedPrice: priceSchema.optional(),
})

View File

@@ -0,0 +1,21 @@
import { z } from "zod"
const flexibilityPrice = z.object({
member: z.number(),
standard: z.number(),
})
export const rateSchema = z.object({
breakfastIncluded: z.boolean(),
description: z.string(),
id: z.number(),
imageSrc: z.string(),
name: z.string(),
prices: z.object({
currency: z.string(),
freeCancellation: flexibilityPrice,
freeRebooking: flexibilityPrice,
nonRefundable: flexibilityPrice,
}),
size: z.string(),
})

View File

@@ -0,0 +1,10 @@
import { z } from "zod"
export const relationshipsSchema = z.object({
links: z.array(
z.object({
type: z.string(),
url: z.string().url(),
})
),
})

View File

@@ -0,0 +1,23 @@
import { z } from "zod"
import { productSchema } from "./product"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
export const roomConfigurationSchema = z.object({
features: z.array(
z.object({
inventory: z.number(),
code: z.enum([
RoomPackageCodeEnum.PET_ROOM,
RoomPackageCodeEnum.ALLERGY_ROOM,
RoomPackageCodeEnum.ACCESSIBILITY_ROOM,
]),
})
),
products: z.array(productSchema),
roomsLeft: z.number(),
roomType: z.string(),
roomTypeCode: z.string(),
status: z.string(),
})

View File

@@ -0,0 +1,21 @@
import { z } from "zod"
import { productTypePriceSchema } from "../productTypePrice"
import { CurrencyEnum } from "@/types/enums/currency"
export const productSchema = z.object({
productType: z.object({
member: productTypePriceSchema.optional(),
public: productTypePriceSchema.default({
localPrice: {
currency: CurrencyEnum.SEK,
pricePerNight: 0,
pricePerStay: 0,
},
rateCode: "",
rateType: "",
requestedPrice: undefined,
}),
}),
})

View File

@@ -0,0 +1,12 @@
import { z } from "zod"
export const rateDefinitionSchema = z.object({
breakfastIncluded: z.boolean(),
cancellationRule: z.string(),
cancellationText: z.string(),
generalTerms: z.array(z.string()),
mustBeGuaranteed: z.boolean(),
rateCode: z.string(),
rateType: z.string().optional(),
title: z.string(),
})

View File

@@ -4,25 +4,24 @@ import { unstable_cache } from "next/cache"
import * as api from "@/lib/api"
import {
apiCitiesByCountrySchema,
apiCitySchema,
apiCountriesSchema,
apiLocationsSchema,
type CitiesGroupedByCountry,
citiesByCountrySchema,
citiesSchema,
countriesSchema,
getHotelIdsByCityIdSchema,
locationsSchema,
} from "./output"
import {
getHotelIdsCounter,
getHotelIdsFailCounter,
getHotelIdsSuccessCounter,
} from "./telemetry"
import type { Country } from "@/types/enums/country"
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
import type { RequestOptionsWithOutBody } from "@/types/fetch"
import { PointOfInterestGroupEnum } from "@/types/hotel"
import type { HotelLocation } from "@/types/trpc/routers/hotel/locations"
import type {
CitiesGroupedByCountry,
Countries,
HotelLocation,
} from "@/types/trpc/routers/hotel/locations"
import type { Lang } from "@/constants/languages"
import type { Endpoint } from "@/lib/api/endpoints"
import { Country } from "@/types/enums/country"
import { metrics } from "./metrics"
export function getPoiGroupByCategoryName(category: string | undefined) {
if (!category) return PointOfInterestGroupEnum.LOCATION
@@ -75,7 +74,7 @@ export async function getCity(
}
const cityJson = await cityResponse.json()
const city = apiCitySchema.safeParse(cityJson)
const city = citiesSchema.safeParse(cityJson)
if (!city.success) {
console.info(`Validation of city failed`)
console.info(`cityUrl: ${locationCityUrl}`)
@@ -108,7 +107,7 @@ export async function getCountries(
}
const countriesJson = await countryResponse.json()
const countries = apiCountriesSchema.safeParse(countriesJson)
const countries = countriesSchema.safeParse(countriesJson)
if (!countries.success) {
console.info(`Validation for countries failed`)
console.error(countries.error)
@@ -149,8 +148,7 @@ export async function getCitiesByCountry(
}
const countryJson = await countryResponse.json()
const citiesByCountry =
apiCitiesByCountrySchema.safeParse(countryJson)
const citiesByCountry = citiesByCountrySchema.safeParse(countryJson)
if (!citiesByCountry.success) {
console.info(`Failed to validate Cities by Country payload`)
console.error(citiesByCountry.error)
@@ -200,7 +198,7 @@ export async function getLocations(
}
const apiJson = await apiResponse.json()
const verifiedLocations = apiLocationsSchema.safeParse(apiJson)
const verifiedLocations = locationsSchema.safeParse(apiJson)
if (!verifiedLocations.success) {
console.info(`Locations Verification Failed`)
console.error(verifiedLocations.error)
@@ -273,7 +271,7 @@ export async function getHotelIdsByCityId(
) {
return unstable_cache(
async function (params: URLSearchParams) {
getHotelIdsCounter.add(1, { params: params.toString() })
metrics.hotelIds.counter.add(1, { params: params.toString() })
console.info(
"api.hotel.hotel-ids start",
JSON.stringify({ params: params.toString() })
@@ -286,7 +284,7 @@ export async function getHotelIdsByCityId(
if (!apiResponse.ok) {
const responseMessage = await apiResponse.text()
getHotelIdsFailCounter.add(1, {
metrics.hotelIds.fail.add(1, {
params: params.toString(),
error_type: "http_error",
error: responseMessage,
@@ -309,7 +307,7 @@ export async function getHotelIdsByCityId(
const apiJson = await apiResponse.json()
const validatedHotelIds = getHotelIdsByCityIdSchema.safeParse(apiJson)
if (!validatedHotelIds.success) {
getHotelIdsFailCounter.add(1, {
metrics.hotelIds.fail.add(1, {
params: params.toString(),
error_type: "validation_error",
error: JSON.stringify(validatedHotelIds.error),
@@ -324,7 +322,7 @@ export async function getHotelIdsByCityId(
return null
}
getHotelIdsSuccessCounter.add(1, { cityId })
metrics.hotelIds.success.add(1, { cityId })
console.info(
"api.hotel.hotel-ids success",
JSON.stringify({ params: params.toString() })
@@ -344,7 +342,7 @@ export async function getHotelIdsByCountry(
) {
return unstable_cache(
async function (params: URLSearchParams) {
getHotelIdsCounter.add(1, { country })
metrics.hotelIds.counter.add(1, { country })
console.info(
"api.hotel.hotel-ids start",
JSON.stringify({ query: { country } })
@@ -357,7 +355,7 @@ export async function getHotelIdsByCountry(
if (!apiResponse.ok) {
const responseMessage = await apiResponse.text()
getHotelIdsFailCounter.add(1, {
metrics.hotelIds.fail.add(1, {
country,
error_type: "http_error",
error: responseMessage,
@@ -380,7 +378,7 @@ export async function getHotelIdsByCountry(
const apiJson = await apiResponse.json()
const validatedHotelIds = getHotelIdsByCityIdSchema.safeParse(apiJson)
if (!validatedHotelIds.success) {
getHotelIdsFailCounter.add(1, {
metrics.hotelIds.fail.add(1, {
country,
error_type: "validation_error",
error: JSON.stringify(validatedHotelIds.error),
@@ -395,7 +393,7 @@ export async function getHotelIdsByCountry(
return null
}
getHotelIdsSuccessCounter.add(1, { country })
metrics.hotelIds.success.add(1, { country })
console.info(
"api.hotel.hotel-ids success",
JSON.stringify({ query: { country } })

View File

@@ -1,6 +1,6 @@
import { meetingRoomsSchema } from "@/server/routers/hotels/schemas/meetingRoom"
import type { z } from "zod"
import type { getMeetingRoomsSchema } from "@/server/routers/hotels/output"
export type MeetingRoomData = z.infer<typeof getMeetingRoomsSchema>
export type MeetingRoomData = z.output<typeof meetingRoomsSchema>
export type MeetingRooms = MeetingRoomData["data"]

View File

@@ -1,10 +1,10 @@
import type { RoomData } from "@/types/hotel"
import type { Room } from "@/types/hotel"
export interface RoomCardProps {
room: RoomData
room: Room
}
export type RoomsProps = {
rooms: RoomData[]
preamble?: string
rooms: Room[]
}

View File

@@ -1,6 +1,6 @@
import type {
Hotel,
RestaurantData,
Restaurant,
RestaurantOpeningHours,
} from "@/types/hotel"
import type { ParkingAmenityProps } from "./parking"
@@ -10,7 +10,7 @@ export type AmenitiesSidePeekProps = {
parking: ParkingAmenityProps
checkInInformation: Hotel["hotelFacts"]["checkin"]
accessibility: Hotel["hotelFacts"]["hotelInformation"]["accessibility"]
restaurants: RestaurantData[]
restaurants: Restaurant[]
}
export type FilteredAmenitiesProps = {

View File

@@ -1,27 +1,29 @@
import type { Hotel } from "@/types/hotel"
import type { Hotel, Parking } from "@/types/hotel"
export enum Periods {
allDay = "AllDay",
day = "Day",
night = "Night",
allDay = "AllDay",
}
export type ParkingAmenityProps = {
hasExtraParkingPage: boolean
parking: Hotel["parking"]
parkingElevatorPitch: string
hasExtraParkingPage: boolean
}
export type ParkingListProps = {
numberOfChargingSpaces: Hotel["parking"][number]["numberOfChargingSpaces"]
canMakeReservation: Hotel["parking"][number]["canMakeReservation"]
numberOfParkingSpots: Hotel["parking"][number]["numberOfParkingSpots"]
distanceToHotel: Hotel["parking"][number]["distanceToHotel"]
address: Hotel["parking"][number]["address"]
}
export interface ParkingListProps
extends Pick<
Parking,
| "address"
| "canMakeReservation"
| "distanceToHotel"
| "numberOfChargingSpaces"
| "numberOfParkingSpots"
> { }
export type ParkingPricesProps = {
pricing: Hotel["parking"][number]["pricing"]["localCurrency"]["ordinary"]
currency: Hotel["parking"][number]["pricing"]["localCurrency"]["currency"]
freeParking: Hotel["parking"][number]["pricing"]["freeParking"]
export interface ParkingPricesProps
extends Pick<Parking["pricing"], "freeParking">,
Pick<NonNullable<Parking["pricing"]["localCurrency"]>, "currency"> {
pricing: NonNullable<Parking["pricing"]["localCurrency"]>["ordinary"]
}

View File

@@ -1,9 +1,9 @@
import type { RestaurantData } from "@/types/hotel"
import type { Restaurant } from "@/types/hotel"
export interface RestaurantBarSidePeekProps {
restaurants: RestaurantData[]
restaurants: Restaurant[]
}
export interface RestaurantBarItemProps {
restaurant: RestaurantData
restaurant: Restaurant
}

View File

@@ -1,5 +1,5 @@
import type { RoomData } from "@/types/hotel"
import type { Room } from "@/types/hotel"
export interface RoomSidePeekProps {
room: RoomData
room: Room
}

View File

@@ -4,4 +4,4 @@ export interface BookingConfirmationProps {
confirmationNumber: string
}
export interface ConfirmationProps extends BookingConfirmation {}
export interface ConfirmationProps extends BookingConfirmation { }

View File

@@ -1,19 +1,17 @@
import { type z } from "zod"
import type { z } from "zod"
import type { breakfastFormSchema } from "@/components/HotelReservation/EnterDetails/Breakfast/schema"
import type {
breakfastPackageSchema,
breakfastPackagesSchema,
} from "@/server/routers/hotels/output"
import type { breakfastPackagesSchema } from "@/server/routers/hotels/output"
import type { breakfastPackageSchema } from "@/server/routers/hotels/schemas/packages"
export interface BreakfastFormSchema
extends z.output<typeof breakfastFormSchema> {}
extends z.output<typeof breakfastFormSchema> { }
export interface BreakfastPackages
extends z.output<typeof breakfastPackagesSchema> {}
extends z.output<typeof breakfastPackagesSchema> { }
export interface BreakfastPackage
extends z.output<typeof breakfastPackageSchema> {}
extends z.output<typeof breakfastPackageSchema> { }
export interface BreakfastProps {
packages: BreakfastPackages

View File

@@ -1,7 +1,5 @@
import type { RouterOutput } from "@/lib/trpc/client"
type HotelDataGet = RouterOutput["hotel"]["hotelData"]["get"]
import type { HotelData } from "@/types/hotel"
export interface HotelHeaderProps {
hotelData: NonNullable<HotelDataGet>
hotelData: HotelData
}

View File

@@ -1,7 +1,7 @@
import { RoomConfiguration } from "@/server/routers/hotels/output"
import type { RoomConfiguration } from "@/types/trpc/routers/hotel/roomAvailability"
export interface SelectedRoomProps {
hotelId: string
room: RoomConfiguration
rateDescription: string
room: RoomConfiguration
}

View File

@@ -1,5 +1,5 @@
import type { Child } from "@/types/components/hotelReservation/selectRate/selectRate"
import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability"
import type { RoomAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
export interface ClientSummaryProps
extends Pick<

View File

@@ -1,6 +1,6 @@
import type { ProductType } from "@/server/routers/hotels/output"
import type { ProductType } from "@/types/trpc/routers/hotel/availability"
export type HotelPriceListProps = {
price: ProductType
hotelId: string
price: ProductType
}

View File

@@ -1,21 +1,21 @@
import type { Hotel } from "@/types/hotel"
import type { ProductType } from "@/server/routers/hotels/output"
import type { ProductType } from "@/types/trpc/routers/hotel/availability"
export enum HotelCardListingTypeEnum {
MapListing = "mapListing",
PageListing = "pageListing",
}
export type HotelCardListingProps = {
hotelData: HotelData[]
type?: HotelCardListingTypeEnum
}
export type HotelData = {
hotelData: Hotel
price: ProductType
}
export type HotelCardListingProps = {
hotelData: HotelData[]
type?: HotelCardListingTypeEnum
}
export interface NullableHotelData extends Omit<HotelData, "hotelData"> {
hotelData: HotelData["hotelData"] | null
}

View File

@@ -1,4 +1,4 @@
import { ProductTypePrices } from "@/server/routers/hotels/output"
import type { ProductTypePrices } from "@/types/trpc/routers/hotel/availability"
export type PriceCardProps = {
productTypePrices: ProductTypePrices

View File

@@ -1,10 +1,9 @@
import type { CheckInData, Hotel, ParkingData } from "@/types/hotel"
import type { HotelLocation } from "@/types/trpc/routers/hotel/locations"
import type { Lang } from "@/constants/languages"
import type {
AlternativeHotelsSearchParams,
SelectHotelSearchParams,
} from "./selectHotelSearchParams"
import type { CheckInData, Hotel, Parking } from "@/types/hotel"
import type { Lang } from "@/constants/languages"
export enum AvailabilityEnum {
Available = "Available",
@@ -23,7 +22,7 @@ export interface ContactProps {
}
export interface ParkingProps {
parking: ParkingData[]
parking: Parking[]
}
export interface AccessibilityProps {

View File

@@ -1,11 +1,13 @@
import type { z } from "zod"
import type {
priceSchema,
Product,
productTypePriceSchema,
RoomConfiguration,
} from "@/server/routers/hotels/output"
} from "@/types/trpc/routers/hotel/roomAvailability"
import type {
priceSchema,
productTypePriceSchema,
} from "@/server/routers/hotels/schemas/productTypePrice"
import type { RoomPackage } from "./roomFilter"
type ProductPrice = z.output<typeof productTypePriceSchema>

View File

@@ -0,0 +1,11 @@
import type { Lang } from "@/constants/languages"
import type { Child } from "./selectRate"
export interface HotelInfoCardProps {
adultCount: number
childArray?: Child[]
fromDate: Date
hotelId: number
lang: Lang
toDate: Date
}

View File

@@ -1,9 +1,9 @@
import type { RoomsAvailability } from "@/server/routers/hotels/output"
import type { RoomPackageData } from "./roomFilter"
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
import type { RoomPackages } from "./roomFilter"
import type { Rate } from "./selectRate"
export interface RateSummaryProps {
isUserLoggedIn: boolean
packages: RoomPackageData | undefined
packages: RoomPackages | undefined
roomsAvailability: RoomsAvailability
}

View File

@@ -1,23 +1,25 @@
import type { z } from "zod"
import type { RoomData } from "@/types/hotel"
import type { Room } from "@/types/hotel"
import type {
packagePriceSchema,
RateDefinition,
RoomConfiguration,
} from "@/server/routers/hotels/output"
} from "@/types/trpc/routers/hotel/roomAvailability"
import type { packagePriceSchema } from "@/server/routers/hotels/schemas/packages"
import type { RoomPriceSchema } from "./flexibilityOption"
import type { RoomPackageCodes, RoomPackageData } from "./roomFilter"
import type { RoomPackageCodes, RoomPackages } from "./roomFilter"
import type { RateCode } from "./selectRate"
export type RoomCardProps = {
hotelId: string
hotelType: string | undefined
roomConfiguration: RoomConfiguration
rateDefinitions: RateDefinition[]
roomCategories: RoomData[]
roomCategories: Room[]
selectedPackages: RoomPackageCodes[]
packages: RoomPackageData | undefined
roomListIndex: number
packages: RoomPackages | undefined
handleSelectRate: React.Dispatch<React.SetStateAction<RateCode | undefined>>
}
type RoomPackagePriceSchema = z.output<typeof packagePriceSchema>

View File

@@ -1,6 +1,6 @@
import type { z } from "zod"
import type { packagesSchema } from "@/server/routers/hotels/output"
import type { packageSchema } from "@/server/routers/hotels/schemas/packages"
export enum RoomPackageCodeEnum {
PET_ROOM = "PETR",
@@ -24,7 +24,6 @@ export interface RoomFilterProps {
roomListIndex: number
}
export type RoomPackage = z.output<typeof packagesSchema>
export interface RoomPackageData extends Array<RoomPackage> {}
export type RoomPackage = z.output<typeof packageSchema>
export type RoomPackageCodes = RoomPackage["code"]
export type RoomPackages = RoomPackage[]

View File

@@ -1,31 +1,32 @@
import type { RoomData } from "@/types/hotel"
import type { RoomsAvailability } from "@/server/routers/hotels/output"
import type {
DefaultFilterOptions,
RoomPackage,
RoomPackageCodes,
RoomPackageData,
RoomPackages,
} from "./roomFilter"
import type { Room } from "@/types/hotel"
import type { RoomsAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
export interface RoomTypeListProps {
roomsAvailability: RoomsAvailability
roomCategories: RoomData[]
availablePackages: RoomPackageData | undefined
roomCategories: Room[]
availablePackages: RoomPackage | undefined
selectedPackages: RoomPackageCodes[]
hotelType: string | undefined
roomListIndex: number
}
export interface SelectRateProps {
roomsAvailability: RoomsAvailability
roomCategories: RoomData[]
availablePackages: RoomPackageData
availablePackages: RoomPackages
hotelType: string | undefined
isUserLoggedIn: boolean
roomsAvailability: RoomsAvailability
roomCategories: Room[]
}
export interface RoomSelectionPanelProps {
roomCategories: RoomData[]
roomCategories: Room[]
availablePackages: RoomPackage[]
selectedPackages: RoomPackageCodes[]
hotelType: string | undefined

View File

@@ -0,0 +1,11 @@
import type { Lang } from "@/constants/languages"
import type { Child } from "./selectRate"
export interface RoomsContainerProps {
adultCount: number
childArray?: Child[]
fromDate: Date
hotelId: number
lang: Lang
toDate: Date
}

View File

@@ -1,4 +1,7 @@
import type { Product, RoomConfiguration } from "@/server/routers/hotels/output"
import type {
Product,
RoomConfiguration,
} from "@/types/trpc/routers/hotel/roomAvailability"
import type { ChildBedMapEnum } from "../../bookingWidget/enums"
import type { RoomPackageCodeEnum } from "./roomFilter"

View File

@@ -1,10 +1,10 @@
import { Hotel } from "@/types/hotel"
import { HotelData } from "@/types/hotel"
export enum SidePeekEnum {
hotelDetails = "hotel-detail-side-peek",
roomDetails = "room-detail-side-peek",
}
export type SidePeekProps = {
hotel: Hotel
export type HotelReservationSidePeekProps = {
hotel: HotelData | null
}

View File

@@ -5,11 +5,11 @@ import type {
Price,
RoomPrice,
} from "@/types/stores/enter-details"
import type { RoomAvailability } from "@/types/trpc/routers/hotel/availability"
import type { BedTypeSchema } from "./enterDetails/bedType"
import type { BreakfastPackage } from "./enterDetails/breakfast"
import type { DetailsSchema } from "./enterDetails/details"
import type { Child, SelectRateSearchParams } from "./selectRate/selectRate"
import type { RoomAvailability } from "@/types/trpc/routers/hotel/roomAvailability"
export type RoomsData = Pick<DetailsState, "roomPrice"> &
Pick<RoomAvailability, "cancellationText" | "rateDetails"> &
@@ -21,7 +21,7 @@ export type RoomsData = Pick<DetailsState, "roomPrice"> &
export interface SummaryProps
extends Pick<RoomAvailability, "cancellationText" | "rateDetails">,
Pick<RoomAvailability["selectedRoom"], "roomType"> {
Pick<RoomAvailability["selectedRoom"], "roomType"> {
isMember: boolean
breakfastIncluded: boolean
}

View File

@@ -1,8 +1,7 @@
import { poiVariants } from "@/components/Maps/Markers/Poi/variants"
import type { VariantProps } from "class-variance-authority"
import type { PointOfInterestGroupEnum } from "@/types/hotel"
import type { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
import type { poiVariants } from "@/components/Maps/Markers/Poi/variants"
export interface PoiMarkerProps extends VariantProps<typeof poiVariants> {
group: PointOfInterestGroupEnum

View File

@@ -1,9 +1,8 @@
import { SidePeekEnum } from "../hotelReservation/sidePeek"
import type { RoomData } from "@/types/hotel"
import type { Room } from "@/types/hotel"
import type { SidePeekEnum } from "../hotelReservation/sidePeek"
export type RoomSidePeekProps = {
room: RoomData
room: Room
activeSidePeek: SidePeekEnum | null
close: () => void
}

View File

@@ -0,0 +1,8 @@
export enum PointOfInterestGroupEnum {
PUBLIC_TRANSPORT = "Public transport",
ATTRACTIONS = "Attractions",
BUSINESS = "Business",
LOCATION = "Location",
PARKING = "Parking",
SHOPPING_DINING = "Shopping & Dining",
}

Some files were not shown because too many files have changed in this diff Show More