feat(SW-201): Refactoring how we fetch hotel page data
This commit is contained in:
@@ -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}`)
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
3
types/components/hotelPage/hotelPage.ts
Normal file
3
types/components/hotelPage/hotelPage.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface HotelPageProps {
|
||||
hotelId: string
|
||||
}
|
||||
Reference in New Issue
Block a user