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 { env } from "@/env/server"
import { getHotelPage } from "@/lib/trpc/memoizedRequests"
import HotelPage from "@/components/ContentType/HotelPage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
@@ -19,7 +20,7 @@ import {
export { generateMetadata } from "@/utils/generateMetadata"
export default function ContentTypePage({
export default async function ContentTypePage({
params,
}: PageArgs<LangParams & ContentTypeParams & UIDParams, {}>) {
setLang(params.lang)
@@ -57,7 +58,12 @@ export default function ContentTypePage({
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <HotelPage />
const hotelPageData = await getHotelPage()
return hotelPageData ? (
<HotelPage hotelId={hotelPageData.hotel_page_id} />
) : (
notFound()
)
default:
const type: never = params.contentType
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 { env } from "@/env/server"
import { serverClient } from "@/lib/trpc/server"
import { getHotelData, getHotelPage } from "@/lib/trpc/memoizedRequests"
import AccordionSection from "@/components/Blocks/Accordion"
import HotelReservationSidePeek from "@/components/HotelReservation/SidePeek"
@@ -16,65 +18,96 @@ import MapCard from "./Map/MapCard"
import MapWithCardWrapper from "./Map/MapWithCard"
import MobileMapToggle from "./Map/MobileMapToggle"
import StaticMap from "./Map/StaticMap"
import WellnessAndExerciseSidePeek from "./SidePeeks/WellnessAndExercise"
import AmenitiesList from "./AmenitiesList"
import Facilities from "./Facilities"
import IntroSection from "./IntroSection"
import PreviewImages from "./PreviewImages"
import { Rooms } from "./Rooms"
import { AboutTheHotelSidePeek, WellnessAndExerciseSidePeek } from "./SidePeeks"
import { AboutTheHotelSidePeek } from "./SidePeeks"
import TabNavigation from "./TabNavigation"
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 { Facility } from "@/types/hotel"
export default async function HotelPage() {
const intl = await getIntl()
const lang = getLang()
export default async function HotelPage({ hotelId }: HotelPageProps) {
const googleMapsApiKey = env.GOOGLE_STATIC_MAP_KEY
const googleMapId = env.GOOGLE_DYNAMIC_MAP_ID
const hotelData = await serverClient().hotel.get()
if (!hotelData) {
return null
const lang = getLang()
const [intl, hotelPageData, hotelData] = await Promise.all([
getIntl(),
getHotelPage(),
getHotelData({ hotelId, language: lang }),
])
if (!hotelData?.data || !hotelPageData) {
return notFound()
}
const { faq, content } = hotelPageData
const {
hotelId,
hotelName,
hotelDescriptions,
hotelLocation,
hotelAddress,
hotelRatings,
hotelDetailedFacilities,
hotelImages,
roomCategories,
activitiesCard,
name,
address,
pointsOfInterest,
facilities,
faq,
alerts,
gallery,
specialAlerts,
healthAndWellness,
restaurantImages,
conferencesAndMeetings,
hotelContent,
detailedFacilities,
healthFacilities,
contact,
socials,
ecoLabels,
} = hotelData
contactInformation,
socialMedia,
hotelFacts,
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 coordinates = {
lat: hotelLocation.latitude,
lng: hotelLocation.longitude,
lat: location.latitude,
lng: location.longitude,
}
return (
<div className={styles.pageContainer}>
<div className={styles.hotelImages}>
{hotelImages?.length && (
<PreviewImages images={hotelImages} hotelName={hotelName} />
)}
{images?.length && <PreviewImages images={images} hotelName={name} />}
</div>
<TabNavigation
restaurantTitle={getRestaurantHeading(hotelDetailedFacilities)}
restaurantTitle={getRestaurantHeading(detailedFacilities)}
hasActivities={!!activitiesCard}
hasFAQ={!!faq.accordions.length}
/>
@@ -82,18 +115,18 @@ export default async function HotelPage() {
<div id={HotelHashValues.overview} className={styles.overview}>
<div className={styles.introContainer}>
<IntroSection
hotelName={hotelName}
hotelDescription={hotelDescriptions.descriptions.short}
location={hotelLocation}
address={hotelAddress}
tripAdvisor={hotelRatings?.tripAdvisor}
hotelName={name}
hotelDescription={description}
location={location}
address={address}
tripAdvisor={ratings?.tripAdvisor}
/>
<AmenitiesList detailedFacilities={hotelDetailedFacilities} />
<AmenitiesList detailedFacilities={detailedFacilities} />
</div>
{alerts.length ? (
{specialAlerts.length ? (
<div className={styles.alertsContainer}>
{alerts.map((alert) => (
{specialAlerts.map((alert) => (
<Alert
key={alert.id}
type={alert.type}
@@ -114,14 +147,14 @@ export default async function HotelPage() {
<>
<aside className={styles.mapContainer}>
<MapWithCardWrapper>
<StaticMap coordinates={coordinates} hotelName={hotelName} />
<MapCard hotelName={hotelName} pois={topThreePois} />
<StaticMap coordinates={coordinates} hotelName={name} />
<MapCard hotelName={name} pois={topThreePois} />
</MapWithCardWrapper>
</aside>
<MobileMapToggle />
<DynamicMap
apiKey={googleMapsApiKey}
hotelName={hotelName}
hotelName={name}
coordinates={coordinates}
pointsOfInterest={pointsOfInterest}
mapId={googleMapId}
@@ -138,12 +171,12 @@ export default async function HotelPage() {
Read more about the amenities here
</SidePeek>
<AboutTheHotelSidePeek
hotelAddress={hotelAddress}
coordinates={hotelLocation}
contact={contact}
socials={socials}
ecoLabels={ecoLabels}
descriptions={hotelDescriptions}
hotelAddress={address}
coordinates={location}
contact={contactInformation}
socials={socialMedia}
ecoLabels={hotelFacts.ecoLabels}
descriptions={hotelContent.texts}
/>
<SidePeek
contentKey={hotelPageParams.restaurantAndBar[lang]}

View File

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

View File

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

View File

@@ -1,13 +1,9 @@
import { metrics } from "@opentelemetry/api"
import { Lang } from "@/constants/languages"
import * as api from "@/lib/api"
import { dt } from "@/lib/dt"
import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
import { request } from "@/lib/graphql/request"
import { badRequestError, notFound } from "@/server/errors/trpc"
import { badRequestError } from "@/server/errors/trpc"
import {
contentStackUidWithServiceProcedure,
publicProcedure,
router,
safeProtectedServiceProcedure,
@@ -17,13 +13,6 @@ import { toApiLang } from "@/server/utils"
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 {
getBreakfastPackageInputSchema,
@@ -52,13 +41,10 @@ import {
TWENTYFOUR_HOURS,
} from "./utils"
import { FacilityCardTypeEnum } from "@/types/components/hotelPage/facilities"
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
import { HotelTypeEnum } from "@/types/enums/hotelType"
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 getHotelCounter = meter.createCounter("trpc.hotel.get")
@@ -111,55 +97,6 @@ const breakfastPackagesFailCounter = meter.createCounter(
"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(
async (input: HotelDataInput, serviceToken: string) => {
const { hotelId, language, isCardOnlyPayment } = input
@@ -273,90 +210,6 @@ export const getHotelData = cache(
)
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({
hotels: serviceProcedure
.input(getHotelsAvailabilityInputSchema)

View File

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

View File

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