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
503 lines
12 KiB
TypeScript
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>
|