From 0697c8d9efc8d0b0907bf2bafae835c547b58a38 Mon Sep 17 00:00:00 2001 From: Chuma McPhoy Date: Wed, 10 Jul 2024 13:41:39 +0200 Subject: [PATCH] feat: add included param to hotel call, fetch room data, setup schema, use in hotelpage --- .../ContentType/HotelPage/HotelPage.tsx | 7 +- .../HotelPage/Rooms/RoomCard/index.tsx | 22 +++-- .../Rooms/RoomCard/roomCard.module.css | 19 ---- .../ContentType/HotelPage/Rooms/index.tsx | 67 ++++++++++----- .../ContentType/HotelPage/Rooms/types.ts | 5 ++ .../HotelPage/hotelPage.module.css | 1 - .../HotelPage/tempHotelPageData.ts | 82 ------------------ i18n/dictionaries/en.json | 4 + i18n/dictionaries/sv.json | 4 + server/routers/hotels/input.ts | 3 + server/routers/hotels/output.ts | 86 +++++++++++++++++-- server/routers/hotels/query.ts | 33 +++++-- types/components/hotelPage/roomCard.ts | 4 +- types/components/hotelPage/rooms.ts | 12 --- types/hotel.ts | 4 +- 15 files changed, 191 insertions(+), 162 deletions(-) create mode 100644 components/ContentType/HotelPage/Rooms/types.ts delete mode 100644 components/ContentType/HotelPage/tempHotelPageData.ts delete mode 100644 types/components/hotelPage/rooms.ts diff --git a/components/ContentType/HotelPage/HotelPage.tsx b/components/ContentType/HotelPage/HotelPage.tsx index 6268318a6..5a69bf9cf 100644 --- a/components/ContentType/HotelPage/HotelPage.tsx +++ b/components/ContentType/HotelPage/HotelPage.tsx @@ -4,7 +4,6 @@ import AmenitiesList from "./AmenitiesList" import IntroSection from "./IntroSection" import { Rooms } from "./Rooms" -import { MOCK_ROOMS } from "./tempHotelPageData" import styles from "./hotelPage.module.css" @@ -18,11 +17,11 @@ export default async function HotelPage({ lang }: LangParams) { return null } - const attributes = await serverClient().hotel.getHotel({ + const { attributes, roomCategories } = await serverClient().hotel.getHotel({ hotelId: hotelPageIdentifierData.hotel_page_id, language: lang, + include: ["RoomCategories"], }) - const rooms = MOCK_ROOMS return (
@@ -36,7 +35,7 @@ export default async function HotelPage({ lang }: LangParams) { /> - +
) } diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx index 257d73cbf..a42124498 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx +++ b/components/ContentType/HotelPage/Rooms/RoomCard/index.tsx @@ -4,6 +4,7 @@ import { useIntl } from "react-intl" import { ImageIcon } from "@/components/Icons" import Image from "@/components/Image" +import Link from "@/components/TempDesignSystem/Link" import Body from "@/components/TempDesignSystem/Text/Body" import Title from "@/components/TempDesignSystem/Text/Title" @@ -43,12 +44,14 @@ export function RoomCard({ {images.length} + {/*NOTE: images from the test API are hosted on test3.scandichotels.com, + which can't be accessed unless on Scandic's Wifi or using Citrix. */} {mainImage.alt}
@@ -58,9 +61,14 @@ export function RoomCard({ {subtitle}
- + + {formatMessage({ id: "hotelPages.rooms.roomCard.seeRoomDetails" })} + ) diff --git a/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css b/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css index 3678cdff2..88446a44a 100644 --- a/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css +++ b/components/ContentType/HotelPage/Rooms/RoomCard/roomCard.module.css @@ -65,22 +65,3 @@ .subtitle { color: var(--UI-Text-Placeholder); } - -.cta { - background-color: transparent; - border-width: 0; - cursor: pointer; - margin: 0; - padding: 0; - display: flex; - align-items: center; - gap: var(--Spacing-x-half); - color: var(--Base-Text-Medium-contrast); - font-family: var(--typography-Body-Bold-fontFamily); - font-size: var(--typography-Body-Bold-fontSize); - font-weight: 600; - text-decoration: underline; -} -.cta:hover { - color: var(--Base-Text-High-contrast); -} diff --git a/components/ContentType/HotelPage/Rooms/index.tsx b/components/ContentType/HotelPage/Rooms/index.tsx index 7dd0f2c88..f7cdde172 100644 --- a/components/ContentType/HotelPage/Rooms/index.tsx +++ b/components/ContentType/HotelPage/Rooms/index.tsx @@ -1,31 +1,54 @@ +import SectionContainer from "@/components/Section/Container" +import SectionHeader from "@/components/Section/Header" +import { getIntl } from "@/i18n" + import { RoomCard } from "./RoomCard" +import { RoomsProps } from "./types" import styles from "./rooms.module.css" -import { RoomsProps } from "@/types/components/hotelPage/rooms" +export async function Rooms({ rooms }: RoomsProps) { + const { formatMessage } = await getIntl() + const mappedRooms = rooms + .map((room) => { + const size = `${room.attributes.roomSize.min} - ${room.attributes.roomSize.max} m²` + const personLabel = + room.attributes.occupancy.total === 1 + ? formatMessage({ id: "hotelPages.rooms.roomCard.person" }) + : formatMessage({ id: "hotelPages.rooms.roomCard.persons" }) -export function Rooms({ rooms }: RoomsProps) { - // TODO: Typings should be adjusted to match the actual data structure - const mappedRooms = rooms.map((room) => ({ - id: room.id, - images: room.images, - title: room.title, - subtitle: room.subtitle, - popularChoice: room.popularChoice, - })) + const subtitle = `${size} (${room.attributes.occupancy.total} ${personLabel})` + return { + id: room.id, + images: room.attributes.content.images, + title: room.attributes.name, + subtitle: subtitle, + sortOrder: room.attributes.sortOrder, + popularChoice: null, + } + }) + .sort((a, b) => a.sortOrder - b.sortOrder) + .slice(0, 3) //TODO: Remove this and render all rooms once we've implemented "show more" logic in SW-203. return ( -
- {mappedRooms.map(({ id, images, title, subtitle, popularChoice }) => ( - - ))} -
+ + +
+ {mappedRooms.map(({ id, images, title, subtitle, popularChoice }) => ( + + ))} +
+
) } diff --git a/components/ContentType/HotelPage/Rooms/types.ts b/components/ContentType/HotelPage/Rooms/types.ts new file mode 100644 index 000000000..b4c320af2 --- /dev/null +++ b/components/ContentType/HotelPage/Rooms/types.ts @@ -0,0 +1,5 @@ +import { RoomData } from "@/types/hotel" + +export type RoomsProps = { + rooms: RoomData[] +} diff --git a/components/ContentType/HotelPage/hotelPage.module.css b/components/ContentType/HotelPage/hotelPage.module.css index 666cd92db..d168ea390 100644 --- a/components/ContentType/HotelPage/hotelPage.module.css +++ b/components/ContentType/HotelPage/hotelPage.module.css @@ -12,7 +12,6 @@ @media screen and (min-width: 1367px) { .pageContainer { - gap: var(--Spacing-x3); padding: var(--Spacing-x9) var(--Spacing-x5); } .introContainer { diff --git a/components/ContentType/HotelPage/tempHotelPageData.ts b/components/ContentType/HotelPage/tempHotelPageData.ts deleted file mode 100644 index 30b4921b6..000000000 --- a/components/ContentType/HotelPage/tempHotelPageData.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { RoomsProps } from "../../../types/components/hotelPage/rooms" - -export const MOCK_ROOMS: RoomsProps["rooms"] = [ - { - id: "1", - title: "Cabin", - subtitle: "15 - 20 m² (2 personer)", - images: [ - { - src: "https://placehold.co/300x200", - alt: "Placeholder image", - width: 300, - height: 200, - }, - { - src: "https://placehold.co/300x200", - alt: "Placeholder image", - width: 300, - height: 200, - }, - ], - popularChoice: false, - }, - { - id: "2", - title: "Standard", - subtitle: "15 - 20 m² (2 personer)", - images: [ - { - src: "https://placehold.co/300x200", - alt: "Placeholder image", - width: 300, - height: 200, - }, - { - src: "https://placehold.co/300x200", - alt: "Placeholder image", - width: 300, - height: 200, - }, - { - src: "https://placehold.co/300x200", - alt: "Placeholder image", - width: 300, - height: 200, - }, - ], - popularChoice: true, - }, - { - id: "3", - title: "Superior", - subtitle: "15 m² (2 personer)", - images: [ - { - src: "https://placehold.co/300x200", - alt: "Placeholder image", - width: 300, - height: 200, - }, - { - src: "https://placehold.co/300x200", - alt: "Placeholder image", - width: 300, - height: 200, - }, - { - src: "https://placehold.co/300x200", - alt: "Placeholder image", - width: 300, - height: 200, - }, - { - src: "https://placehold.co/300x200", - alt: "Placeholder image", - width: 300, - height: 200, - }, - ], - popularChoice: false, - }, -] diff --git a/i18n/dictionaries/en.json b/i18n/dictionaries/en.json index b59ff7859..b6608a825 100644 --- a/i18n/dictionaries/en.json +++ b/i18n/dictionaries/en.json @@ -41,6 +41,10 @@ "From": "From", "Get inspired": "Get inspired", "Go back to overview": "Go back to overview", + "hotelPages.rooms.title": "Rooms", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "persons", + "hotelPages.rooms.roomCard.seeRoomDetails": "See room details", "How it works": "How it works", "Join Scandic Friends": "Join Scandic Friends", "Language": "Language", diff --git a/i18n/dictionaries/sv.json b/i18n/dictionaries/sv.json index 6f0e99cde..0a8cabaa0 100644 --- a/i18n/dictionaries/sv.json +++ b/i18n/dictionaries/sv.json @@ -41,6 +41,10 @@ "Get inspired": "Bli inspirerad", "Go back to overview": "Gå tillbaka till översikten", "How it works": "Hur det fungerar", + "hotelPages.rooms.title": "Rum", + "hotelPages.rooms.roomCard.person": "person", + "hotelPages.rooms.roomCard.persons": "personer", + "hotelPages.rooms.roomCard.seeRoomDetails": "Se rumsdetaljer", "Join Scandic Friends": "Gå med i Scandic Friends", "Language": "Språk", "Level": "Nivå", diff --git a/server/routers/hotels/input.ts b/server/routers/hotels/input.ts index bb8a1ebdd..03ea9e11d 100644 --- a/server/routers/hotels/input.ts +++ b/server/routers/hotels/input.ts @@ -5,6 +5,9 @@ import { Lang } from "@/constants/languages" export const getHotelInputSchema = z.object({ hotelId: z.string(), language: z.nativeEnum(Lang), + include: z + .array(z.enum(["RoomCategories", "NearbyHotels", "Restaurants", "City"])) + .optional(), }) export const getRatesInputSchema = z.object({ diff --git a/server/routers/hotels/output.ts b/server/routers/hotels/output.ts index d62c25f5f..044280d6e 100644 --- a/server/routers/hotels/output.ts +++ b/server/routers/hotels/output.ts @@ -335,6 +335,83 @@ const RelationshipsSchema = z.object({ }), }) +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({ @@ -385,11 +462,10 @@ export const getHotelDataSchema = z.object({ }), relationships: RelationshipsSchema, }), - //TODO: We can pass an "included" param to the hotel API to retrieve additional data for an individual hotel. - // - This is out of scope for current work (and I'm unsure if we need it for hotel pages specifically), - // - but if/when we do we can extend this schema to add necessary requirements. - // - Example "included" data available in our tempHotelData file. - // included: z.any(), + // NOTE: We can pass an "include" param to the hotel API to retrieve + // additional data for an individual hotel. + // Example "included" data can be found in our tempHotelData file. + included: z.array(RoomSchema).optional(), }) const Rate = z.object({ diff --git a/server/routers/hotels/query.ts b/server/routers/hotels/query.ts index fb4ca8da3..edd888519 100644 --- a/server/routers/hotels/query.ts +++ b/server/routers/hotels/query.ts @@ -3,7 +3,7 @@ import { badRequestError } from "@/server/errors/trpc" import { publicProcedure, router } from "@/server/trpc" import { getHotelInputSchema, getRatesInputSchema } from "./input" -import { getHotelDataSchema, getRatesSchema } from "./output" +import { getHotelDataSchema, getRatesSchema, RoomSchema } from "./output" import tempHotelData from "./tempHotelData.json" import tempRatesData from "./tempRatesData.json" import { toApiLang } from "./utils" @@ -12,12 +12,15 @@ export const hotelQueryRouter = router({ getHotel: publicProcedure .input(getHotelInputSchema) .query(async ({ input, ctx }) => { - const { hotelId, language } = input + const { hotelId, language, include } = input const params = new URLSearchParams() const apiLang = toApiLang(language) params.set("hotelId", hotelId.toString()) params.set("language", apiLang) + if (include) { + params.set("include", include.join(",")) + } // TODO: Enable once we have authorized API access. // const apiResponse = await api.get( @@ -33,10 +36,9 @@ export const hotelQueryRouter = router({ // } // const apiJson = await apiResponse.json() - //TODO: We can pass an "included" param to the hotel API to retrieve additional data for an individual hotel. - // - This is out of scope for current work (and I'm unsure if we need it for hotel pages specifically), - // - but if/when we do we can extend the endpoint (and schema) to add necessary requirements. - // - Example "included" data available in our tempHotelData file. + // NOTE: We can pass an "include" param to the hotel API to retrieve + // additional data for an individual hotel. + // Example "included" data can be found in our tempHotelData file. const { included, ...apiJsonWithoutIncluded } = tempHotelData const validatedHotelData = getHotelDataSchema.safeParse( apiJsonWithoutIncluded @@ -48,7 +50,24 @@ export const hotelQueryRouter = router({ throw badRequestError() } - return validatedHotelData.data.data.attributes + const roomCategories = included + ? included + .filter((item) => item.type === "roomcategories") + .map((roomCategory) => { + const validatedRoom = RoomSchema.safeParse(roomCategory) + if (!validatedRoom.success) { + console.info(`Get Room Category Data - Verified Data Error`) + console.error(validatedRoom.error) + throw badRequestError() + } + return validatedRoom.data + }) + : [] + + return { + attributes: validatedHotelData.data.data.attributes, + roomCategories: roomCategories, + } }), getRates: publicProcedure .input(getRatesInputSchema) diff --git a/types/components/hotelPage/roomCard.ts b/types/components/hotelPage/roomCard.ts index bfdec37bb..00238fb9a 100644 --- a/types/components/hotelPage/roomCard.ts +++ b/types/components/hotelPage/roomCard.ts @@ -1,8 +1,8 @@ -import { ImageProps } from "next/image" +import { RoomData } from "@/types/hotel" export interface RoomCardProps { id: string - images: ImageProps[] + images: RoomData["attributes"]["content"]["images"] title: string subtitle: string badgeTextTransKey?: string | null diff --git a/types/components/hotelPage/rooms.ts b/types/components/hotelPage/rooms.ts deleted file mode 100644 index d7ccb5859..000000000 --- a/types/components/hotelPage/rooms.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ImageProps } from "next/image" - -// TODO: Typings should be adjusted to match the actual data structure -export interface RoomsProps { - rooms: { - id: string - title: string - subtitle: string - popularChoice: boolean - images: ImageProps[] - }[] -} diff --git a/types/hotel.ts b/types/hotel.ts index a8a715c3d..9d277d145 100644 --- a/types/hotel.ts +++ b/types/hotel.ts @@ -1,6 +1,6 @@ import { z } from "zod" -import { getHotelDataSchema } from "@/server/routers/hotels/output" +import { getHotelDataSchema,RoomSchema } from "@/server/routers/hotels/output" export type HotelData = z.infer @@ -9,3 +9,5 @@ export type HotelAddress = HotelData["data"]["attributes"]["address"] export type HotelLocation = HotelData["data"]["attributes"]["location"] export type HotelTripAdvisor = HotelData["data"]["attributes"]["ratings"]["tripAdvisor"] + +export type RoomData = z.infer