feat(SW-1012): Added possibility for multiple include params for hotels

This commit is contained in:
Erik Tiekstra
2024-11-26 11:07:47 +01:00
committed by Fredrik Thorsson
parent 92bbfcf533
commit 05006506f0
17 changed files with 218 additions and 57 deletions

View File

@@ -0,0 +1 @@
export default function RestaurantSidepeek() {}

View File

@@ -0,0 +1,32 @@
import { restaurantAndBar } from "@/constants/routes/hotelPageParams"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext"
import styles from "./restaurantBar.module.css"
import type { RestaurantBarSidePeekProps } from "@/types/components/hotelPage/sidepeek/restaurantBar"
export default async function RestaurantBarSidePeek({
restaurants,
}: RestaurantBarSidePeekProps) {
const lang = getLang()
const intl = await getIntl()
return (
<SidePeek
contentKey={restaurantAndBar[lang]}
title={intl.formatMessage({ id: "Restaurant & Bar" })}
>
<div className={styles.content}>
{restaurants.map((restaurant) => (
<div key={restaurant.id}>
<h3>{restaurant.name}</h3>
</div>
))}
</div>
</SidePeek>
)
}

View File

@@ -1,7 +1,6 @@
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { restaurantAndBar } from "@/constants/routes/hotelPageParams"
import { env } from "@/env/server"
import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests"
@@ -21,6 +20,7 @@ import MapCard from "./Map/MapCard"
import MapWithCardWrapper from "./Map/MapWithCard"
import MobileMapToggle from "./Map/MobileMapToggle"
import StaticMap from "./Map/StaticMap"
import RestaurantBarSidePeek from "./SidePeeks/RestaurantBar"
import AmenitiesList from "./AmenitiesList"
import Facilities from "./Facilities"
import IntroSection from "./IntroSection"
@@ -78,8 +78,8 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
ratings,
parking,
} = hotelData.data.attributes
const roomCategories =
hotelData.included?.filter((item) => item.type === "roomcategories") || []
const roomCategories = hotelData.included?.rooms || []
const restaurants = hotelData.included?.restaurants || []
const images = gallery?.smallerImages
const description = hotelContent.texts.descriptions.medium
const activitiesCard = content?.[0]?.upcoming_activities_card || null
@@ -197,14 +197,8 @@ export default async function HotelPage({ hotelId }: HotelPageProps) {
ecoLabels={hotelFacts.ecoLabels}
descriptions={hotelContent.texts}
/>
<SidePeek
contentKey={restaurantAndBar[lang]}
title={intl.formatMessage({ id: "Restaurant & Bar" })}
>
{/* TODO */}
Restaurant & Bar
</SidePeek>
<WellnessAndExerciseSidePeek healthFacilities={healthFacilities} />
<RestaurantBarSidePeek restaurants={restaurants} />
{activitiesCard && (
<ActivitiesSidePeek contentPage={activitiesCard.contentPage} />
)}

View File

@@ -95,7 +95,7 @@ export async function RoomsContainer({
user={user}
availablePackages={packages ?? []}
roomsAvailability={roomsAvailability}
roomCategories={hotelData?.included ?? []}
roomCategories={hotelData?.included?.rooms ?? []}
hotelType={hotelData?.data.attributes?.hotelType}
/>
)

View File

@@ -32,7 +32,7 @@ export default function HotelReservationSidePeek({
}
)
const selectedRoom = hotelData?.included?.find((room) =>
const selectedRoom = hotelData?.included?.rooms?.find((room) =>
room.roomTypes.some((type) => type.code === roomTypeCode)
)

View File

@@ -30,11 +30,20 @@ const wrappedFetch = fetchRetry(fetch, {
export async function get(
endpoint: Endpoint,
options: RequestOptionsWithOutBody,
params = {}
params: Record<string, any> = {}
) {
const url = new URL(env.API_BASEURL)
url.pathname = endpoint
url.search = new URLSearchParams(params).toString()
const searchParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach((val) => searchParams.append(key, val))
} else {
searchParams.set(key, value)
}
})
url.search = searchParams.toString()
return wrappedFetch(
url,
merge.all([defaultOptions, { method: "GET" }, options])

View File

@@ -13,6 +13,10 @@ import type {
HotelDataInput,
} from "@/server/routers/hotels/input"
import type { GetSavedPaymentCardsInput } from "@/server/routers/user/input"
import type {
BreackfastPackagesInput,
PackagesInput,
} from "@/types/requests/packages"
export const getLocations = cache(async function getMemoizedLocations() {
return serverClient().hotel.locations.get()

View File

@@ -51,6 +51,10 @@ export const getHotelDataInputSchema = z.object({
isCardOnlyPayment: z.boolean().optional(),
})
export const getRestaurantsInputSchema = z.object({
hotelId: z.string(),
})
export type HotelDataInput = z.input<typeof getHotelDataInputSchema>
export const getBreakfastPackageInputSchema = z.object({

View File

@@ -4,15 +4,21 @@ import { ChildBedTypeEnum, type PaymentMethodEnum } from "@/constants/booking"
import { dt } from "@/lib/dt"
import { toLang } from "@/server/utils"
import { imageMetaDataSchema, imageSizesSchema } from "./schemas/image"
import {
imageMetaDataSchema,
imageSchema,
imageSizesSchema,
} from "./schemas/image"
import { restaurantSchema } from "./schemas/restaurants"
import { roomSchema } from "./schemas/room"
import { specialAlertsSchema } from "./schemas/specialAlerts"
import { getPoiGroupByCategoryName } from "./utils"
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
import { AlertTypeEnum } from "@/types/enums/alert"
import { CurrencyEnum } from "@/types/enums/currency"
import { FacilityEnum } from "@/types/enums/facilities"
import { PackageTypeEnum } from "@/types/enums/packages"
import { RestaurantData, RoomData } from "@/types/hotel"
const ratingsSchema = z
.object({
@@ -159,11 +165,6 @@ export const facilitySchema = z.object({
),
})
export const imageSchema = z.object({
metaData: imageMetaDataSchema,
imageSizes: imageSizesSchema,
})
export const gallerySchema = z.object({
heroImages: z.array(imageSchema),
smallerImages: z.array(imageSchema),
@@ -332,36 +333,6 @@ const socialMediaSchema = z.object({
facebook: z.string().optional(),
})
const specialAlertSchema = z.object({
type: z.string(),
title: z.string().optional(),
description: z.string().optional(),
displayInBookingFlow: z.boolean(),
startDate: z.string().optional(),
endDate: z.string().optional(),
})
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) => ({
id: `alert-${alert.type}-${idx}`,
type: AlertTypeEnum.Info,
heading: alert.title || null,
text: alert.description || null,
}))
})
.default([])
const relationshipsSchema = z.object({
restaurants: z.object({
links: z.object({
@@ -487,6 +458,19 @@ export const hotelAttributesSchema = z.object({
specialNeedGroups: z.array(specialNeedGroupSchema),
})
const includedSchema = z
.array(z.union([roomSchema, restaurantSchema]))
.transform((data) => {
const rooms = data.filter((d) => d.type === "roomcategories") as RoomData[]
const restaurants = data.filter(
(d) => d.type === "restaurants"
) as RestaurantData[]
return {
rooms,
restaurants,
}
})
// NOTE: Find schema at: https://aks-test.scandichotels.com/hotel/swagger/v1/index.html
export const getHotelDataSchema = z.object({
data: z.object({
@@ -504,7 +488,7 @@ export const getHotelDataSchema = z.object({
}),
// NOTE: We can pass an "include" param to the hotel API to retrieve
// additional data for an individual hotel.
included: z.array(roomSchema).optional(),
included: includedSchema.optional(),
})
export const childrenSchema = z.object({

View File

@@ -51,6 +51,14 @@ const getHotelCounter = meter.createCounter("trpc.hotel.get")
const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success")
const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail")
const getRestaurantsCounter = meter.createCounter("trpc.hotel.restaurants.get")
const getRestaurantsSuccessCounter = meter.createCounter(
"trpc.hotel.restaurants.get-success"
)
const getRestaurantsFailCounter = meter.createCounter(
"trpc.hotel.restaurants.get-fail"
)
const getPackagesCounter = meter.createCounter("trpc.hotel.packages.get")
const getPackagesSuccessCounter = meter.createCounter(
"trpc.hotel.packages.get-success"
@@ -101,12 +109,12 @@ export const getHotelData = cache(
async (input: HotelDataInput, serviceToken: string) => {
const { hotelId, language, isCardOnlyPayment } = input
const params: Record<string, string> = {
const params: Record<string, string | string[]> = {
hotelId,
language,
}
params.include = "RoomCategories" // "RoomCategories","NearbyHotels","Restaurants","City",
params.include = ["RoomCategories", "Restaurants"] // "RoomCategories","NearbyHotels","Restaurants","City",
getHotelCounter.add(1, {
hotelId,
@@ -780,7 +788,7 @@ export const hotelQueryRouter = router({
Authorization: `Bearer ${ctx.serviceToken}`,
},
},
params
searchParams
)
if (!apiResponse.ok) {

View File

@@ -13,3 +13,8 @@ export const imageMetaDataSchema = z.object({
altText_En: z.string(),
copyRight: z.string(),
})
export const imageSchema = z.object({
metaData: imageMetaDataSchema,
imageSizes: imageSizesSchema,
})

View File

@@ -0,0 +1,80 @@
import { z } from "zod"
import { imageSchema } from "./image"
import { specialAlertsSchema } from "./specialAlerts"
const restaurantPriceSchema = z.object({
currency: z.string(),
amount: z.number(),
})
const restaurantDaySchema = z.object({
sortOrder: z.number(),
alwaysOpen: z.boolean(),
isClosed: z.boolean(),
openingTime: z.string(),
closingTime: z.string(),
})
const restaurantOpeningHoursSchema = z.object({
isActive: z.boolean(),
name: z.string().optional(),
openingTime: z.string().optional(),
closingTime: z.string().optional(),
monday: restaurantDaySchema.optional(),
tuesday: restaurantDaySchema.optional(),
wednesday: restaurantDaySchema.optional(),
thursday: restaurantDaySchema.optional(),
friday: restaurantDaySchema.optional(),
saturday: restaurantDaySchema.optional(),
sunday: restaurantDaySchema.optional(),
})
const restaurantOpeningDetailSchema = z.object({
openingHours: restaurantOpeningHoursSchema,
alternateOpeningHours: restaurantOpeningHoursSchema.optional(),
})
export const restaurantSchema = z
.object({
attributes: z.object({
name: z.string().optional(),
isPublished: z.boolean(),
email: z.string().optional(),
phoneNumber: z.string().optional(),
externalBreakfast: z.object({
isAvailable: z.boolean(),
localPriceForExternalGuests: restaurantPriceSchema.optional(),
requestedPriceForExternalGuests: restaurantPriceSchema.optional(),
}),
menus: z
.array(
z.object({
name: z.string(),
url: z.string(),
})
)
.optional(),
openingDetails: z.array(restaurantOpeningDetailSchema),
content: z.object({
images: z.array(imageSchema),
texts: z.object({
descriptions: z.object({
short: z.string(),
medium: z.string(),
}),
}),
bookTableUrl: z.string().optional(),
specialAlerts: specialAlertsSchema,
}),
}),
id: z.string(),
type: z.literal("restaurants"),
})
.transform(({ attributes, id, type }) => ({ ...attributes, id, type }))
export const getRestaurantsSchema = z
.object({
data: z.array(restaurantSchema),
})
.transform(({ data }) => {
return data.filter((item) => !!item.isPublished)
})

View File

@@ -78,7 +78,7 @@ export const roomSchema = z
}),
}),
id: z.string(),
type: z.enum(["roomcategories"]),
type: z.literal("roomcategories"),
})
.transform((data) => {
return {

View File

@@ -0,0 +1,33 @@
import { dt } from "@/lib/dt"
import { AlertTypeEnum } from "@/types/enums/alert"
import { z } from "zod"
const specialAlertSchema = z.object({
type: z.string(),
title: z.string().optional(),
description: z.string().optional(),
displayInBookingFlow: z.boolean(),
startDate: z.string().optional(),
endDate: z.string().optional(),
})
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) => ({
id: `alert-${alert.type}-${idx}`,
type: AlertTypeEnum.Info,
heading: alert.title || null,
text: alert.description || null,
}))
})
.default([])

View File

@@ -0,0 +1,5 @@
import type { RestaurantData } from "@/types/hotel"
export interface RestaurantBarSidePeekProps {
restaurants: RestaurantData[]
}

View File

@@ -4,10 +4,11 @@ import {
checkinSchema,
facilitySchema,
getHotelDataSchema,
imageSchema,
parkingSchema,
pointOfInterestSchema,
} from "@/server/routers/hotels/output"
import { imageSchema } from "@/server/routers/hotels/schemas/image"
import { restaurantSchema } from "@/server/routers/hotels/schemas/restaurants"
import { roomSchema } from "@/server/routers/hotels/schemas/room"
export type HotelData = z.infer<typeof getHotelDataSchema>
@@ -23,6 +24,7 @@ export type HotelTripAdvisor =
| undefined
export type RoomData = z.infer<typeof roomSchema>
export type RestaurantData = z.infer<typeof restaurantSchema>
export type GalleryImage = z.infer<typeof imageSchema>
export type CheckInData = z.infer<typeof checkinSchema>