diff --git a/components/ContentType/HotelPage/SidePeeks/RestaurantBar/Restaurant/index.tsx b/components/ContentType/HotelPage/SidePeeks/RestaurantBar/Restaurant/index.tsx new file mode 100644 index 000000000..b7be3faa4 --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/RestaurantBar/Restaurant/index.tsx @@ -0,0 +1 @@ +export default function RestaurantSidepeek() {} diff --git a/components/ContentType/HotelPage/SidePeeks/RestaurantBar/index.tsx b/components/ContentType/HotelPage/SidePeeks/RestaurantBar/index.tsx new file mode 100644 index 000000000..9b1e2501a --- /dev/null +++ b/components/ContentType/HotelPage/SidePeeks/RestaurantBar/index.tsx @@ -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 ( + +
+ {restaurants.map((restaurant) => ( +
+

{restaurant.name}

+
+ ))} +
+
+ ) +} diff --git a/components/ContentType/HotelPage/SidePeeks/RestaurantBar/restaurantBar.module.css b/components/ContentType/HotelPage/SidePeeks/RestaurantBar/restaurantBar.module.css new file mode 100644 index 000000000..e69de29bb diff --git a/components/ContentType/HotelPage/index.tsx b/components/ContentType/HotelPage/index.tsx index a80662003..fd6d4497d 100644 --- a/components/ContentType/HotelPage/index.tsx +++ b/components/ContentType/HotelPage/index.tsx @@ -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} /> - - {/* TODO */} - Restaurant & Bar - + {activitiesCard && ( )} diff --git a/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx b/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx index aafdfa0ab..d8570e159 100644 --- a/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx +++ b/components/HotelReservation/SelectRate/Rooms/RoomsContainer.tsx @@ -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} /> ) diff --git a/components/HotelReservation/SidePeek/index.tsx b/components/HotelReservation/SidePeek/index.tsx index 54702cd68..6329df6ed 100644 --- a/components/HotelReservation/SidePeek/index.tsx +++ b/components/HotelReservation/SidePeek/index.tsx @@ -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) ) diff --git a/lib/api/index.ts b/lib/api/index.ts index 5842b5597..62ae6a8a2 100644 --- a/lib/api/index.ts +++ b/lib/api/index.ts @@ -30,11 +30,20 @@ const wrappedFetch = fetchRetry(fetch, { export async function get( endpoint: Endpoint, options: RequestOptionsWithOutBody, - params = {} + params: Record = {} ) { 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]) diff --git a/lib/trpc/memoizedRequests/index.ts b/lib/trpc/memoizedRequests/index.ts index 22d1ac385..006c5f626 100644 --- a/lib/trpc/memoizedRequests/index.ts +++ b/lib/trpc/memoizedRequests/index.ts @@ -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() diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 4a66009b3..0a252e351 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -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 export const getBreakfastPackageInputSchema = z.object({ diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 1bffd3543..868c17ad0 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -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({ diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index 4b40ecc33..ff3964db1 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -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 = { + const params: Record = { 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) { diff --git a/server/routers/hotels/schemas/image.ts b/server/routers/hotels/schemas/image.ts index 296530316..03f092d3a 100644 --- a/server/routers/hotels/schemas/image.ts +++ b/server/routers/hotels/schemas/image.ts @@ -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, +}) diff --git a/server/routers/hotels/schemas/restaurants.ts b/server/routers/hotels/schemas/restaurants.ts new file mode 100644 index 000000000..0a65a60e3 --- /dev/null +++ b/server/routers/hotels/schemas/restaurants.ts @@ -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) + }) diff --git a/server/routers/hotels/schemas/room.ts b/server/routers/hotels/schemas/room.ts index 5141086ed..bf8947412 100644 --- a/server/routers/hotels/schemas/room.ts +++ b/server/routers/hotels/schemas/room.ts @@ -78,7 +78,7 @@ export const roomSchema = z }), }), id: z.string(), - type: z.enum(["roomcategories"]), + type: z.literal("roomcategories"), }) .transform((data) => { return { diff --git a/server/routers/hotels/schemas/specialAlerts.ts b/server/routers/hotels/schemas/specialAlerts.ts new file mode 100644 index 000000000..9d5dbd63c --- /dev/null +++ b/server/routers/hotels/schemas/specialAlerts.ts @@ -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([]) diff --git a/types/components/hotelPage/sidepeek/restaurantBar.ts b/types/components/hotelPage/sidepeek/restaurantBar.ts new file mode 100644 index 000000000..fe0f3d0bd --- /dev/null +++ b/types/components/hotelPage/sidepeek/restaurantBar.ts @@ -0,0 +1,5 @@ +import type { RestaurantData } from "@/types/hotel" + +export interface RestaurantBarSidePeekProps { + restaurants: RestaurantData[] +} diff --git a/types/hotel.ts b/types/hotel.ts index def1e2ebe..c42f15423 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -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 @@ -23,6 +24,7 @@ export type HotelTripAdvisor = | undefined export type RoomData = z.infer +export type RestaurantData = z.infer export type GalleryImage = z.infer export type CheckInData = z.infer