feat(SW-664): Hotel listing component and queries for content pages
This commit is contained in:
73
app/api/web/revalidate/hotel/route.ts
Normal file
73
app/api/web/revalidate/hotel/route.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { revalidateTag } from "next/cache"
|
||||
import { headers } from "next/headers"
|
||||
import { z } from "zod"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import { badRequest, internalServerError, notFound } from "@/server/errors/next"
|
||||
|
||||
import { generateHotelUrlTag } from "@/utils/generateTag"
|
||||
|
||||
import type { NextRequest } from "next/server"
|
||||
|
||||
const validateJsonBody = z.object({
|
||||
data: z.object({
|
||||
content_type: z.object({
|
||||
uid: z.literal("hotel_page"),
|
||||
}),
|
||||
entry: z.object({
|
||||
hotel_page_id: z.string(),
|
||||
locale: z.nativeEnum(Lang),
|
||||
publish_details: z.object({ locale: z.nativeEnum(Lang) }).optional(),
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const headersList = headers()
|
||||
const secret = headersList.get("x-revalidate-secret")
|
||||
|
||||
if (secret !== env.REVALIDATE_SECRET) {
|
||||
console.error(`Invalid Secret`)
|
||||
console.error({ secret })
|
||||
return badRequest({ revalidated: false, now: Date.now() })
|
||||
}
|
||||
|
||||
const data = await request.json()
|
||||
const validatedData = validateJsonBody.safeParse(data)
|
||||
if (!validatedData.success) {
|
||||
console.error("Bad validation for `validatedData` in hotel revalidation")
|
||||
console.error(validatedData.error)
|
||||
return internalServerError({ revalidated: false, now: Date.now() })
|
||||
}
|
||||
|
||||
const {
|
||||
data: {
|
||||
data: { content_type, entry },
|
||||
},
|
||||
} = validatedData
|
||||
|
||||
// The publish_details.locale is the locale that the entry is published in, regardless if it is "localized" or not
|
||||
const locale = entry.publish_details?.locale ?? entry.locale
|
||||
|
||||
let tag = ""
|
||||
if (content_type.uid === "hotel_page") {
|
||||
const tag = generateHotelUrlTag(locale, entry.hotel_page_id)
|
||||
} else {
|
||||
console.error(
|
||||
`Invalid content_type, received ${content_type.uid}, expected "hotel_page"`
|
||||
)
|
||||
return notFound({ revalidated: false, now: Date.now() })
|
||||
}
|
||||
|
||||
console.info(`Revalidating hotel url tag: ${tag}`)
|
||||
revalidateTag(tag)
|
||||
|
||||
return Response.json({ revalidated: true, now: Date.now() })
|
||||
} catch (error) {
|
||||
console.error("Failed to revalidate tag(s) for hotel")
|
||||
console.error(error)
|
||||
return internalServerError({ revalidated: false, now: Date.now() })
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ScandicLogoIcon } from "@/components/Icons"
|
||||
import HotelLogo from "@/components/Icons/Logos"
|
||||
import Image from "@/components/Image"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
@@ -10,25 +10,27 @@ import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
import getSingleDecimal from "@/utils/numberFormatting"
|
||||
|
||||
import { getTypeSpecificInformation } from "./utils"
|
||||
|
||||
import styles from "./hotelListingItem.module.css"
|
||||
|
||||
import type { HotelListingItemProps } from "@/types/components/contentPage/hotelListingItem"
|
||||
|
||||
export default async function HotelListingItem({
|
||||
imageUrl,
|
||||
altText,
|
||||
name,
|
||||
address,
|
||||
distanceToCentre,
|
||||
description,
|
||||
link,
|
||||
hotel,
|
||||
contentType = "hotel",
|
||||
url,
|
||||
}: HotelListingItemProps) {
|
||||
const intl = await getIntl()
|
||||
const { description, imageSrc, altText } = getTypeSpecificInformation(
|
||||
contentType,
|
||||
hotel
|
||||
)
|
||||
|
||||
return (
|
||||
<article className={styles.container}>
|
||||
<Image
|
||||
src={imageUrl}
|
||||
src={imageSrc}
|
||||
alt={altText}
|
||||
width={300}
|
||||
height={200}
|
||||
@@ -36,35 +38,43 @@ export default async function HotelListingItem({
|
||||
/>
|
||||
<section className={styles.content}>
|
||||
<div className={styles.intro}>
|
||||
<ScandicLogoIcon color="red" />
|
||||
<HotelLogo hotelId={hotel.operaId} hotelType={hotel.hotelType} />
|
||||
<Subtitle asChild>
|
||||
<Title as="h3">{name}</Title>
|
||||
<Title as="h3">{hotel.name}</Title>
|
||||
</Subtitle>
|
||||
<div className={styles.captions}>
|
||||
<Caption color="uiTextPlaceholder">{address}</Caption>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{hotel.address.streetAddress}
|
||||
</Caption>
|
||||
<div className={styles.dividerContainer}>
|
||||
<Divider variant="vertical" color="beige" />
|
||||
</div>
|
||||
<Caption color="uiTextPlaceholder">
|
||||
{intl.formatMessage(
|
||||
{ id: "Distance in km to city centre" },
|
||||
{ number: getSingleDecimal(distanceToCentre / 1000) }
|
||||
{
|
||||
number: getSingleDecimal(
|
||||
hotel.location.distanceToCentre / 1000
|
||||
),
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
</div>
|
||||
</div>
|
||||
<Body>{description}</Body>
|
||||
<Button
|
||||
intent="primary"
|
||||
theme="base"
|
||||
size="small"
|
||||
className={styles.button}
|
||||
asChild
|
||||
>
|
||||
<Link href={link} color="white">
|
||||
{intl.formatMessage({ id: "See hotel details" })}
|
||||
</Link>
|
||||
</Button>
|
||||
{url && (
|
||||
<Button
|
||||
intent="primary"
|
||||
theme="base"
|
||||
size="small"
|
||||
className={styles.button}
|
||||
asChild
|
||||
>
|
||||
<Link href={url} color="white">
|
||||
{intl.formatMessage({ id: "See hotel details" })}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</section>
|
||||
</article>
|
||||
)
|
||||
36
components/Blocks/HotelListing/HotelListingItem/utils.ts
Normal file
36
components/Blocks/HotelListing/HotelListingItem/utils.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { HotelListing } from "@/types/trpc/routers/contentstack/blocks"
|
||||
|
||||
export function getTypeSpecificInformation(
|
||||
contentType: HotelListing["contentType"],
|
||||
hotel: Hotel
|
||||
) {
|
||||
const { restaurantsOverviewPage, images } = hotel.hotelContent
|
||||
const { descriptions, meetingDescription } = hotel.hotelContent.texts
|
||||
const hotelData = {
|
||||
description: descriptions.short,
|
||||
imageSrc: images.imageSizes.small,
|
||||
altText: images.metaData.altText,
|
||||
}
|
||||
switch (contentType) {
|
||||
case "meeting":
|
||||
const meetingImage = hotel.conferencesAndMeetings?.heroImages[0]
|
||||
return {
|
||||
description: meetingDescription?.short || hotelData.description,
|
||||
imageSrc: meetingImage?.imageSizes.small || hotelData.imageSrc,
|
||||
altText: meetingImage?.metaData.altText || hotelData.altText,
|
||||
}
|
||||
case "restaurant":
|
||||
const restaurantImage = hotel.restaurantImages?.heroImages[0]
|
||||
return {
|
||||
description:
|
||||
restaurantsOverviewPage.restaurantsContentDescriptionShort ||
|
||||
hotelData.description,
|
||||
imageSrc: restaurantImage?.imageSizes.small || hotelData.imageSrc,
|
||||
altText: restaurantImage?.metaData.altText || hotelData.altText,
|
||||
}
|
||||
case "hotel":
|
||||
default:
|
||||
return hotelData
|
||||
}
|
||||
}
|
||||
40
components/Blocks/HotelListing/index.tsx
Normal file
40
components/Blocks/HotelListing/index.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { getHotels } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import SectionContainer from "@/components/Section/Container"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import HotelListingItem from "./HotelListingItem"
|
||||
|
||||
import type { HotelListingProps } from "@/types/components/blocks/hotelListing"
|
||||
|
||||
export default async function HotelListing({
|
||||
heading,
|
||||
locationFilter,
|
||||
hotelsToInclude,
|
||||
contentType,
|
||||
}: HotelListingProps) {
|
||||
const hotels = await getHotels({
|
||||
locationFilter,
|
||||
hotelsToInclude: hotelsToInclude,
|
||||
})
|
||||
|
||||
if (!hotels.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<SectionContainer>
|
||||
<Title level="h4" as="h3" textTransform="capitalize">
|
||||
{heading}
|
||||
</Title>
|
||||
{hotels.map(({ data, url }) => (
|
||||
<HotelListingItem
|
||||
key={data.name}
|
||||
hotel={data}
|
||||
contentType={contentType}
|
||||
url={url}
|
||||
/>
|
||||
))}
|
||||
</SectionContainer>
|
||||
)
|
||||
}
|
||||
@@ -6,13 +6,14 @@ import UspGrid from "@/components/Blocks/UspGrid"
|
||||
import JsonToHtml from "@/components/JsonToHtml"
|
||||
|
||||
import AccordionSection from "./Accordion"
|
||||
import HotelListing from "./HotelListing"
|
||||
import Table from "./Table"
|
||||
|
||||
import type { BlocksProps } from "@/types/components/blocks"
|
||||
import { BlocksEnums } from "@/types/enums/blocks"
|
||||
|
||||
export default function Blocks({ blocks }: BlocksProps) {
|
||||
return blocks.map((block, idx) => {
|
||||
return blocks.map(async (block, idx) => {
|
||||
const firstItem = idx === 0
|
||||
switch (block.typename) {
|
||||
case BlocksEnums.block.Accordion:
|
||||
@@ -48,6 +49,21 @@ export default function Blocks({ blocks }: BlocksProps) {
|
||||
key={`${block.dynamic_content.title}-${idx}`}
|
||||
/>
|
||||
)
|
||||
case BlocksEnums.block.HotelListing:
|
||||
const { heading, contentType, locationFilter, hotelsToInclude } =
|
||||
block.hotel_listing
|
||||
if (!locationFilter && !hotelsToInclude.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<HotelListing
|
||||
heading={heading}
|
||||
locationFilter={locationFilter}
|
||||
hotelsToInclude={hotelsToInclude}
|
||||
contentType={contentType}
|
||||
/>
|
||||
)
|
||||
case BlocksEnums.block.Shortcuts:
|
||||
return (
|
||||
<ShortcutsList
|
||||
|
||||
@@ -7,6 +7,7 @@ import { selectRate } from "@/constants/routes/hotelReservation"
|
||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||
|
||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||
import HotelLogo from "@/components/Icons/Logos"
|
||||
import ImageGallery from "@/components/ImageGallery"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
@@ -18,7 +19,6 @@ import getSingleDecimal from "@/utils/numberFormatting"
|
||||
|
||||
import ReadMore from "../ReadMore"
|
||||
import TripAdvisorChip from "../TripAdvisorChip"
|
||||
import HotelLogo from "./HotelLogo"
|
||||
import HotelPriceCard from "./HotelPriceCard"
|
||||
import NoPriceAvailableCard from "./NoPriceAvailableCard"
|
||||
import { hotelCardVariants } from "./variants"
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ScandicLogoIcon,
|
||||
} from "@/components/Icons"
|
||||
|
||||
import type { HotelLogoProps } from "@/types/components/hotelReservation/selectHotel/hotelLogoProps"
|
||||
import type { HotelLogoProps } from "@/types/components/hotelLogo"
|
||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||
import { SignatureHotelEnum } from "@/types/enums/signatureHotel"
|
||||
|
||||
24
lib/graphql/Fragments/Blocks/HotelListing.graphql
Normal file
24
lib/graphql/Fragments/Blocks/HotelListing.graphql
Normal file
@@ -0,0 +1,24 @@
|
||||
fragment HotelListing on HotelListing {
|
||||
heading
|
||||
location_filter {
|
||||
city_denmark
|
||||
city_finland
|
||||
city_germany
|
||||
city_norway
|
||||
city_poland
|
||||
city_sweden
|
||||
country
|
||||
excluded
|
||||
}
|
||||
manual_filter {
|
||||
hotels
|
||||
}
|
||||
content_type
|
||||
}
|
||||
|
||||
fragment HotelListing_ContentPage on ContentPageBlocksHotelListing {
|
||||
__typename
|
||||
hotel_listing {
|
||||
...HotelListing
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
#import "../../Fragments/Blocks/CardsGrid.graphql"
|
||||
#import "../../Fragments/Blocks/Content.graphql"
|
||||
#import "../../Fragments/Blocks/DynamicContent.graphql"
|
||||
#import "../../Fragments/Blocks/HotelListing.graphql"
|
||||
#import "../../Fragments/Blocks/Shortcuts.graphql"
|
||||
#import "../../Fragments/Blocks/Table.graphql"
|
||||
#import "../../Fragments/Blocks/TextCols.graphql"
|
||||
@@ -65,6 +66,7 @@ query GetContentPageBlocksBatch2($locale: String!, $uid: String!) {
|
||||
content_page(uid: $uid, locale: $locale) {
|
||||
blocks {
|
||||
__typename
|
||||
...HotelListing_ContentPage
|
||||
...Shortcuts_ContentPage
|
||||
...Table_ContentPage
|
||||
...TextCols_ContentPage
|
||||
|
||||
12
lib/graphql/Query/HotelPage/HotelPageUrl.graphql
Normal file
12
lib/graphql/Query/HotelPage/HotelPageUrl.graphql
Normal file
@@ -0,0 +1,12 @@
|
||||
#import "../../Fragments/System.graphql"
|
||||
|
||||
query GetHotelPageUrl($locale: String!, $hotelId: String!) {
|
||||
all_hotel_page(locale: $locale, where: { hotel_page_id: $hotelId }) {
|
||||
items {
|
||||
url
|
||||
system {
|
||||
...System
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from "@/types/requests/packages"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type {
|
||||
GetHotelsInput,
|
||||
GetRoomsAvailabilityInput,
|
||||
GetSelectedRoomAvailabilityInput,
|
||||
HotelDataInput,
|
||||
@@ -62,6 +63,12 @@ export const getUserTracking = cache(async function getMemoizedUserTracking() {
|
||||
return serverClient().user.tracking()
|
||||
})
|
||||
|
||||
export const getHotels = cache(async function getMemoizedHotels(
|
||||
input: GetHotelsInput
|
||||
) {
|
||||
return serverClient().hotel.hotels.get(input)
|
||||
})
|
||||
|
||||
export const getHotelData = cache(async function getMemoizedHotelData(
|
||||
input: HotelDataInput
|
||||
) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
dynamicContentRefsSchema,
|
||||
dynamicContentSchema as blockDynamicContentSchema,
|
||||
} from "../schemas/blocks/dynamicContent"
|
||||
import { hotelListingSchema } from "../schemas/blocks/hotelListing"
|
||||
import {
|
||||
shortcutsRefsSchema,
|
||||
shortcutsSchema,
|
||||
@@ -103,6 +104,12 @@ export const contentPageAccordion = z
|
||||
})
|
||||
.merge(accordionSchema)
|
||||
|
||||
export const contentPageHotelListing = z
|
||||
.object({
|
||||
__typename: z.literal(ContentPageEnum.ContentStack.blocks.HotelListing),
|
||||
})
|
||||
.merge(hotelListingSchema)
|
||||
|
||||
export const blocksSchema = z.discriminatedUnion("__typename", [
|
||||
contentPageAccordion,
|
||||
contentPageCards,
|
||||
@@ -112,6 +119,7 @@ export const blocksSchema = z.discriminatedUnion("__typename", [
|
||||
contentPageTable,
|
||||
contentPageTextCols,
|
||||
contentPageUspGrid,
|
||||
contentPageHotelListing,
|
||||
])
|
||||
|
||||
export const contentPageSidebarContent = z
|
||||
|
||||
@@ -2,6 +2,8 @@ import { z } from "zod"
|
||||
|
||||
import { discriminatedUnionArray } from "@/lib/discriminatedUnion"
|
||||
|
||||
import { removeMultipleSlashes } from "@/utils/url"
|
||||
|
||||
import {
|
||||
activitiesCardRefSchema,
|
||||
activitiesCardSchema,
|
||||
@@ -58,3 +60,26 @@ export const hotelPageRefsSchema = z.object({
|
||||
url: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
export const hotelPageUrlSchema = z
|
||||
.object({
|
||||
all_hotel_page: z.object({
|
||||
items: z
|
||||
.array(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
system: systemSchema,
|
||||
})
|
||||
)
|
||||
.max(1),
|
||||
}),
|
||||
})
|
||||
.transform((data) => {
|
||||
const page = data.all_hotel_page.items[0]
|
||||
if (!page) {
|
||||
return null
|
||||
}
|
||||
|
||||
const lang = page.system.locale
|
||||
return removeMultipleSlashes(`/${lang}/${page.url}`)
|
||||
})
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
@@ -8,20 +6,13 @@ import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||
import { generateTag } from "@/utils/generateTag"
|
||||
|
||||
import { hotelPageSchema } from "./output"
|
||||
import {
|
||||
getHotelPageCounter,
|
||||
getHotelPageFailCounter,
|
||||
getHotelPageSuccessCounter,
|
||||
} from "./telemetry"
|
||||
|
||||
import { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage"
|
||||
|
||||
// OpenTelemetry metrics
|
||||
const meter = metrics.getMeter("trpc.contentstack.hotelPage")
|
||||
const getHotelPageCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get"
|
||||
)
|
||||
const getHotelPageSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get-success"
|
||||
)
|
||||
const getHotelPageFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get-fail"
|
||||
)
|
||||
import type { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage"
|
||||
|
||||
export const hotelPageQueryRouter = router({
|
||||
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
||||
|
||||
33
server/routers/contentstack/hotelPage/telemetry.ts
Normal file
33
server/routers/contentstack/hotelPage/telemetry.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
const meter = metrics.getMeter("trpc.contentstack.hotelPage")
|
||||
|
||||
export const getHotelPageRefsCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get"
|
||||
)
|
||||
export const getHotelPageRefsFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get-fail"
|
||||
)
|
||||
export const getHotelPageRefsSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get-success"
|
||||
)
|
||||
|
||||
export const getHotelPageCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get"
|
||||
)
|
||||
export const getHotelPageSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get-success"
|
||||
)
|
||||
export const getHotelPageFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get-fail"
|
||||
)
|
||||
|
||||
export const getHotelPageUrlCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPageUrl.get"
|
||||
)
|
||||
export const getHotelPageUrlSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPageUrl.get-success"
|
||||
)
|
||||
export const getHotelPageUrlFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPageUrl.get-fail"
|
||||
)
|
||||
@@ -1,37 +1,32 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { GetHotelPageRefs } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
|
||||
import { GetHotelPageUrl } from "@/lib/graphql/Query/HotelPage/HotelPageUrl.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
|
||||
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
||||
import {
|
||||
generateHotelUrlTag,
|
||||
generateTag,
|
||||
generateTagsFromSystem,
|
||||
} from "@/utils/generateTag"
|
||||
|
||||
import { hotelPageRefsSchema } from "./output"
|
||||
import { hotelPageRefsSchema, hotelPageUrlSchema } from "./output"
|
||||
import {
|
||||
getHotelPageRefsCounter,
|
||||
getHotelPageRefsFailCounter,
|
||||
getHotelPageRefsSuccessCounter,
|
||||
getHotelPageUrlCounter,
|
||||
getHotelPageUrlFailCounter,
|
||||
getHotelPageUrlSuccessCounter,
|
||||
} from "./telemetry"
|
||||
|
||||
import { HotelPageEnum } from "@/types/enums/hotelPage"
|
||||
import { System } from "@/types/requests/system"
|
||||
import {
|
||||
import type { System } from "@/types/requests/system"
|
||||
import type {
|
||||
GetHotelPageRefsSchema,
|
||||
GetHotelPageUrlData,
|
||||
HotelPageRefs,
|
||||
} from "@/types/trpc/routers/contentstack/hotelPage"
|
||||
|
||||
const meter = metrics.getMeter("trpc.hotelPage")
|
||||
// OpenTelemetry metrics: HotelPage
|
||||
|
||||
export const getHotelPageCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get"
|
||||
)
|
||||
|
||||
const getHotelPageRefsCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get"
|
||||
)
|
||||
const getHotelPageRefsFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get-fail"
|
||||
)
|
||||
const getHotelPageRefsSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.hotelPage.get-success"
|
||||
)
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
export async function fetchHotelPageRefs(lang: Lang, uid: string) {
|
||||
getHotelPageRefsCounter.add(1, { lang, uid })
|
||||
@@ -140,3 +135,64 @@ export function getConnections({ hotel_page }: HotelPageRefs) {
|
||||
}
|
||||
return connections
|
||||
}
|
||||
|
||||
export async function getHotelPageUrl(lang: Lang, hotelId: string) {
|
||||
getHotelPageUrlCounter.add(1, { lang, hotelId })
|
||||
console.info(
|
||||
"contentstack.hotelPageUrl start",
|
||||
JSON.stringify({ query: { lang, hotelId } })
|
||||
)
|
||||
const response = await request<GetHotelPageUrlData>(
|
||||
GetHotelPageUrl,
|
||||
{
|
||||
locale: lang,
|
||||
hotelId,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateHotelUrlTag(lang, hotelId)],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
getHotelPageUrlFailCounter.add(1, {
|
||||
lang,
|
||||
hotelId,
|
||||
error_type: "not_found",
|
||||
error: `Hotel page not found for hotelId: ${hotelId}`,
|
||||
})
|
||||
console.error(
|
||||
"contentstack.hotelPageUrl not found error",
|
||||
JSON.stringify({ query: { lang, hotelId } })
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const validatedHotelPageUrl = hotelPageUrlSchema.safeParse(response.data)
|
||||
|
||||
if (!validatedHotelPageUrl.success) {
|
||||
getHotelPageUrlFailCounter.add(1, {
|
||||
lang,
|
||||
hotelId,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedHotelPageUrl.error),
|
||||
})
|
||||
console.error(
|
||||
"contentstack.hotelPageUrl validation error",
|
||||
JSON.stringify({
|
||||
query: { lang, hotelId },
|
||||
error: validatedHotelPageUrl.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
getHotelPageUrlSuccessCounter.add(1, { lang, hotelId })
|
||||
console.info(
|
||||
"contentstack.hotelPageUrl success",
|
||||
JSON.stringify({ query: { lang, hotelId } })
|
||||
)
|
||||
|
||||
return validatedHotelPageUrl.data
|
||||
}
|
||||
|
||||
62
server/routers/contentstack/schemas/blocks/hotelListing.ts
Normal file
62
server/routers/contentstack/schemas/blocks/hotelListing.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { BlocksEnums } from "@/types/enums/blocks"
|
||||
import { Country } from "@/types/enums/country"
|
||||
|
||||
export const locationFilterSchema = z
|
||||
.object({
|
||||
country: z.nativeEnum(Country).nullable(),
|
||||
city_denmark: z.string().optional().nullable(),
|
||||
city_finland: z.string().optional().nullable(),
|
||||
city_germany: z.string().optional().nullable(),
|
||||
city_poland: z.string().optional().nullable(),
|
||||
city_norway: z.string().optional().nullable(),
|
||||
city_sweden: z.string().optional().nullable(),
|
||||
excluded: z.array(z.string()),
|
||||
})
|
||||
.transform((data) => {
|
||||
const cities = [
|
||||
data.city_denmark,
|
||||
data.city_finland,
|
||||
data.city_germany,
|
||||
data.city_poland,
|
||||
data.city_norway,
|
||||
data.city_sweden,
|
||||
].filter((city): city is string => Boolean(city))
|
||||
|
||||
// When there are multiple city values, we return null as the filter is invalid.
|
||||
if (cities.length > 1) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
country: cities.length ? null : data.country,
|
||||
city: cities.length ? cities[0] : null,
|
||||
excluded: data.excluded,
|
||||
}
|
||||
})
|
||||
|
||||
export const hotelListingSchema = z.object({
|
||||
typename: z
|
||||
.literal(BlocksEnums.block.HotelListing)
|
||||
.default(BlocksEnums.block.HotelListing),
|
||||
hotel_listing: z
|
||||
.object({
|
||||
heading: z.string().optional(),
|
||||
location_filter: locationFilterSchema,
|
||||
manual_filter: z
|
||||
.object({
|
||||
hotels: z.array(z.string()),
|
||||
})
|
||||
.transform((data) => ({ hotels: data.hotels.filter(Boolean) })),
|
||||
content_type: z.enum(["hotel", "restaurant", "meeting"]),
|
||||
})
|
||||
.transform(({ heading, location_filter, manual_filter, content_type }) => {
|
||||
return {
|
||||
heading,
|
||||
locationFilter: location_filter,
|
||||
hotelsToInclude: manual_filter.hotels,
|
||||
contentType: content_type,
|
||||
}
|
||||
}),
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||
import { Country } from "@/types/enums/country"
|
||||
|
||||
export const getHotelsAvailabilityInputSchema = z.object({
|
||||
cityId: z.string(),
|
||||
@@ -53,6 +54,18 @@ export const getHotelDataInputSchema = z.object({
|
||||
|
||||
export type HotelDataInput = z.input<typeof getHotelDataInputSchema>
|
||||
|
||||
export const getHotelsInput = z.object({
|
||||
locationFilter: z
|
||||
.object({
|
||||
city: z.string().nullable(),
|
||||
country: z.nativeEnum(Country).nullable(),
|
||||
excluded: z.array(z.string()),
|
||||
})
|
||||
.nullable(),
|
||||
hotelsToInclude: z.array(z.string()),
|
||||
})
|
||||
export interface GetHotelsInput extends z.infer<typeof getHotelsInput> {}
|
||||
|
||||
export const getBreakfastPackageInputSchema = z.object({
|
||||
adults: z.number().min(1, { message: "at least one adult is required" }),
|
||||
fromDate: z
|
||||
|
||||
@@ -870,3 +870,14 @@ export const getRoomPackagesSchema = z
|
||||
.optional(),
|
||||
})
|
||||
.transform((data) => data.data?.attributes?.packages ?? [])
|
||||
|
||||
export const getHotelIdsByCityIdSchema = z
|
||||
.object({
|
||||
data: z.array(
|
||||
z.object({
|
||||
// We only care about the hotel id
|
||||
id: z.string(),
|
||||
})
|
||||
),
|
||||
})
|
||||
.transform((data) => data.data.map((hotel) => hotel.id))
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import * as api from "@/lib/api"
|
||||
import { dt } from "@/lib/dt"
|
||||
import { badRequestError } from "@/server/errors/trpc"
|
||||
import {
|
||||
contentStackBaseWithServiceProcedure,
|
||||
publicProcedure,
|
||||
router,
|
||||
safeProtectedServiceProcedure,
|
||||
@@ -13,12 +12,14 @@ import { toApiLang } from "@/server/utils"
|
||||
|
||||
import { cache } from "@/utils/cache"
|
||||
|
||||
import { getHotelPageUrl } from "../contentstack/hotelPage/utils"
|
||||
import { getVerifiedUser, parsedUser } from "../user/query"
|
||||
import {
|
||||
getBreakfastPackageInputSchema,
|
||||
getCityCoordinatesInputSchema,
|
||||
getHotelDataInputSchema,
|
||||
getHotelsAvailabilityInputSchema,
|
||||
getHotelsInput,
|
||||
getRatesInputSchema,
|
||||
getRoomPackagesInputSchema,
|
||||
getRoomsAvailabilityInputSchema,
|
||||
@@ -33,10 +34,35 @@ import {
|
||||
getRoomPackagesSchema,
|
||||
getRoomsAvailabilitySchema,
|
||||
} from "./output"
|
||||
import {
|
||||
breakfastPackagesCounter,
|
||||
breakfastPackagesFailCounter,
|
||||
breakfastPackagesSuccessCounter,
|
||||
getHotelCounter,
|
||||
getHotelFailCounter,
|
||||
getHotelsCounter,
|
||||
getHotelsFailCounter,
|
||||
getHotelsSuccessCounter,
|
||||
getHotelSuccessCounter,
|
||||
getPackagesCounter,
|
||||
getPackagesFailCounter,
|
||||
getPackagesSuccessCounter,
|
||||
hotelsAvailabilityCounter,
|
||||
hotelsAvailabilityFailCounter,
|
||||
hotelsAvailabilitySuccessCounter,
|
||||
roomsAvailabilityCounter,
|
||||
roomsAvailabilityFailCounter,
|
||||
roomsAvailabilitySuccessCounter,
|
||||
selectedRoomAvailabilityCounter,
|
||||
selectedRoomAvailabilityFailCounter,
|
||||
selectedRoomAvailabilitySuccessCounter,
|
||||
} from "./telemetry"
|
||||
import tempRatesData from "./tempRatesData.json"
|
||||
import {
|
||||
getCitiesByCountry,
|
||||
getCountries,
|
||||
getHotelIdsByCityId,
|
||||
getHotelIdsByCountry,
|
||||
getLocations,
|
||||
TWENTYFOUR_HOURS,
|
||||
} from "./utils"
|
||||
@@ -45,57 +71,9 @@ import type { BedTypeSelection } from "@/types/components/hotelReservation/enter
|
||||
import { BreakfastPackageEnum } from "@/types/enums/breakfast"
|
||||
import { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
|
||||
const meter = metrics.getMeter("trpc.hotels")
|
||||
const getHotelCounter = meter.createCounter("trpc.hotel.get")
|
||||
const getHotelSuccessCounter = meter.createCounter("trpc.hotel.get-success")
|
||||
const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail")
|
||||
|
||||
const getPackagesCounter = meter.createCounter("trpc.hotel.packages.get")
|
||||
const getPackagesSuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.packages.get-success"
|
||||
)
|
||||
const getPackagesFailCounter = meter.createCounter(
|
||||
"trpc.hotel.packages.get-fail"
|
||||
)
|
||||
|
||||
const hotelsAvailabilityCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.hotels"
|
||||
)
|
||||
const hotelsAvailabilitySuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.hotels-success"
|
||||
)
|
||||
const hotelsAvailabilityFailCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.hotels-fail"
|
||||
)
|
||||
|
||||
const roomsAvailabilityCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.rooms"
|
||||
)
|
||||
const roomsAvailabilitySuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.rooms-success"
|
||||
)
|
||||
const roomsAvailabilityFailCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.rooms-fail"
|
||||
)
|
||||
|
||||
const selectedRoomAvailabilityCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.room"
|
||||
)
|
||||
const selectedRoomAvailabilitySuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.room-success"
|
||||
)
|
||||
const selectedRoomAvailabilityFailCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.room-fail"
|
||||
)
|
||||
|
||||
const breakfastPackagesCounter = meter.createCounter("trpc.package.breakfast")
|
||||
const breakfastPackagesSuccessCounter = meter.createCounter(
|
||||
"trpc.package.breakfast-success"
|
||||
)
|
||||
const breakfastPackagesFailCounter = meter.createCounter(
|
||||
"trpc.package.breakfast-fail"
|
||||
)
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { HotelPageUrl } from "@/types/trpc/routers/contentstack/hotelPage"
|
||||
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
|
||||
|
||||
export const getHotelData = cache(
|
||||
async (input: HotelDataInput, serviceToken: string) => {
|
||||
@@ -695,6 +673,199 @@ export const hotelQueryRouter = router({
|
||||
return getHotelData(input, ctx.serviceToken)
|
||||
}),
|
||||
}),
|
||||
hotels: router({
|
||||
get: contentStackBaseWithServiceProcedure
|
||||
.input(getHotelsInput)
|
||||
.query(async function ({ ctx, input }) {
|
||||
const { locationFilter, hotelsToInclude } = input
|
||||
|
||||
const language = ctx.lang
|
||||
const options: RequestOptionsWithOutBody = {
|
||||
cache: "force-cache",
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
next: {
|
||||
revalidate: TWENTYFOUR_HOURS,
|
||||
},
|
||||
}
|
||||
|
||||
let hotelsToFetch: string[] = []
|
||||
|
||||
getHotelsCounter.add(1, {
|
||||
input: JSON.stringify(input),
|
||||
language,
|
||||
})
|
||||
console.info(
|
||||
"api.hotel.hotels start",
|
||||
JSON.stringify({
|
||||
query: {
|
||||
...input,
|
||||
language,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
if (hotelsToInclude.length) {
|
||||
hotelsToFetch = hotelsToInclude
|
||||
} else if (locationFilter?.city) {
|
||||
const locationsParams = new URLSearchParams({
|
||||
language: toApiLang(ctx.lang),
|
||||
})
|
||||
const locations = await getLocations(
|
||||
ctx.lang,
|
||||
options,
|
||||
locationsParams,
|
||||
null
|
||||
)
|
||||
if (!locations || "error" in locations) {
|
||||
return []
|
||||
}
|
||||
|
||||
const cityId = locations
|
||||
.filter((loc): loc is CityLocation => loc.type === "cities")
|
||||
.find((loc) => loc.cityIdentifier === locationFilter.city)?.id
|
||||
|
||||
if (!cityId) {
|
||||
getHotelsFailCounter.add(1, {
|
||||
input: JSON.stringify(input),
|
||||
language,
|
||||
error_type: "not_found",
|
||||
error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
|
||||
})
|
||||
|
||||
console.error(
|
||||
"api.hotel.hotels not found error",
|
||||
JSON.stringify({
|
||||
query: { ...input, language },
|
||||
error: `CityId not found for cityIdentifier: ${locationFilter.city}`,
|
||||
})
|
||||
)
|
||||
return []
|
||||
}
|
||||
const hotelIdsParams = new URLSearchParams({
|
||||
language: ctx.lang,
|
||||
city: cityId,
|
||||
onlyBasicInfo: "true",
|
||||
})
|
||||
const hotelIds = await getHotelIdsByCityId(
|
||||
cityId,
|
||||
options,
|
||||
hotelIdsParams
|
||||
)
|
||||
|
||||
if (!hotelIds?.length) {
|
||||
getHotelsFailCounter.add(1, {
|
||||
cityId,
|
||||
language,
|
||||
error_type: "not_found",
|
||||
error: `No hotelIds found for cityId: ${cityId}`,
|
||||
})
|
||||
|
||||
console.error(
|
||||
"api.hotel.hotels not found error",
|
||||
JSON.stringify({
|
||||
query: { cityId, language },
|
||||
error: `No hotelIds found for cityId: ${cityId}`,
|
||||
})
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
const filteredHotelIds = hotelIds.filter(
|
||||
(id) => !locationFilter.excluded.includes(id)
|
||||
)
|
||||
|
||||
hotelsToFetch = filteredHotelIds
|
||||
} else if (locationFilter?.country) {
|
||||
const hotelIdsParams = new URLSearchParams({
|
||||
language: ctx.lang,
|
||||
country: locationFilter.country,
|
||||
onlyBasicInfo: "true",
|
||||
})
|
||||
const hotelIds = await getHotelIdsByCountry(
|
||||
locationFilter.country,
|
||||
options,
|
||||
hotelIdsParams
|
||||
)
|
||||
|
||||
if (!hotelIds?.length) {
|
||||
getHotelsFailCounter.add(1, {
|
||||
country: locationFilter.country,
|
||||
language,
|
||||
error_type: "not_found",
|
||||
error: `No hotelIds found for country: ${locationFilter.country}`,
|
||||
})
|
||||
|
||||
console.error(
|
||||
"api.hotel.hotels not found error",
|
||||
JSON.stringify({
|
||||
query: { country: locationFilter.country, language },
|
||||
error: `No hotelIds found for cityId: ${locationFilter.country}`,
|
||||
})
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
const filteredHotelIds = hotelIds.filter(
|
||||
(id) => !locationFilter.excluded.includes(id)
|
||||
)
|
||||
|
||||
hotelsToFetch = filteredHotelIds
|
||||
}
|
||||
|
||||
if (!hotelsToFetch.length) {
|
||||
getHotelsFailCounter.add(1, {
|
||||
input: JSON.stringify(input),
|
||||
language,
|
||||
error_type: "not_found",
|
||||
error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
|
||||
})
|
||||
|
||||
console.error(
|
||||
"api.hotel.hotels not found error",
|
||||
JSON.stringify({
|
||||
query: JSON.stringify(input),
|
||||
error: `Couldn't find any hotels for given input: ${JSON.stringify(input)}`,
|
||||
})
|
||||
)
|
||||
return []
|
||||
}
|
||||
|
||||
const hotels = await Promise.all(
|
||||
hotelsToFetch.map(async (hotelId) => {
|
||||
const [hotelData, url] = await Promise.all([
|
||||
getHotelData({ hotelId, language }, ctx.serviceToken),
|
||||
getHotelPageUrl(language, hotelId),
|
||||
])
|
||||
|
||||
return {
|
||||
data: hotelData?.data.attributes,
|
||||
url,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
getHotelsSuccessCounter.add(1, {
|
||||
input: JSON.stringify(input),
|
||||
language,
|
||||
})
|
||||
|
||||
console.info(
|
||||
"api.hotels success",
|
||||
JSON.stringify({
|
||||
query: {
|
||||
input: JSON.stringify(input),
|
||||
language,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
return hotels.filter(
|
||||
(hotel): hotel is { data: Hotel; url: HotelPageUrl } => !!hotel.data
|
||||
)
|
||||
}),
|
||||
}),
|
||||
locations: router({
|
||||
get: serviceProcedure.query(async function ({ ctx }) {
|
||||
const searchParams = new URLSearchParams()
|
||||
|
||||
74
server/routers/hotels/telemetry.ts
Normal file
74
server/routers/hotels/telemetry.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
const meter = metrics.getMeter("trpc.hotels")
|
||||
export const getHotelCounter = meter.createCounter("trpc.hotel.get")
|
||||
export const getHotelSuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.get-success"
|
||||
)
|
||||
export const getHotelFailCounter = meter.createCounter("trpc.hotel.get-fail")
|
||||
|
||||
export const getPackagesCounter = meter.createCounter("trpc.hotel.packages.get")
|
||||
export const getPackagesSuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.packages.get-success"
|
||||
)
|
||||
export const getPackagesFailCounter = meter.createCounter(
|
||||
"trpc.hotel.packages.get-fail"
|
||||
)
|
||||
|
||||
export const hotelsAvailabilityCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.hotels"
|
||||
)
|
||||
export const hotelsAvailabilitySuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.hotels-success"
|
||||
)
|
||||
export const hotelsAvailabilityFailCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.hotels-fail"
|
||||
)
|
||||
|
||||
export const roomsAvailabilityCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.rooms"
|
||||
)
|
||||
export const roomsAvailabilitySuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.rooms-success"
|
||||
)
|
||||
export const roomsAvailabilityFailCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.rooms-fail"
|
||||
)
|
||||
|
||||
export const selectedRoomAvailabilityCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.room"
|
||||
)
|
||||
export const selectedRoomAvailabilitySuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.room-success"
|
||||
)
|
||||
export const selectedRoomAvailabilityFailCounter = meter.createCounter(
|
||||
"trpc.hotel.availability.room-fail"
|
||||
)
|
||||
|
||||
export const breakfastPackagesCounter = meter.createCounter(
|
||||
"trpc.package.breakfast"
|
||||
)
|
||||
export const breakfastPackagesSuccessCounter = meter.createCounter(
|
||||
"trpc.package.breakfast-success"
|
||||
)
|
||||
export const breakfastPackagesFailCounter = meter.createCounter(
|
||||
"trpc.package.breakfast-fail"
|
||||
)
|
||||
|
||||
export const getHotelsCounter = meter.createCounter("trpc.hotel.hotels.get")
|
||||
export const getHotelsSuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.hotels.get-success"
|
||||
)
|
||||
export const getHotelsFailCounter = meter.createCounter(
|
||||
"trpc.hotel.hotels.get-fail"
|
||||
)
|
||||
|
||||
export const getHotelIdsCounter = meter.createCounter(
|
||||
"trpc.hotel.hotel-ids.get"
|
||||
)
|
||||
export const getHotelIdsSuccessCounter = meter.createCounter(
|
||||
"trpc.hotel.hotel-ids.get-success"
|
||||
)
|
||||
export const getHotelIdsFailCounter = meter.createCounter(
|
||||
"trpc.hotel.hotel-ids.get-fail"
|
||||
)
|
||||
@@ -10,11 +10,18 @@ import {
|
||||
apiLocationsSchema,
|
||||
type CitiesGroupedByCountry,
|
||||
type Countries,
|
||||
getHotelIdsByCityIdSchema,
|
||||
} from "./output"
|
||||
import {
|
||||
getHotelIdsCounter,
|
||||
getHotelIdsFailCounter,
|
||||
getHotelIdsSuccessCounter,
|
||||
} from "./telemetry"
|
||||
|
||||
import type { Country } from "@/types/enums/country"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
import { PointOfInterestGroupEnum } from "@/types/hotel"
|
||||
import { HotelLocation } from "@/types/trpc/routers/hotel/locations"
|
||||
import type { HotelLocation } from "@/types/trpc/routers/hotel/locations"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import type { Endpoint } from "@/lib/api/endpoints"
|
||||
|
||||
@@ -258,3 +265,145 @@ export async function getLocations(
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(params, citiesByCountry)
|
||||
}
|
||||
|
||||
export async function getHotelIdsByCityId(
|
||||
cityId: string,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (params: URLSearchParams) {
|
||||
getHotelIdsCounter.add(1, { cityId })
|
||||
console.info(
|
||||
"api.hotel.hotel-ids start",
|
||||
JSON.stringify({ query: { cityId } })
|
||||
)
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.hotels,
|
||||
options,
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const responseMessage = await apiResponse.text()
|
||||
getHotelIdsFailCounter.add(1, {
|
||||
cityId,
|
||||
error_type: "http_error",
|
||||
error: responseMessage,
|
||||
})
|
||||
console.error(
|
||||
"api.hotel.hotel-ids fetch error",
|
||||
JSON.stringify({
|
||||
query: { cityId },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text: responseMessage,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const validatedHotelIds = getHotelIdsByCityIdSchema.safeParse(apiJson)
|
||||
if (!validatedHotelIds.success) {
|
||||
getHotelIdsFailCounter.add(1, {
|
||||
cityId,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedHotelIds.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotel.hotel-ids validation error",
|
||||
JSON.stringify({
|
||||
query: { cityId },
|
||||
error: validatedHotelIds.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
getHotelIdsSuccessCounter.add(1, { cityId })
|
||||
console.info(
|
||||
"api.hotel.hotel-ids success",
|
||||
JSON.stringify({ query: { cityId } })
|
||||
)
|
||||
|
||||
return validatedHotelIds.data
|
||||
},
|
||||
[`hotels`, params.toString()],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(params)
|
||||
}
|
||||
|
||||
export async function getHotelIdsByCountry(
|
||||
country: Country,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (params: URLSearchParams) {
|
||||
getHotelIdsCounter.add(1, { country })
|
||||
console.info(
|
||||
"api.hotel.hotel-ids start",
|
||||
JSON.stringify({ query: { country } })
|
||||
)
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.hotels,
|
||||
options,
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const responseMessage = await apiResponse.text()
|
||||
getHotelIdsFailCounter.add(1, {
|
||||
country,
|
||||
error_type: "http_error",
|
||||
error: responseMessage,
|
||||
})
|
||||
console.error(
|
||||
"api.hotel.hotel-ids fetch error",
|
||||
JSON.stringify({
|
||||
query: { country },
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text: responseMessage,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const validatedHotelIds = getHotelIdsByCityIdSchema.safeParse(apiJson)
|
||||
if (!validatedHotelIds.success) {
|
||||
getHotelIdsFailCounter.add(1, {
|
||||
country,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedHotelIds.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotel.hotel-ids validation error",
|
||||
JSON.stringify({
|
||||
query: { country },
|
||||
error: validatedHotelIds.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
getHotelIdsSuccessCounter.add(1, { country })
|
||||
console.info(
|
||||
"api.hotel.hotel-ids success",
|
||||
JSON.stringify({ query: { country } })
|
||||
)
|
||||
|
||||
return validatedHotelIds.data
|
||||
},
|
||||
[`hotels`, params.toString()],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(params)
|
||||
}
|
||||
|
||||
8
types/components/blocks/hotelListing.ts
Normal file
8
types/components/blocks/hotelListing.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import type { HotelListing } from "@/types/trpc/routers/contentstack/blocks"
|
||||
|
||||
export interface HotelListingProps {
|
||||
heading?: string
|
||||
locationFilter: HotelListing["locationFilter"]
|
||||
hotelsToInclude: HotelListing["hotelsToInclude"]
|
||||
contentType: HotelListing["contentType"]
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
export type HotelListingItemProps = {
|
||||
imageUrl: string
|
||||
altText: string
|
||||
name: string
|
||||
address: string
|
||||
distanceToCentre: number
|
||||
description: string
|
||||
link: string
|
||||
import type { Hotel } from "@/types/hotel"
|
||||
import type { HotelListing } from "@/types/trpc/routers/contentstack/blocks"
|
||||
|
||||
export interface HotelListingItemProps {
|
||||
hotel: Hotel
|
||||
contentType: HotelListing["contentType"]
|
||||
url: string | null
|
||||
}
|
||||
|
||||
@@ -9,5 +9,6 @@ export namespace BlocksEnums {
|
||||
TextCols = "TextCols",
|
||||
TextContent = "TextContent",
|
||||
UspGrid = "UspGrid",
|
||||
HotelListing = "HotelListing",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export namespace ContentPageEnum {
|
||||
TextCols = "ContentPageBlocksTextCols",
|
||||
UspGrid = "ContentPageBlocksUspGrid",
|
||||
Table = "ContentPageBlocksTable",
|
||||
HotelListing = "ContentPageBlocksHotelListing",
|
||||
}
|
||||
|
||||
export const enum sidebar {
|
||||
|
||||
8
types/enums/country.ts
Normal file
8
types/enums/country.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export enum Country {
|
||||
Denmark = "Denmark",
|
||||
Finland = "Finland",
|
||||
Germany = "Germany",
|
||||
Norway = "Norway",
|
||||
Poland = "Poland",
|
||||
Sweden = "Sweden",
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { z } from "zod"
|
||||
import type { z } from "zod"
|
||||
|
||||
import {
|
||||
import type {
|
||||
cardsGridSchema,
|
||||
teaserCardBlockSchema,
|
||||
} from "@/server/routers/contentstack/schemas/blocks/cardsGrid"
|
||||
import { contentSchema } from "@/server/routers/contentstack/schemas/blocks/content"
|
||||
import { dynamicContentSchema } from "@/server/routers/contentstack/schemas/blocks/dynamicContent"
|
||||
import { shortcutsSchema } from "@/server/routers/contentstack/schemas/blocks/shortcuts"
|
||||
import { tableSchema } from "@/server/routers/contentstack/schemas/blocks/table"
|
||||
import { textColsSchema } from "@/server/routers/contentstack/schemas/blocks/textCols"
|
||||
import { uspGridSchema } from "@/server/routers/contentstack/schemas/blocks/uspGrid"
|
||||
import type { contentSchema } from "@/server/routers/contentstack/schemas/blocks/content"
|
||||
import type { dynamicContentSchema } from "@/server/routers/contentstack/schemas/blocks/dynamicContent"
|
||||
import type { hotelListingSchema } from "@/server/routers/contentstack/schemas/blocks/hotelListing"
|
||||
import type { shortcutsSchema } from "@/server/routers/contentstack/schemas/blocks/shortcuts"
|
||||
import type { tableSchema } from "@/server/routers/contentstack/schemas/blocks/table"
|
||||
import type { textColsSchema } from "@/server/routers/contentstack/schemas/blocks/textCols"
|
||||
import type { uspGridSchema } from "@/server/routers/contentstack/schemas/blocks/uspGrid"
|
||||
|
||||
export interface TeaserCard extends z.output<typeof teaserCardBlockSchema> {}
|
||||
export interface CardsGrid extends z.output<typeof cardsGridSchema> {}
|
||||
@@ -21,3 +22,5 @@ export interface TableBlock extends z.output<typeof tableSchema> {}
|
||||
export type TableData = TableBlock["table"]
|
||||
export interface TextCols extends z.output<typeof textColsSchema> {}
|
||||
export interface UspGrid extends z.output<typeof uspGridSchema> {}
|
||||
interface GetHotelListing extends z.output<typeof hotelListingSchema> {}
|
||||
export type HotelListing = GetHotelListing["hotel_listing"]
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { z } from "zod"
|
||||
import type { z } from "zod"
|
||||
|
||||
import {
|
||||
import type {
|
||||
contentBlock,
|
||||
hotelPageRefsSchema,
|
||||
hotelPageSchema,
|
||||
hotelPageUrlSchema,
|
||||
} from "@/server/routers/contentstack/hotelPage/output"
|
||||
import { activitiesCardSchema } from "@/server/routers/contentstack/schemas/blocks/activitiesCard"
|
||||
import type { activitiesCardSchema } from "@/server/routers/contentstack/schemas/blocks/activitiesCard"
|
||||
|
||||
export interface GetHotelPageData extends z.input<typeof hotelPageSchema> {}
|
||||
export interface HotelPage extends z.output<typeof hotelPageSchema> {}
|
||||
@@ -18,3 +19,7 @@ export interface GetHotelPageRefsSchema
|
||||
extends z.input<typeof hotelPageRefsSchema> {}
|
||||
|
||||
export interface HotelPageRefs extends z.output<typeof hotelPageRefsSchema> {}
|
||||
|
||||
export interface GetHotelPageUrlData
|
||||
extends z.input<typeof hotelPageUrlSchema> {}
|
||||
export type HotelPageUrl = z.output<typeof hotelPageUrlSchema>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { System } from "@/types/requests/system"
|
||||
import type { System } from "@/types/requests/system"
|
||||
import type { Edges } from "@/types/requests/utils/edges"
|
||||
import type { NodeRefs } from "@/types/requests/utils/refs"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
@@ -109,3 +109,14 @@ export function generateLoyaltyConfigTag(
|
||||
export function generateServiceTokenTag(scopes: string[]) {
|
||||
return `service_token:${scopes.join("-")}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to generate tags for hotel page urls
|
||||
*
|
||||
* @param lang Lang
|
||||
* @param hotelId hotelId of reference
|
||||
* @returns string
|
||||
*/
|
||||
export function generateHotelUrlTag(lang: Lang, hotelId: string) {
|
||||
return `${lang}:hotel_page_url:${hotelId}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user