diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css index 9877bc4cf..3a8bcfe07 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css +++ b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.module.css @@ -6,10 +6,9 @@ min-height: 100dvh; } -.hotelCards { +.section { display: flex; flex-direction: column; - gap: var(--Spacing-x4); } .link { diff --git a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx index 6d350b61c..63facf6d8 100644 --- a/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx +++ b/app/[lang]/(live)/(public)/hotelreservation/select-hotel/page.tsx @@ -1,41 +1,92 @@ import { serverClient } from "@/lib/trpc/server" -import HotelCard from "@/components/HotelReservation/HotelCard" +import HotelCardListing from "@/components/HotelReservation/HotelCardListing" import HotelFilter from "@/components/HotelReservation/SelectHotel/HotelFilter" import { ChevronRightIcon } from "@/components/Icons" import StaticMap from "@/components/Maps/StaticMap" import Link from "@/components/TempDesignSystem/Link" -import Title from "@/components/TempDesignSystem/Text/Title" import { getIntl } from "@/i18n" -import { setLang } from "@/i18n/serverContext" +import { getLang, setLang } from "@/i18n/serverContext" import styles from "./page.module.css" +import { AvailabilityInput } from "@/types/components/hotelReservation/selectHotel/availabilityInput" +import { HotelData } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" import { LangParams, PageArgs } from "@/types/params" +async function getAvailableHotels( + input: AvailabilityInput +): Promise { + const getAvailableHotels = await serverClient().hotel.availability.get(input) + + if (!getAvailableHotels) throw new Error() + + const { availability } = getAvailableHotels + + const hotels = availability.map(async (hotel) => { + const hotelData = await serverClient().hotel.hotelData.get({ + hotelId: hotel.hotelId.toString(), + language: getLang(), + }) + + if (!hotelData) throw new Error() + + return { + hotelData: hotelData.data.attributes, + price: hotel.bestPricePerNight, + } + }) + + return await Promise.all(hotels) +} + export default async function SelectHotelPage({ params, }: PageArgs) { - const intl = await getIntl() setLang(params.lang) const tempSearchTerm = "Stockholm" - const hotelFilters = await serverClient().hotel.filters.get({ - hotelId: "879", - }) + const intl = await getIntl() - const availableHotels = await serverClient().hotel.availability.get({ + const hotels = await getAvailableHotels({ cityId: "8ec4bba3-1c38-4606-82d1-bbe3f6738e54", roomStayStartDate: "2024-11-02", roomStayEndDate: "2024-11-03", adults: 1, }) - if (!availableHotels) return null + const filters = hotels.flatMap((data) => data.hotelData.detailedFacilities) + + const filterIds = [...new Set(filters.map((data) => data.id))] + const filterList: { + name: string + id: number + applyToAllHotels: boolean + public: boolean + icon: string + sortOrder: number + code?: string + iconName?: string + }[] = filterIds + .map((id) => filters.find((find) => find.id === id)) + .filter( + ( + filter + ): filter is { + name: string + id: number + applyToAllHotels: boolean + public: boolean + icon: string + sortOrder: number + code?: string + iconName?: string + } => filter !== undefined + ) return (
-
+
- -
-
- {availableHotels.availability.length ? ( - availableHotels.availability.map((hotel) => ( - - )) - ) : ( - // TODO: handle no hotels found - No hotels found - )} +
+
) } diff --git a/components/HotelReservation/HotelCard/index.tsx b/components/HotelReservation/HotelCard/index.tsx index 43cf56a03..76d702b98 100644 --- a/components/HotelReservation/HotelCard/index.tsx +++ b/components/HotelReservation/HotelCard/index.tsx @@ -1,4 +1,4 @@ -import tempHotelData from "@/server/routers/hotels/tempHotelData.json" +import { useIntl } from "react-intl" import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data" import { @@ -14,24 +14,18 @@ import Link from "@/components/TempDesignSystem/Link" import Caption from "@/components/TempDesignSystem/Text/Caption" import Footnote from "@/components/TempDesignSystem/Text/Footnote" import Title from "@/components/TempDesignSystem/Text/Title" -import { getIntl } from "@/i18n" import styles from "./hotelCard.module.css" import { HotelCardProps } from "@/types/components/hotelReservation/selectHotel/hotelCardProps" -export default async function HotelCard({ - checkInDate, - checkOutDate, - hotelId, - price, -}: HotelCardProps) { - const intl = await getIntl() +export default function HotelCard({ hotel }: HotelCardProps) { + const intl = useIntl() - // TODO: Use real endpoint. - const hotel = tempHotelData.data.attributes + const { hotelData } = hotel + const { price } = hotel - const sortedAmenities = hotel.detailedFacilities + const sortedAmenities = hotelData.detailedFacilities .sort((a, b) => b.sortOrder - a.sortOrder) .slice(0, 5) @@ -39,8 +33,8 @@ export default async function HotelCard({
{hotel.hotelContent.images.metaData.altText} - {hotel.ratings?.tripAdvisor.rating} + {hotelData.ratings?.tripAdvisor.rating}
- {hotel.name} + {hotelData.name} - {`${hotel.address.streetAddress}, ${hotel.address.city}`} + {`${hotelData.address.streetAddress}, ${hotelData.address.city}`} - {`${hotel.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`} + {`${hotelData.location.distanceToCentre} ${intl.formatMessage({ id: "km to city center" })}`}
diff --git a/components/HotelReservation/HotelCardListing/hotelCardListing.module.css b/components/HotelReservation/HotelCardListing/hotelCardListing.module.css new file mode 100644 index 000000000..be62321df --- /dev/null +++ b/components/HotelReservation/HotelCardListing/hotelCardListing.module.css @@ -0,0 +1,5 @@ +.hotelCards { + display: flex; + flex-direction: column; + gap: var(--Spacing-x4); +} diff --git a/components/HotelReservation/HotelCardListing/index.tsx b/components/HotelReservation/HotelCardListing/index.tsx new file mode 100644 index 000000000..bba0ce99d --- /dev/null +++ b/components/HotelReservation/HotelCardListing/index.tsx @@ -0,0 +1,25 @@ +"use client" + +import Title from "@/components/TempDesignSystem/Text/Title" + +import HotelCard from "../HotelCard" + +import styles from "./hotelCardListing.module.css" + +import { HotelCardListingProps } from "@/types/components/hotelReservation/selectHotel/hotelCardListingProps" + +export default function HotelCardListing({ hotelData }: HotelCardListingProps) { + // TODO: filter with url params + + return ( +
+ {hotelData && hotelData.length ? ( + hotelData.map((hotel) => ( + + )) + ) : ( + No hotels found + )} +
+ ) +} diff --git a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx index 1c30bf264..f10191cec 100644 --- a/components/HotelReservation/SelectHotel/HotelFilter/index.tsx +++ b/components/HotelReservation/SelectHotel/HotelFilter/index.tsx @@ -1,40 +1,37 @@ -import { getIntl } from "@/i18n" +"use client" + +import { useIntl } from "react-intl" import styles from "./hotelFilter.module.css" -import { HotelFilterProps } from "@/types/components/hotelReservation/selectHotel/hotelFilterProps" +import { HotelFiltersProps } from "@/types/components/hotelReservation/selectHotel/hotelFiltersProps" -export default async function HotelFilter({ filters }: HotelFilterProps) { - const { formatMessage } = await getIntl() +export default function HotelFilter({ filters }: HotelFiltersProps) { + const intl = useIntl() + + function handleOnChange() { + // TODO: Update URL with selected values + } return ( ) diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index 562a0e608..416d41eee 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -21,6 +21,10 @@ export const getRatesInputSchema = z.object({ hotelId: z.string(), }) -export const getFiltersInputSchema = z.object({ +export const getlHotelDataInputSchema = z.object({ hotelId: z.string(), + language: z.string(), + include: z + .array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"])) + .optional(), }) diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index 192bab55c..1eba75ea0 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -551,14 +551,4 @@ const rate = z.object({ }) export const getRatesSchema = z.array(rate) - export type Rate = z.infer - -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 diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index b35ed7cd2..fe92fa109 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -27,18 +27,16 @@ import { } from "../contentstack/hotelPage/output" import { getAvailabilityInputSchema, - getFiltersInputSchema, getHotelInputSchema, + getlHotelDataInputSchema, getRatesInputSchema, } from "./input" import { getAvailabilitySchema, - getFiltersSchema, getHotelDataSchema, getRatesSchema, roomSchema, } from "./output" -import tempFilterData from "./tempFilterData.json" import tempRatesData from "./tempRatesData.json" import { HotelBlocksTypenameEnum } from "@/types/components/hotelPage/enums" @@ -419,33 +417,102 @@ export const hotelQueryRouter = router({ return validatedHotelData.data }), }), - filters: router({ - get: publicProcedure - .input(getFiltersInputSchema) - .query(async ({ input, ctx }) => { - console.info("api.hotels.filters start", JSON.stringify({})) + hotelData: router({ + get: serviceProcedure + .input(getlHotelDataInputSchema) + .query(async ({ ctx, input }) => { + const { hotelId, language, include } = input - if (!tempFilterData) { - console.error( - "api.hotels.filters error", - JSON.stringify({ error: null }) - ) - //Can't return null here since consuming component does not handle null yet - // return null + const params: Record = { + hotelId, + language, } - const validateFilterData = getFiltersSchema.safeParse(tempFilterData) - if (!validateFilterData.success) { + if (include) { + params.include = include.join(",") + } + + getHotelCounter.add(1, { + hotelId, + language, + include, + }) + console.info( + "api.hotels.hotelData start", + JSON.stringify({ query: { hotelId, params } }) + ) + + const apiResponse = await api.get( + `${api.endpoints.v1.hotels}/${hotelId}`, + { + headers: { + Authorization: `Bearer ${ctx.serviceToken}`, + }, + }, + params + ) + + if (!apiResponse.ok) { + const text = await apiResponse.text() + getHotelFailCounter.add(1, { + hotelId, + language, + include, + error_type: "http_error", + error: JSON.stringify({ + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }), + }) console.error( - "api.hotels.filters validation error", + "api.hotels.hotelData error", JSON.stringify({ - error: validateFilterData.error, + query: { hotelId, params }, + error: { + status: apiResponse.status, + statusText: apiResponse.statusText, + text, + }, + }) + ) + return null + } + + const apiJson = await apiResponse.json() + const validateHotelData = getHotelDataSchema.safeParse(apiJson) + + if (!validateHotelData.success) { + getHotelFailCounter.add(1, { + hotelId, + language, + include, + error_type: "validation_error", + error: JSON.stringify(validateHotelData.error), + }) + + console.error( + "api.hotels.hotelData validation error", + JSON.stringify({ + query: { hotelId, params }, + error: validateHotelData.error, }) ) throw badRequestError() } - console.info("api.hotels.rates success", JSON.stringify({})) - return validateFilterData.data + + getHotelSuccessCounter.add(1, { + hotelId, + language, + include, + }) + console.info( + "api.hotels.hotelData success", + JSON.stringify({ + query: { hotelId, params: params }, + }) + ) + return validateHotelData.data }), }), }) diff --git a/server/routers/hotels/tempFilterData.json b/server/routers/hotels/tempFilterData.json deleted file mode 100644 index e58bdf50b..000000000 --- a/server/routers/hotels/tempFilterData.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "roomFacilities": ["Balcony", "Bathub", "View", "Conntecting doors"], - "hotelFacilities": [ - "Parking inside", - "Parking outside", - "Parking electric", - "Sauna", - "Pool", - "Restaurant", - "Bar", - "Sky/rooftop bar", - "Gym", - "Coworking" - ], - "hotelSurroundings": [ - "Beach", - "Lake or sea", - "Hiking", - "Mountains", - "Golf course" - ] -} diff --git a/types/components/hotelReservation/selectHotel/availabilityInput.ts b/types/components/hotelReservation/selectHotel/availabilityInput.ts new file mode 100644 index 000000000..ff25984b9 --- /dev/null +++ b/types/components/hotelReservation/selectHotel/availabilityInput.ts @@ -0,0 +1,10 @@ +export type AvailabilityInput = { + cityId: string + roomStayStartDate: string + roomStayEndDate: string + adults: number + children?: number + promotionCode?: string + reservationProfileType?: string + attachedProfileId?: string +} diff --git a/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts b/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts new file mode 100644 index 000000000..0ab3df1ad --- /dev/null +++ b/types/components/hotelReservation/selectHotel/hotelCardListingProps.ts @@ -0,0 +1,12 @@ +import { AvailabilityPrices } from "@/server/routers/hotels/output" + +import { Hotel } from "@/types/hotel" + +export type HotelCardListingProps = { + hotelData: HotelData[] +} + +export type HotelData = { + hotelData: Hotel + price: AvailabilityPrices +} diff --git a/types/components/hotelReservation/selectHotel/hotelCardProps.ts b/types/components/hotelReservation/selectHotel/hotelCardProps.ts index feafa6228..dbfdd797c 100644 --- a/types/components/hotelReservation/selectHotel/hotelCardProps.ts +++ b/types/components/hotelReservation/selectHotel/hotelCardProps.ts @@ -1,11 +1,5 @@ -import { - Availability, - AvailabilityPrices, -} from "@/server/routers/hotels/output" +import { HotelData } from "./hotelCardListingProps" export type HotelCardProps = { - checkInDate: Availability["data"][number]["attributes"]["checkInDate"] - checkOutDate: Availability["data"][number]["attributes"]["checkOutDate"] - hotelId: Availability["data"][number]["attributes"]["hotelId"] - price: AvailabilityPrices + hotel: HotelData } diff --git a/types/components/hotelReservation/selectHotel/hotelFilterProps.ts b/types/components/hotelReservation/selectHotel/hotelFilterProps.ts deleted file mode 100644 index e100131ba..000000000 --- a/types/components/hotelReservation/selectHotel/hotelFilterProps.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { HotelFilter } from "@/server/routers/hotels/output" - -export type HotelFilterProps = { filters: HotelFilter } diff --git a/types/components/hotelReservation/selectHotel/hotelFiltersProps.ts b/types/components/hotelReservation/selectHotel/hotelFiltersProps.ts new file mode 100644 index 000000000..1be84f63d --- /dev/null +++ b/types/components/hotelReservation/selectHotel/hotelFiltersProps.ts @@ -0,0 +1,5 @@ +import { Hotel } from "@/types/hotel" + +export type HotelFiltersProps = { + filters: Hotel["detailedFacilities"] +}