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. */}
@@ -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