Files
web/server/routers/hotels/output.ts
Niclas Edenvin f178f7fde0 Merged in feature/select-room-ux-one-page (pull request #523)
This updates the select room page according to the new UX. It has different sections on the same page, but with specific URLs per section. Since neither UX, UI nor API is completely done both design and data structures are a bit temporary.

Approved-by: Simon.Emanuelsson
2024-08-29 13:38:14 +00:00

503 lines
12 KiB
TypeScript

import { z } from "zod"
import { toLang } from "@/server/utils"
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(),
})
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 hotelFacilityDetailSchema = z.object({
heading: z.string(),
description: z.string(),
})
const hotelFacilitySchema = z.object({
breakfast: hotelFacilityDetailSchema,
checkout: hotelFacilityDetailSchema,
gym: hotelFacilityDetailSchema,
internet: hotelFacilityDetailSchema,
laundry: hotelFacilityDetailSchema,
luggage: hotelFacilityDetailSchema,
shop: hotelFacilityDetailSchema,
telephone: hotelFacilityDetailSchema,
})
const hotelInformationDetailSchema = z.object({
heading: z.string(),
description: z.string(),
link: z.string().optional(),
})
const hotelInformationSchema = z.object({
accessibility: hotelInformationDetailSchema,
safety: hotelInformationDetailSchema,
sustainability: hotelInformationDetailSchema,
})
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 imageMetaDataSchema = z.object({
title: z.string(),
altText: z.string(),
altText_En: z.string(),
copyRight: z.string(),
})
const imageSizesSchema = z.object({
tiny: z.string(),
small: z.string(),
medium: z.string(),
large: z.string(),
})
const hotelContentSchema = z.object({
images: z.object({
metaData: imageMetaDataSchema,
imageSizes: imageSizesSchema,
}),
texts: z.object({
facilityInformation: z.string(),
surroundingInformation: z.string(),
descriptions: z.object({
short: z.string(),
medium: z.string(),
}),
}),
restaurantsOverviewPage: z.object({
restaurantsOverviewPageLinkText: z.string(),
restaurantsOverviewPageLink: z.string(),
restaurantsContentDescriptionShort: z.string(),
restaurantsContentDescriptionMedium: z.string(),
}),
})
const detailedFacilitySchema = z.object({
id: z.number(),
name: z.string(),
code: z.string().optional(),
applyToAllHotels: z.boolean(),
public: z.boolean(),
icon: z.string(),
iconName: z.string().optional(),
sortOrder: z.number(),
})
const healthFacilitySchema = z.object({
type: z.string(),
content: z.object({
images: z.array(
z.object({
metaData: imageMetaDataSchema,
imageSizes: imageSizesSchema,
})
),
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(),
}),
})
const pointsOfInterestSchema = z.object({
name: z.string(),
distance: z.number(),
category: z.object({
name: z.string(),
group: z.string(),
}),
location: locationSchema,
isHighlighted: z.boolean(),
})
const parkingPricingSchema = z.object({
freeParking: z.boolean(),
paymentType: z.string(),
localCurrency: z.object({
currency: z.string(),
range: z.object({
min: z.number(),
max: z.number().optional(),
}),
ordinary: z.array(
z.object({
period: z.string(),
amount: z.number().optional(),
startTime: z.string(),
endTime: z.string(),
})
),
weekend: z.array(
z.object({
period: z.string(),
amount: z.number().optional(),
startTime: z.string(),
endTime: z.string(),
})
),
}),
requestedCurrency: z
.object({
currency: z.string(),
range: z.object({
min: z.number(),
max: z.number(),
}),
ordinary: z.array(
z.object({
period: z.string(),
amount: z.number(),
startTime: z.string(),
endTime: z.string(),
})
),
weekend: z.array(
z.object({
period: z.string(),
amount: z.number(),
startTime: z.string(),
endTime: z.string(),
})
),
})
.optional(),
})
const parkingSchema = z.object({
type: z.string(),
name: z.string(),
address: z.string(),
numberOfParkingSpots: z.number(),
numberOfChargingSpaces: z.number(),
distanceToHotel: z.number(),
canMakeReservation: z.boolean(),
pricing: parkingPricingSchema,
})
const specialNeedSchema = z.object({
name: z.string(),
details: z.string(),
})
const specialNeedGroupSchema = z.object({
name: z.string(),
specialNeeds: z.array(specialNeedSchema),
})
const socialMediaSchema = z.object({
instagram: z.string().optional(),
facebook: z.string().optional(),
})
const metaSpecialAlertSchema = z.object({
type: z.string(),
description: z.string().optional(),
displayInBookingFlow: z.boolean(),
startDate: z.string(),
endDate: z.string(),
})
const metaSchema = z.object({
specialAlerts: z.array(metaSpecialAlertSchema),
})
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 roomContentSchema = z.object({
images: z.array(
z.object({
metaData: imageMetaDataSchema,
imageSizes: imageSizesSchema,
})
),
texts: z.object({
descriptions: z.object({
short: z.string(),
medium: z.string(),
}),
}),
})
const roomTypesSchema = z.object({
name: z.string(),
description: z.string(),
code: z.string(),
roomCount: z.number(),
mainBed: z.object({
type: z.string(),
description: z.string(),
widthRange: z.object({
min: z.number(),
max: z.number(),
}),
}),
fixedExtraBed: z.object({
type: z.string(),
description: z.string().optional(),
widthRange: z.object({
min: z.number(),
max: z.number(),
}),
}),
roomSize: z.object({
min: z.number(),
max: z.number(),
}),
occupancy: z.object({
total: z.number(),
adults: z.number(),
children: z.number(),
}),
isLackingCribs: z.boolean(),
isLackingExtraBeds: z.boolean(),
})
const roomFacilitiesSchema = z.object({
availableInAllRooms: z.boolean(),
name: z.string(),
isUniqueSellingPoint: z.boolean(),
sortOrder: z.number(),
})
export const roomSchema = z.object({
attributes: z.object({
name: z.string(),
sortOrder: z.number(),
content: roomContentSchema,
roomTypes: z.array(roomTypesSchema),
roomFacilities: z.array(roomFacilitiesSchema),
occupancy: z.object({
total: z.number(),
adults: z.number(),
children: z.number(),
}),
roomSize: z.object({
min: z.number(),
max: z.number(),
}),
}),
id: z.string(),
type: z.enum(["roomcategories"]),
})
// 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.string(), // 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
}),
attributes: z.object({
name: z.string(),
operaId: z.string(),
keywords: z.array(z.string()),
isPublished: z.boolean(),
cityId: z.string(),
cityName: z.string(),
ratings: ratingsSchema,
address: addressSchema,
contactInformation: contactInformationSchema,
hotelFacts: z.object({
checkin: checkinSchema,
ecoLabels: ecoLabelsSchema,
hotelFacilityDetail: hotelFacilitySchema,
hotelInformation: hotelInformationSchema,
interior: interiorSchema,
receptionHours: receptionHoursSchema,
yearBuilt: z.string(),
}),
location: locationSchema,
hotelContent: hotelContentSchema,
detailedFacilities: z.array(detailedFacilitySchema),
healthFacilities: z.array(healthFacilitySchema),
rewardNight: rewardNightSchema,
pointsOfInterest: z.array(pointsOfInterestSchema),
parking: z.array(parkingSchema),
specialNeedGroups: z.array(specialNeedGroupSchema),
socialMedia: socialMediaSchema,
meta: metaSchema.optional(),
isActive: z.boolean(),
}),
relationships: relationshipsSchema,
}),
// NOTE: We can pass an "include" param to the hotel API to retrieve
// additional data for an individual hotel.
included: z.array(roomSchema).optional(),
})
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>