feat(SW-201): Refactoring how we fetch hotel page data

This commit is contained in:
Erik Tiekstra
2024-11-18 14:55:24 +01:00
parent 75c811eb32
commit ca2f60253f
7 changed files with 99 additions and 200 deletions

View File

@@ -3,6 +3,7 @@ import { notFound } from "next/navigation"
import { isSignupPage } from "@/constants/routes/signup" import { isSignupPage } from "@/constants/routes/signup"
import { env } from "@/env/server" import { env } from "@/env/server"
import { getHotelPage } from "@/lib/trpc/memoizedRequests"
import HotelPage from "@/components/ContentType/HotelPage" import HotelPage from "@/components/ContentType/HotelPage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage" import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
@@ -19,7 +20,7 @@ import {
export { generateMetadata } from "@/utils/generateMetadata" export { generateMetadata } from "@/utils/generateMetadata"
export default function ContentTypePage({ export default async function ContentTypePage({
params, params,
}: PageArgs<LangParams & ContentTypeParams & UIDParams, {}>) { }: PageArgs<LangParams & ContentTypeParams & UIDParams, {}>) {
setLang(params.lang) setLang(params.lang)
@@ -57,7 +58,12 @@ export default function ContentTypePage({
if (env.HIDE_FOR_NEXT_RELEASE) { if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound() return notFound()
} }
return <HotelPage /> const hotelPageData = await getHotelPage()
return hotelPageData ? (
<HotelPage hotelId={hotelPageData.hotel_page_id} />
) : (
notFound()
)
default: default:
const type: never = params.contentType const type: never = params.contentType
console.error(`Unsupported content type given: ${type}`) console.error(`Unsupported content type given: ${type}`)

View File

@@ -1,6 +1,8 @@
import { notFound } from "next/navigation"
import hotelPageParams from "@/constants/routes/hotelPageParams" import hotelPageParams from "@/constants/routes/hotelPageParams"
import { env } from "@/env/server" import { env } from "@/env/server"
import { serverClient } from "@/lib/trpc/server" import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests"
import AccordionSection from "@/components/Blocks/Accordion" import AccordionSection from "@/components/Blocks/Accordion"
import HotelReservationSidePeek from "@/components/HotelReservation/SidePeek" import HotelReservationSidePeek from "@/components/HotelReservation/SidePeek"
@@ -16,65 +18,96 @@ import MapCard from "./Map/MapCard"
import MapWithCardWrapper from "./Map/MapWithCard" import MapWithCardWrapper from "./Map/MapWithCard"
import MobileMapToggle from "./Map/MobileMapToggle" import MobileMapToggle from "./Map/MobileMapToggle"
import StaticMap from "./Map/StaticMap" import StaticMap from "./Map/StaticMap"
import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise"
import AmenitiesList from "./AmenitiesList" import AmenitiesList from "./AmenitiesList"
import Facilities from "./Facilities" import Facilities from "./Facilities"
import IntroSection from "./IntroSection" import IntroSection from "./IntroSection"
import PreviewImages from "./PreviewImages" import PreviewImages from "./PreviewImages"
import { Rooms } from "./Rooms" import { Rooms } from "./Rooms"
import { AboutTheHotelSidePeek, WellnessAndExerciseSidePeek } from "./SidePeeks" import { AboutTheHotelSidePeek } from "./SidePeeks"
import TabNavigation from "./TabNavigation" import TabNavigation from "./TabNavigation"
import styles from "./hotelPage.module.css" import styles from "./hotelPage.module.css"
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
import { HotelPageProps } from "@/types/components/hotelPage/hotelPage"
import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation" import { HotelHashValues } from "@/types/components/hotelPage/tabNavigation"
import { Facility } from "@/types/hotel"
export default async function HotelPage() { export default async function HotelPage({ hotelId }: HotelPageProps) {
const intl = await getIntl()
const lang = getLang()
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
const hotelData = await serverClient().hotel.get() const lang = getLang()
if (!hotelData) { const [intl, hotelPageData, hotelData] = await Promise.all([
return null getIntl(),
getHotelPage(),
getHotelData({ hotelId, language: lang }),
])
if (!hotelData?.data || !hotelPageData) {
return notFound()
} }
const { faq, content } = hotelPageData
const { const {
hotelId, name,
hotelName, address,
hotelDescriptions,
hotelLocation,
hotelAddress,
hotelRatings,
hotelDetailedFacilities,
hotelImages,
roomCategories,
activitiesCard,
pointsOfInterest, pointsOfInterest,
facilities, gallery,
faq, specialAlerts,
alerts, healthAndWellness,
restaurantImages,
conferencesAndMeetings,
hotelContent,
detailedFacilities,
healthFacilities, healthFacilities,
contact, contactInformation,
socials, socialMedia,
ecoLabels, hotelFacts,
} = hotelData location,
ratings,
} = hotelData.data.attributes
const roomCategories =
hotelData.included?.filter((item) => item.type === "roomcategories") || []
const images = gallery?.smallerImages
const description = hotelContent.texts.descriptions.short
const activitiesCard = content?.[0]?.upcoming_activities_card || null
const facilities: Facility[] = [
{
...restaurantImages,
id: FacilityCardTypeEnum.restaurant,
headingText: restaurantImages?.headingText ?? "",
heroImages: restaurantImages?.heroImages ?? [],
},
{
...conferencesAndMeetings,
id: FacilityCardTypeEnum.conference,
headingText: conferencesAndMeetings?.headingText ?? "",
heroImages: conferencesAndMeetings?.heroImages ?? [],
},
{
...healthAndWellness,
id: FacilityCardTypeEnum.wellness,
headingText: healthAndWellness?.headingText ?? "",
heroImages: healthAndWellness?.heroImages ?? [],
},
]
const topThreePois = pointsOfInterest.slice(0, 3) const topThreePois = pointsOfInterest.slice(0, 3)
const coordinates = { const coordinates = {
lat: hotelLocation.latitude, lat: location.latitude,
lng: hotelLocation.longitude, lng: location.longitude,
} }
return ( return (
<div className={styles.pageContainer}> <div className={styles.pageContainer}>
<div className={styles.hotelImages}> <div className={styles.hotelImages}>
{hotelImages?.length && ( {images?.length && <PreviewImages images={images} hotelName={name} />}
<PreviewImages images={hotelImages} hotelName={hotelName} />
)}
</div> </div>
<TabNavigation <TabNavigation
restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)} restaurantTitle={getRestaurantHeading(detailedFacilities)}
hasActivities={!!activitiesCard} hasActivities={!!activitiesCard}
hasFAQ={!!faq.accordions.length} hasFAQ={!!faq.accordions.length}
/> />
@@ -82,18 +115,18 @@ export default async function HotelPage() {
<div id={HotelHashValues.overview} className={styles.overview}> <div id={HotelHashValues.overview} className={styles.overview}>
<div className={styles.introContainer}> <div className={styles.introContainer}>
<IntroSection <IntroSection
hotelName={hotelName} hotelName={name}
hotelDescription={hotelDescriptions.descriptions.short} hotelDescription={description}
location={hotelLocation} location={location}
address={hotelAddress} address={address}
tripAdvisor={hotelRatings?.tripAdvisor} tripAdvisor={ratings?.tripAdvisor}
/> />
<AmenitiesList detailedFacilities={hotelDetailedFacilities} /> <AmenitiesList detailedFacilities={detailedFacilities} />
</div> </div>
{alerts.length ? ( {specialAlerts.length ? (
<div className={styles.alertsContainer}> <div className={styles.alertsContainer}>
{alerts.map((alert) => ( {specialAlerts.map((alert) => (
<Alert <Alert
key={alert.id} key={alert.id}
type={alert.type} type={alert.type}
@@ -114,14 +147,14 @@ export default async function HotelPage() {
<> <>
<aside className={styles.mapContainer}> <aside className={styles.mapContainer}>
<MapWithCardWrapper> <MapWithCardWrapper>
<StaticMap coordinates={coordinates} hotelName={hotelName} /> <StaticMap coordinates={coordinates} hotelName={name} />
<MapCard hotelName={hotelName} pois={topThreePois} /> <MapCard hotelName={name} pois={topThreePois} />
</MapWithCardWrapper> </MapWithCardWrapper>
</aside> </aside>
<MobileMapToggle /> <MobileMapToggle />
<DynamicMap <DynamicMap
apiKey={googleMapsApiKey} apiKey={googleMapsApiKey}
hotelName={hotelName} hotelName={name}
coordinates={coordinates} coordinates={coordinates}
pointsOfInterest={pointsOfInterest} pointsOfInterest={pointsOfInterest}
mapId={googleMapId} mapId={googleMapId}
@@ -138,12 +171,12 @@ export default async function HotelPage() {
Read more about the amenities here Read more about the amenities here
</SidePeek> </SidePeek>
<AboutTheHotelSidePeek <AboutTheHotelSidePeek
hotelAddress={hotelAddress} hotelAddress={address}
coordinates={hotelLocation} coordinates={location}
contact={contact} contact={contactInformation}
socials={socials} socials={socialMedia}
ecoLabels={ecoLabels} ecoLabels={hotelFacts.ecoLabels}
descriptions={hotelDescriptions} descriptions={hotelContent.texts}
/> />
<SidePeek <SidePeek
contentKey={hotelPageParams.restaurantAndBar[lang]} contentKey={hotelPageParams.restaurantAndBar[lang]}

View File

@@ -66,6 +66,10 @@ export const getHotelData = cache(function getMemoizedHotelData(
return serverClient().hotel.hotelData.get(props) return serverClient().hotel.hotelData.get(props)
}) })
export const getHotelPage = cache(async function getMemoizedHotelPage() {
return serverClient().contentstack.hotelPage.get()
})
export const getRoomsAvailability = cache(function getMemoizedRoomAvailability( export const getRoomsAvailability = cache(function getMemoizedRoomAvailability(
args: GetRoomsAvailabilityInput args: GetRoomsAvailabilityInput
) { ) {

View File

@@ -115,7 +115,7 @@ export async function getDescription(data: RawMetadataSchema) {
return "" return ""
} }
export function getImages(data: RawMetadataSchema) { export function getImage(data: RawMetadataSchema) {
const metadataImage = data.web?.seo_metadata?.seo_image const metadataImage = data.web?.seo_metadata?.seo_image
const heroImage = data.hero_image const heroImage = data.hero_image

View File

@@ -1,13 +1,9 @@
import { metrics } from "@opentelemetry/api" import { metrics } from "@opentelemetry/api"
import { Lang } from "@/constants/languages"
import * as api from "@/lib/api" import * as api from "@/lib/api"
import { dt } from "@/lib/dt" import { dt } from "@/lib/dt"
import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql" import { badRequestError } from "@/server/errors/trpc"
import { request } from "@/lib/graphql/request"
import { badRequestError, notFound } from "@/server/errors/trpc"
import { import {
contentStackUidWithServiceProcedure,
publicProcedure, publicProcedure,
router, router,
safeProtectedServiceProcedure, safeProtectedServiceProcedure,
@@ -17,13 +13,6 @@ import { toApiLang } from "@/server/utils"
import { cache } from "@/utils/cache" import { cache } from "@/utils/cache"
import { hotelPageSchema } from "../contentstack/hotelPage/output"
import {
fetchHotelPageRefs,
generatePageTags,
getHotelPageCounter,
validateHotelPageRefs,
} from "../contentstack/hotelPage/utils"
import { getVerifiedUser, parsedUser } from "../user/query" import { getVerifiedUser, parsedUser } from "../user/query"
import { import {
getBreakfastPackageInputSchema, getBreakfastPackageInputSchema,
@@ -52,13 +41,10 @@ import {
TWENTYFOUR_HOURS, TWENTYFOUR_HOURS,
} from "./utils" } from "./utils"
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType" import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
import { BreakfastPackageEnum } from "@/types/enums/breakfast" import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { HotelTypeEnum } from "@/types/enums/hotelType" import { HotelTypeEnum } from "@/types/enums/hotelType"
import type { RequestOptionsWithOutBody } from "@/types/fetch" import type { RequestOptionsWithOutBody } from "@/types/fetch"
import type { Facility } from "@/types/hotel"
import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage"
const meter = metrics.getMeter("trpc.hotels") const meter = metrics.getMeter("trpc.hotels")
const getHotelCounter = meter.createCounter("trpc.hotel.get") const getHotelCounter = meter.createCounter("trpc.hotel.get")
@@ -111,55 +97,6 @@ const breakfastPackagesFailCounter = meter.createCounter(
"trpc.package.breakfast-fail" "trpc.package.breakfast-fail"
) )
async function getContentstackData(lang: Lang, uid?: string | null) {
if (!uid) {
return null
}
const contentPageRefsData = await fetchHotelPageRefs(lang, uid)
const contentPageRefs = validateHotelPageRefs(contentPageRefsData, lang, uid)
if (!contentPageRefs) {
return null
}
const tags = generatePageTags(contentPageRefs, lang)
getHotelPageCounter.add(1, { lang, uid })
console.info(
"contentstack.hotelPage start",
JSON.stringify({
query: { lang, uid },
})
)
const response = await request<GetHotelPageData>(
GetHotelPage,
{
locale: lang,
uid,
},
{
cache: "force-cache",
next: {
tags,
},
}
)
if (!response.data) {
throw notFound(response)
}
const hotelPageData = hotelPageSchema.safeParse(response.data)
if (!hotelPageData.success) {
console.error(
`Failed to validate Hotel Page - (uid: ${uid}, lang: ${lang})`
)
console.error(hotelPageData.error)
return null
}
return hotelPageData.data.hotel_page
}
export const getHotelData = cache( export const getHotelData = cache(
async (input: HotelDataInput, serviceToken: string) => { async (input: HotelDataInput, serviceToken: string) => {
const { hotelId, language, isCardOnlyPayment } = input const { hotelId, language, isCardOnlyPayment } = input
@@ -273,90 +210,6 @@ export const getHotelData = cache(
) )
export const hotelQueryRouter = router({ export const hotelQueryRouter = router({
get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => {
const { lang, uid } = ctx
const contentstackData = await getContentstackData(lang, uid)
const hotelId = contentstackData?.hotel_page_id
if (!hotelId) {
throw notFound(`Hotel not found for uid: ${uid}`)
}
const hotelData = await getHotelData(
{
hotelId,
language: ctx.lang,
},
ctx.serviceToken
)
if (!hotelData) {
throw notFound()
}
const included = hotelData.included || []
const hotelAttributes = hotelData.data.attributes
const images = hotelAttributes.gallery?.smallerImages
const hotelAlerts = hotelAttributes.specialAlerts
const roomCategories = included
? included.filter((item) => item.type === "roomcategories")
: []
const activities = contentstackData?.content
? contentstackData?.content[0]
: null
const facilities: Facility[] = [
{
...hotelData.data.attributes.restaurantImages,
id: FacilityCardTypeEnum.restaurant,
headingText:
hotelData?.data.attributes.restaurantImages?.headingText ?? "",
heroImages:
hotelData?.data.attributes.restaurantImages?.heroImages ?? [],
},
{
...hotelData.data.attributes.conferencesAndMeetings,
id: FacilityCardTypeEnum.conference,
headingText:
hotelData?.data.attributes.conferencesAndMeetings?.headingText ?? "",
heroImages:
hotelData?.data.attributes.conferencesAndMeetings?.heroImages ?? [],
},
{
...hotelData.data.attributes.healthAndWellness,
id: FacilityCardTypeEnum.wellness,
headingText:
hotelData?.data.attributes.healthAndWellness?.headingText ?? "",
heroImages:
hotelData?.data.attributes.healthAndWellness?.heroImages ?? [],
},
]
return {
hotelId,
hotelName: hotelAttributes.name,
hotelDescriptions: hotelAttributes.hotelContent.texts,
hotelLocation: hotelAttributes.location,
hotelAddress: hotelAttributes.address,
hotelRatings: hotelAttributes.ratings,
hotelDetailedFacilities: hotelAttributes.detailedFacilities,
hotelImages: images,
pointsOfInterest: hotelAttributes.pointsOfInterest,
roomCategories,
activitiesCard: activities?.upcoming_activities_card,
facilities,
alerts: hotelAlerts,
faq: contentstackData?.faq,
healthFacilities: hotelAttributes.healthFacilities,
contact: hotelAttributes.contactInformation,
socials: hotelAttributes.socialMedia,
ecoLabels: hotelAttributes.hotelFacts.ecoLabels,
}
}),
availability: router({ availability: router({
hotels: serviceProcedure hotels: serviceProcedure
.input(getHotelsAvailabilityInputSchema) .input(getHotelsAvailabilityInputSchema)

View File

@@ -4,7 +4,7 @@ import type { CardProps } from "@/components/TempDesignSystem/Card/card"
export type FacilitiesProps = { export type FacilitiesProps = {
facilities: Facility[] facilities: Facility[]
activitiesCard?: ActivityCard activitiesCard: ActivityCard | null
} }
export type FacilityImage = { export type FacilityImage = {

View File

@@ -0,0 +1,3 @@
export interface HotelPageProps {
hotelId: string
}