Merged master into feat/1268-mask-private-data
This commit is contained in:
@@ -52,7 +52,7 @@ GOOGLE_STATIC_MAP_SIGNATURE_SECRET=""
|
|||||||
GOOGLE_STATIC_MAP_ID=""
|
GOOGLE_STATIC_MAP_ID=""
|
||||||
GOOGLE_DYNAMIC_MAP_ID=""
|
GOOGLE_DYNAMIC_MAP_ID=""
|
||||||
|
|
||||||
HIDE_FOR_NEXT_RELEASE="false"
|
NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE="false"
|
||||||
|
|
||||||
ENABLE_BOOKING_FLOW="false"
|
ENABLE_BOOKING_FLOW="false"
|
||||||
ENABLE_BOOKING_WIDGET="false"
|
ENABLE_BOOKING_WIDGET="false"
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ GOOGLE_STATIC_MAP_KEY="test"
|
|||||||
GOOGLE_STATIC_MAP_SIGNATURE_SECRET="test"
|
GOOGLE_STATIC_MAP_SIGNATURE_SECRET="test"
|
||||||
GOOGLE_STATIC_MAP_ID="test"
|
GOOGLE_STATIC_MAP_ID="test"
|
||||||
GOOGLE_DYNAMIC_MAP_ID="test"
|
GOOGLE_DYNAMIC_MAP_ID="test"
|
||||||
HIDE_FOR_NEXT_RELEASE="true"
|
NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE="true"
|
||||||
SALESFORCE_PREFERENCE_BASE_URL="test"
|
SALESFORCE_PREFERENCE_BASE_URL="test"
|
||||||
USE_NEW_REWARDS_ENDPOINT="true"
|
USE_NEW_REWARDS_ENDPOINT="true"
|
||||||
USE_NEW_REWARD_MODEL="true"
|
USE_NEW_REWARD_MODEL="true"
|
||||||
|
|||||||
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 Image from "@/components/Image"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
@@ -10,25 +10,27 @@ import Title from "@/components/TempDesignSystem/Text/Title"
|
|||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import getSingleDecimal from "@/utils/numberFormatting"
|
import getSingleDecimal from "@/utils/numberFormatting"
|
||||||
|
|
||||||
|
import { getTypeSpecificInformation } from "./utils"
|
||||||
|
|
||||||
import styles from "./hotelListingItem.module.css"
|
import styles from "./hotelListingItem.module.css"
|
||||||
|
|
||||||
import type { HotelListingItemProps } from "@/types/components/contentPage/hotelListingItem"
|
import type { HotelListingItemProps } from "@/types/components/contentPage/hotelListingItem"
|
||||||
|
|
||||||
export default async function HotelListingItem({
|
export default async function HotelListingItem({
|
||||||
imageUrl,
|
hotel,
|
||||||
altText,
|
contentType = "hotel",
|
||||||
name,
|
url,
|
||||||
address,
|
|
||||||
distanceToCentre,
|
|
||||||
description,
|
|
||||||
link,
|
|
||||||
}: HotelListingItemProps) {
|
}: HotelListingItemProps) {
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
|
const { description, imageSrc, altText } = getTypeSpecificInformation(
|
||||||
|
contentType,
|
||||||
|
hotel
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={styles.container}>
|
<article className={styles.container}>
|
||||||
<Image
|
<Image
|
||||||
src={imageUrl}
|
src={imageSrc}
|
||||||
alt={altText}
|
alt={altText}
|
||||||
width={300}
|
width={300}
|
||||||
height={200}
|
height={200}
|
||||||
@@ -36,35 +38,43 @@ export default async function HotelListingItem({
|
|||||||
/>
|
/>
|
||||||
<section className={styles.content}>
|
<section className={styles.content}>
|
||||||
<div className={styles.intro}>
|
<div className={styles.intro}>
|
||||||
<ScandicLogoIcon color="red" />
|
<HotelLogo hotelId={hotel.operaId} hotelType={hotel.hotelType} />
|
||||||
<Subtitle asChild>
|
<Subtitle asChild>
|
||||||
<Title as="h3">{name}</Title>
|
<Title as="h3">{hotel.name}</Title>
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
<div className={styles.captions}>
|
<div className={styles.captions}>
|
||||||
<Caption color="uiTextPlaceholder">{address}</Caption>
|
<Caption color="uiTextPlaceholder">
|
||||||
|
{hotel.address.streetAddress}
|
||||||
|
</Caption>
|
||||||
<div className={styles.dividerContainer}>
|
<div className={styles.dividerContainer}>
|
||||||
<Divider variant="vertical" color="beige" />
|
<Divider variant="vertical" color="beige" />
|
||||||
</div>
|
</div>
|
||||||
<Caption color="uiTextPlaceholder">
|
<Caption color="uiTextPlaceholder">
|
||||||
{intl.formatMessage(
|
{intl.formatMessage(
|
||||||
{ id: "Distance in km to city centre" },
|
{ id: "Distance in km to city centre" },
|
||||||
{ number: getSingleDecimal(distanceToCentre / 1000) }
|
{
|
||||||
|
number: getSingleDecimal(
|
||||||
|
hotel.location.distanceToCentre / 1000
|
||||||
|
),
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
</Caption>
|
</Caption>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Body>{description}</Body>
|
<Body>{description}</Body>
|
||||||
<Button
|
{url && (
|
||||||
intent="primary"
|
<Button
|
||||||
theme="base"
|
intent="primary"
|
||||||
size="small"
|
theme="base"
|
||||||
className={styles.button}
|
size="small"
|
||||||
asChild
|
className={styles.button}
|
||||||
>
|
asChild
|
||||||
<Link href={link} color="white">
|
>
|
||||||
{intl.formatMessage({ id: "See hotel details" })}
|
<Link href={url} color="white">
|
||||||
</Link>
|
{intl.formatMessage({ id: "See hotel details" })}
|
||||||
</Button>
|
</Link>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
</article>
|
</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 JsonToHtml from "@/components/JsonToHtml"
|
||||||
|
|
||||||
import AccordionSection from "./Accordion"
|
import AccordionSection from "./Accordion"
|
||||||
|
import HotelListing from "./HotelListing"
|
||||||
import Table from "./Table"
|
import Table from "./Table"
|
||||||
|
|
||||||
import type { BlocksProps } from "@/types/components/blocks"
|
import type { BlocksProps } from "@/types/components/blocks"
|
||||||
import { BlocksEnums } from "@/types/enums/blocks"
|
import { BlocksEnums } from "@/types/enums/blocks"
|
||||||
|
|
||||||
export default function Blocks({ blocks }: BlocksProps) {
|
export default function Blocks({ blocks }: BlocksProps) {
|
||||||
return blocks.map((block, idx) => {
|
return blocks.map(async (block, idx) => {
|
||||||
const firstItem = idx === 0
|
const firstItem = idx === 0
|
||||||
switch (block.typename) {
|
switch (block.typename) {
|
||||||
case BlocksEnums.block.Accordion:
|
case BlocksEnums.block.Accordion:
|
||||||
@@ -48,6 +49,21 @@ export default function Blocks({ blocks }: BlocksProps) {
|
|||||||
key={`${block.dynamic_content.title}-${idx}`}
|
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:
|
case BlocksEnums.block.Shortcuts:
|
||||||
return (
|
return (
|
||||||
<ShortcutsList
|
<ShortcutsList
|
||||||
|
|||||||
@@ -32,24 +32,28 @@ export default function PreviewImages({
|
|||||||
className={styles.image}
|
className={styles.image}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<Button
|
{images.length > 1 && (
|
||||||
theme="base"
|
<>
|
||||||
intent="inverted"
|
<Button
|
||||||
size="small"
|
theme="base"
|
||||||
onClick={() => setLightboxIsOpen(true)}
|
intent="inverted"
|
||||||
className={styles.seeAllButton}
|
size="small"
|
||||||
>
|
onClick={() => setLightboxIsOpen(true)}
|
||||||
{intl.formatMessage({ id: "See all photos" })}
|
className={styles.seeAllButton}
|
||||||
</Button>
|
>
|
||||||
<Lightbox
|
{intl.formatMessage({ id: "See all photos" })}
|
||||||
images={images}
|
</Button>
|
||||||
dialogTitle={intl.formatMessage(
|
<Lightbox
|
||||||
{ id: "Image gallery" },
|
images={images}
|
||||||
{ name: hotelName }
|
dialogTitle={intl.formatMessage(
|
||||||
)}
|
{ id: "Image gallery" },
|
||||||
isOpen={lightboxIsOpen}
|
{ name: hotelName }
|
||||||
onClose={() => setLightboxIsOpen(false)}
|
)}
|
||||||
/>
|
isOpen={lightboxIsOpen}
|
||||||
|
onClose={() => setLightboxIsOpen(false)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { useEnterDetailsStore } from "@/stores/enter-details"
|
||||||
|
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
|
||||||
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
|
import type { BedTypeInfoProps } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||||
|
|
||||||
|
export default function BedTypeInfo({ hasMultipleBedTypes }: BedTypeInfoProps) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
const hasChildWithExtraBed = useEnterDetailsStore((state) =>
|
||||||
|
state.booking.rooms[0].children?.some(
|
||||||
|
(child) => Number(child.bed) === ChildBedMapEnum.IN_EXTRA_BED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const availabilityText = intl.formatMessage({
|
||||||
|
id: "Your selected bed type will be provided based on availability",
|
||||||
|
})
|
||||||
|
|
||||||
|
const extraBedText = intl.formatMessage({
|
||||||
|
id: "Extra bed will be provided additionally",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (hasMultipleBedTypes && hasChildWithExtraBed) {
|
||||||
|
return (
|
||||||
|
<Body>
|
||||||
|
{availabilityText}. {extraBedText}
|
||||||
|
</Body>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMultipleBedTypes) {
|
||||||
|
return <Body>{availabilityText}</Body>
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChildWithExtraBed) {
|
||||||
|
return <Body>{extraBedText}</Body>
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
@@ -1,3 +1,9 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
}
|
||||||
|
|
||||||
.form {
|
.form {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--Spacing-x2);
|
gap: var(--Spacing-x2);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useEnterDetailsStore } from "@/stores/enter-details"
|
|||||||
import { KingBedIcon } from "@/components/Icons"
|
import { KingBedIcon } from "@/components/Icons"
|
||||||
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
|
import RadioCard from "@/components/TempDesignSystem/Form/ChoiceCard/Radio"
|
||||||
|
|
||||||
|
import BedTypeInfo from "./BedTypeInfo"
|
||||||
import { bedTypeFormSchema } from "./schema"
|
import { bedTypeFormSchema } from "./schema"
|
||||||
|
|
||||||
import styles from "./bedOptions.module.css"
|
import styles from "./bedOptions.module.css"
|
||||||
@@ -62,26 +63,29 @@ export default function BedType({ bedTypes }: BedTypeProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormProvider {...methods}>
|
<FormProvider {...methods}>
|
||||||
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
<div className={styles.container}>
|
||||||
{bedTypes.map((roomType) => {
|
<BedTypeInfo hasMultipleBedTypes={bedTypes.length > 1} />
|
||||||
const width =
|
<form className={styles.form} onSubmit={methods.handleSubmit(onSubmit)}>
|
||||||
roomType.size.max === roomType.size.min
|
{bedTypes.map((roomType) => {
|
||||||
? `${roomType.size.min} cm`
|
const width =
|
||||||
: `${roomType.size.min} cm - ${roomType.size.max} cm`
|
roomType.size.max === roomType.size.min
|
||||||
return (
|
? `${roomType.size.min} cm`
|
||||||
<RadioCard
|
: `${roomType.size.min} cm - ${roomType.size.max} cm`
|
||||||
key={roomType.value}
|
return (
|
||||||
Icon={KingBedIcon}
|
<RadioCard
|
||||||
iconWidth={46}
|
key={roomType.value}
|
||||||
id={roomType.value}
|
Icon={KingBedIcon}
|
||||||
name="bedType"
|
iconWidth={46}
|
||||||
subtitle={width}
|
id={roomType.value}
|
||||||
title={roomType.description}
|
name="bedType"
|
||||||
value={roomType.value}
|
subtitle={width}
|
||||||
/>
|
title={roomType.description}
|
||||||
)
|
value={roomType.value}
|
||||||
})}
|
/>
|
||||||
</form>
|
)
|
||||||
|
})}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import Modal from "../../Modal"
|
|||||||
|
|
||||||
import styles from "./ui.module.css"
|
import styles from "./ui.module.css"
|
||||||
|
|
||||||
|
import { ChildBedMapEnum } from "@/types/components/bookingWidget/enums"
|
||||||
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
import type { SummaryProps } from "@/types/components/hotelReservation/summary"
|
||||||
import type { DetailsState } from "@/types/stores/enter-details"
|
import type { DetailsState } from "@/types/stores/enter-details"
|
||||||
|
|
||||||
@@ -67,6 +68,25 @@ export default function SummaryUI({
|
|||||||
const adults = booking.rooms[0].adults
|
const adults = booking.rooms[0].adults
|
||||||
const children = booking.rooms[0].children
|
const children = booking.rooms[0].children
|
||||||
|
|
||||||
|
const childrenBeds = children?.reduce(
|
||||||
|
(acc, value) => {
|
||||||
|
const bedType = Number(value.bed)
|
||||||
|
if (bedType === ChildBedMapEnum.IN_ADULTS_BED) {
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
const count = acc.get(bedType) ?? 0
|
||||||
|
acc.set(bedType, count + 1)
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
new Map<ChildBedMapEnum, number>([
|
||||||
|
[ChildBedMapEnum.IN_CRIB, 0],
|
||||||
|
[ChildBedMapEnum.IN_EXTRA_BED, 0],
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
const childBedCrib = childrenBeds?.get(ChildBedMapEnum.IN_CRIB)
|
||||||
|
const childBedExtraBed = childrenBeds?.get(ChildBedMapEnum.IN_EXTRA_BED)
|
||||||
|
|
||||||
const memberPrice = roomRate.memberRate
|
const memberPrice = roomRate.memberRate
|
||||||
? {
|
? {
|
||||||
currency: roomRate.memberRate.localPrice.currency,
|
currency: roomRate.memberRate.localPrice.currency,
|
||||||
@@ -179,12 +199,7 @@ export default function SummaryUI({
|
|||||||
: null}
|
: null}
|
||||||
{bedType ? (
|
{bedType ? (
|
||||||
<div className={styles.entry}>
|
<div className={styles.entry}>
|
||||||
<div>
|
<Body color="uiTextHighContrast">{bedType.description}</Body>
|
||||||
<Body color="uiTextHighContrast">{bedType.description}</Body>
|
|
||||||
<Caption color="uiTextMediumContrast">
|
|
||||||
{intl.formatMessage({ id: "Based on availability" })}
|
|
||||||
</Caption>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Body color="uiTextHighContrast">
|
<Body color="uiTextHighContrast">
|
||||||
{intl.formatNumber(0, {
|
{intl.formatNumber(0, {
|
||||||
@@ -194,7 +209,39 @@ export default function SummaryUI({
|
|||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{childBedCrib ? (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{`${intl.formatMessage({ id: "Crib (child)" })} × ${childBedCrib}`}
|
||||||
|
</Body>
|
||||||
|
<Caption color="uiTextMediumContrast">
|
||||||
|
{intl.formatMessage({ id: "Based on availability" })}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{intl.formatNumber(0, {
|
||||||
|
currency: roomPrice.local.currency,
|
||||||
|
style: "currency",
|
||||||
|
})}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{childBedExtraBed ? (
|
||||||
|
<div className={styles.entry}>
|
||||||
|
<div>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{`${intl.formatMessage({ id: "Extra bed (child)" })} × ${childBedExtraBed}`}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{intl.formatNumber(0, {
|
||||||
|
currency: roomPrice.local.currency,
|
||||||
|
style: "currency",
|
||||||
|
})}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{breakfast === false ? (
|
{breakfast === false ? (
|
||||||
<div className={styles.entry}>
|
<div className={styles.entry}>
|
||||||
<Body color="uiTextHighContrast">
|
<Body color="uiTextHighContrast">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { selectRate } from "@/constants/routes/hotelReservation"
|
|||||||
import { useHotelsMapStore } from "@/stores/hotels-map"
|
import { useHotelsMapStore } from "@/stores/hotels-map"
|
||||||
|
|
||||||
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
import { mapFacilityToIcon } from "@/components/ContentType/HotelPage/data"
|
||||||
|
import HotelLogo from "@/components/Icons/Logos"
|
||||||
import ImageGallery from "@/components/ImageGallery"
|
import ImageGallery from "@/components/ImageGallery"
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
import Divider from "@/components/TempDesignSystem/Divider"
|
import Divider from "@/components/TempDesignSystem/Divider"
|
||||||
@@ -18,7 +19,6 @@ import getSingleDecimal from "@/utils/numberFormatting"
|
|||||||
|
|
||||||
import ReadMore from "../ReadMore"
|
import ReadMore from "../ReadMore"
|
||||||
import TripAdvisorChip from "../TripAdvisorChip"
|
import TripAdvisorChip from "../TripAdvisorChip"
|
||||||
import HotelLogo from "./HotelLogo"
|
|
||||||
import HotelPriceCard from "./HotelPriceCard"
|
import HotelPriceCard from "./HotelPriceCard"
|
||||||
import NoPriceAvailableCard from "./NoPriceAvailableCard"
|
import NoPriceAvailableCard from "./NoPriceAvailableCard"
|
||||||
import { hotelCardVariants } from "./variants"
|
import { hotelCardVariants } from "./variants"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
ScandicLogoIcon,
|
ScandicLogoIcon,
|
||||||
} from "@/components/Icons"
|
} 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 { HotelTypeEnum } from "@/types/enums/hotelType"
|
||||||
import { SignatureHotelEnum } from "@/types/enums/signatureHotel"
|
import { SignatureHotelEnum } from "@/types/enums/signatureHotel"
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ export const countriesMap = {
|
|||||||
"Congo, The Democratic Republic of the": "CD",
|
"Congo, The Democratic Republic of the": "CD",
|
||||||
"Cook Islands": "CK",
|
"Cook Islands": "CK",
|
||||||
"Costa Rica": "CR",
|
"Costa Rica": "CR",
|
||||||
'Cote D"Ivoire': "CI",
|
"Côte d'Ivoire": "CI",
|
||||||
Croatia: "HR",
|
Croatia: "HR",
|
||||||
Cuba: "CU",
|
Cuba: "CU",
|
||||||
Curacao: "CW",
|
Curacao: "CW",
|
||||||
@@ -109,7 +109,6 @@ export const countriesMap = {
|
|||||||
"Isle of Man": "IM",
|
"Isle of Man": "IM",
|
||||||
Israel: "IL",
|
Israel: "IL",
|
||||||
Italy: "IT",
|
Italy: "IT",
|
||||||
"Ivory Coast": "CI",
|
|
||||||
Jamaica: "JM",
|
Jamaica: "JM",
|
||||||
Japan: "JP",
|
Japan: "JP",
|
||||||
Jersey: "JE",
|
Jersey: "JE",
|
||||||
@@ -171,7 +170,7 @@ export const countriesMap = {
|
|||||||
Oman: "OM",
|
Oman: "OM",
|
||||||
Pakistan: "PK",
|
Pakistan: "PK",
|
||||||
Palau: "PW",
|
Palau: "PW",
|
||||||
"Palestinian Territory, Occupied": "PS",
|
Palestine: "PS",
|
||||||
Panama: "PA",
|
Panama: "PA",
|
||||||
"Papua New Guinea": "PG",
|
"Papua New Guinea": "PG",
|
||||||
Paraguay: "PY",
|
Paraguay: "PY",
|
||||||
@@ -215,7 +214,6 @@ export const countriesMap = {
|
|||||||
Sudan: "SD",
|
Sudan: "SD",
|
||||||
Suriname: "SR",
|
Suriname: "SR",
|
||||||
"Svalbard and Jan Mayen": "SJ",
|
"Svalbard and Jan Mayen": "SJ",
|
||||||
Swaziland: "SZ",
|
|
||||||
Sweden: "SE",
|
Sweden: "SE",
|
||||||
Switzerland: "CH",
|
Switzerland: "CH",
|
||||||
"Syrian Arab Republic": "SY",
|
"Syrian Arab Republic": "SY",
|
||||||
|
|||||||
8
env/client.ts
vendored
8
env/client.ts
vendored
@@ -5,10 +5,18 @@ export const env = createEnv({
|
|||||||
client: {
|
client: {
|
||||||
NEXT_PUBLIC_NODE_ENV: z.enum(["development", "test", "production"]),
|
NEXT_PUBLIC_NODE_ENV: z.enum(["development", "test", "production"]),
|
||||||
NEXT_PUBLIC_PORT: z.string().default("3000"),
|
NEXT_PUBLIC_PORT: z.string().default("3000"),
|
||||||
|
NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE: z
|
||||||
|
.string()
|
||||||
|
// only allow "true" or "false"
|
||||||
|
.refine((s) => s === "true" || s === "false")
|
||||||
|
// transform to boolean
|
||||||
|
.transform((s) => s === "true"),
|
||||||
},
|
},
|
||||||
emptyStringAsUndefined: true,
|
emptyStringAsUndefined: true,
|
||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,
|
NEXT_PUBLIC_NODE_ENV: process.env.NODE_ENV,
|
||||||
NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT,
|
NEXT_PUBLIC_PORT: process.env.NEXT_PUBLIC_PORT,
|
||||||
|
NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE:
|
||||||
|
process.env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
2
env/server.ts
vendored
2
env/server.ts
vendored
@@ -185,7 +185,7 @@ export const env = createEnv({
|
|||||||
process.env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET,
|
process.env.GOOGLE_STATIC_MAP_SIGNATURE_SECRET,
|
||||||
GOOGLE_STATIC_MAP_ID: process.env.GOOGLE_STATIC_MAP_ID,
|
GOOGLE_STATIC_MAP_ID: process.env.GOOGLE_STATIC_MAP_ID,
|
||||||
GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID,
|
GOOGLE_DYNAMIC_MAP_ID: process.env.GOOGLE_DYNAMIC_MAP_ID,
|
||||||
HIDE_FOR_NEXT_RELEASE: process.env.HIDE_FOR_NEXT_RELEASE,
|
HIDE_FOR_NEXT_RELEASE: process.env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE,
|
||||||
USE_NEW_REWARDS_ENDPOINT: process.env.USE_NEW_REWARDS_ENDPOINT,
|
USE_NEW_REWARDS_ENDPOINT: process.env.USE_NEW_REWARDS_ENDPOINT,
|
||||||
USE_NEW_REWARD_MODEL: process.env.USE_NEW_REWARD_MODEL,
|
USE_NEW_REWARD_MODEL: process.env.USE_NEW_REWARD_MODEL,
|
||||||
ENABLE_BOOKING_FLOW: process.env.ENABLE_BOOKING_FLOW,
|
ENABLE_BOOKING_FLOW: process.env.ENABLE_BOOKING_FLOW,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect } from "react"
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
import { env } from "@/env/client"
|
||||||
import useStickyPositionStore, {
|
import useStickyPositionStore, {
|
||||||
StickyElementNameEnum,
|
type StickyElementNameEnum,
|
||||||
} from "@/stores/sticky-position"
|
} from "@/stores/sticky-position"
|
||||||
|
|
||||||
import { debounce } from "@/utils/debounce"
|
import { debounce } from "@/utils/debounce"
|
||||||
@@ -44,6 +45,15 @@ export default function useStickyPosition({
|
|||||||
getAllElements,
|
getAllElements,
|
||||||
} = useStickyPositionStore()
|
} = useStickyPositionStore()
|
||||||
|
|
||||||
|
/* Used for Current mobile header since that doesn't use this hook.
|
||||||
|
*
|
||||||
|
* Instead, calculate if the mobile header is shown and add the height of
|
||||||
|
* that "manually" to all offsets using this hook.
|
||||||
|
*
|
||||||
|
* TODO: Remove this and just use 0 when the current header has been removed.
|
||||||
|
*/
|
||||||
|
const [baseTopOffset, setBaseTopOffset] = useState(0)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref && name) {
|
if (ref && name) {
|
||||||
// Register the sticky element with the given ref, name, and group.
|
// Register the sticky element with the given ref, name, and group.
|
||||||
@@ -79,19 +89,28 @@ export default function useStickyPosition({
|
|||||||
const topOffset = stickyElements
|
const topOffset = stickyElements
|
||||||
.slice(0, index)
|
.slice(0, index)
|
||||||
.filter((el) => el.group !== currentGroup)
|
.filter((el) => el.group !== currentGroup)
|
||||||
.reduce((acc, el) => acc + el.height, 0)
|
.reduce((acc, el) => acc + el.height, baseTopOffset)
|
||||||
|
|
||||||
// Apply the calculated top offset to the current element's style.
|
// Apply the calculated top offset to the current element's style.
|
||||||
// This positions the element at the correct location within the document.
|
// This positions the element at the correct location within the document.
|
||||||
ref.current.style.top = `${topOffset}px`
|
ref.current.style.top = `${topOffset}px`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [stickyElements, ref])
|
}, [baseTopOffset, stickyElements, ref])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resizeObserver) {
|
if (!resizeObserver) {
|
||||||
const debouncedResizeHandler = debounce(() => {
|
const debouncedResizeHandler = debounce(() => {
|
||||||
updateHeights()
|
updateHeights()
|
||||||
|
|
||||||
|
// Only do this special handling if we have the current header
|
||||||
|
if (env.NEXT_PUBLIC_HIDE_FOR_NEXT_RELEASE) {
|
||||||
|
if (document.body.clientWidth > 950) {
|
||||||
|
setBaseTopOffset(0)
|
||||||
|
} else {
|
||||||
|
setBaseTopOffset(52.41) // The height of current mobile header
|
||||||
|
}
|
||||||
|
}
|
||||||
}, 100)
|
}, 100)
|
||||||
|
|
||||||
resizeObserver = new ResizeObserver(debouncedResizeHandler)
|
resizeObserver = new ResizeObserver(debouncedResizeHandler)
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
"Creative spaces for meetings": "Kreative rum til møder",
|
"Creative spaces for meetings": "Kreative rum til møder",
|
||||||
"Credit card": "Kreditkort",
|
"Credit card": "Kreditkort",
|
||||||
"Credit card deleted successfully": "Kreditkort blev slettet",
|
"Credit card deleted successfully": "Kreditkort blev slettet",
|
||||||
|
"Crib (child)": "Kørestol (barn)",
|
||||||
"Currency Code": "DKK",
|
"Currency Code": "DKK",
|
||||||
"Current password": "Nuværende kodeord",
|
"Current password": "Nuværende kodeord",
|
||||||
"Customer service": "Kundeservice",
|
"Customer service": "Kundeservice",
|
||||||
@@ -142,6 +143,8 @@
|
|||||||
"Expires at the earliest": "Udløber tidligst {date}",
|
"Expires at the earliest": "Udløber tidligst {date}",
|
||||||
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
|
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
|
||||||
"Explore nearby": "Udforsk i nærheden",
|
"Explore nearby": "Udforsk i nærheden",
|
||||||
|
"Extra bed (child)": "Ekstra seng (barn)",
|
||||||
|
"Extra bed will be provided additionally": "Der vil blive stillet en ekstra seng til rådighed",
|
||||||
"Extras to your booking": "Tillæg til din booking",
|
"Extras to your booking": "Tillæg til din booking",
|
||||||
"FAQ": "Ofte stillede spørgsmål",
|
"FAQ": "Ofte stillede spørgsmål",
|
||||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.",
|
"Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.",
|
||||||
@@ -486,6 +489,7 @@
|
|||||||
"Your level": "Dit niveau",
|
"Your level": "Dit niveau",
|
||||||
"Your points to spend": "Dine brugbare point",
|
"Your points to spend": "Dine brugbare point",
|
||||||
"Your room": "Dit værelse",
|
"Your room": "Dit værelse",
|
||||||
|
"Your selected bed type will be provided based on availability": "Din valgte sengtype vil blive stillet til rådighed baseret på tilgængelighed",
|
||||||
"Zip code": "Postnummer",
|
"Zip code": "Postnummer",
|
||||||
"Zoo": "Zoo",
|
"Zoo": "Zoo",
|
||||||
"Zoom in": "Zoom ind",
|
"Zoom in": "Zoom ind",
|
||||||
@@ -532,7 +536,7 @@
|
|||||||
"room type": "værelsestype",
|
"room type": "værelsestype",
|
||||||
"room types": "værelsestyper",
|
"room types": "værelsestyper",
|
||||||
"signup.terms": "Ved at tilmelde dig accepterer du Scandic Friends <termsLink>vilkår og betingelser</termsLink>. Dit medlemskab er gyldigt indtil videre, og du kan til enhver tid opsige dit medlemskab ved at sende en e-mail til Scandics kundeservice",
|
"signup.terms": "Ved at tilmelde dig accepterer du Scandic Friends <termsLink>vilkår og betingelser</termsLink>. Dit medlemskab er gyldigt indtil videre, og du kan til enhver tid opsige dit medlemskab ved at sende en e-mail til Scandics kundeservice",
|
||||||
"signupPage.terms": "Ved at acceptere <termsAndConditions>vilkårene og betingelserne for Scandic Friends</termsAndConditions>, forstår jeg, at mine personlige oplysninger vil blive behandlet i overensstemmelse med<privacyPolicy>Scandics privatlivspolitik</privacyPolicy>.",
|
"signupPage.terms": "Ved at acceptere <termsAndConditions>vilkårene og betingelserne for Scandic Friends</termsAndConditions>, forstår jeg, at mine personlige oplysninger vil blive behandlet i overensstemmelse med <privacyPolicy>Scandics privatlivspolitik</privacyPolicy>.",
|
||||||
"special character": "speciel karakter",
|
"special character": "speciel karakter",
|
||||||
"spendable points expiring by": "{points} Brugbare point udløber den {date}",
|
"spendable points expiring by": "{points} Brugbare point udløber den {date}",
|
||||||
"to": "til",
|
"to": "til",
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
"Creative spaces for meetings": "Kreative Räume für Meetings",
|
"Creative spaces for meetings": "Kreative Räume für Meetings",
|
||||||
"Credit card": "Kreditkarte",
|
"Credit card": "Kreditkarte",
|
||||||
"Credit card deleted successfully": "Kreditkarte erfolgreich gelöscht",
|
"Credit card deleted successfully": "Kreditkarte erfolgreich gelöscht",
|
||||||
|
"Crib (child)": "Kinderbett (Kind)",
|
||||||
"Currency Code": "EUR",
|
"Currency Code": "EUR",
|
||||||
"Current password": "Aktuelles Passwort",
|
"Current password": "Aktuelles Passwort",
|
||||||
"Customer service": "Kundendienst",
|
"Customer service": "Kundendienst",
|
||||||
@@ -142,6 +143,8 @@
|
|||||||
"Expires at the earliest": "Läuft frühestens am {date} ab",
|
"Expires at the earliest": "Läuft frühestens am {date} ab",
|
||||||
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
|
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
|
||||||
"Explore nearby": "Erkunden Sie die Umgebung",
|
"Explore nearby": "Erkunden Sie die Umgebung",
|
||||||
|
"Extra bed (child)": "Ekstra seng (Kind)",
|
||||||
|
"Extra bed will be provided additionally": "Ein zusätzliches Bett wird bereitgestellt",
|
||||||
"Extras to your booking": "Extras zu Ihrer Buchung",
|
"Extras to your booking": "Extras zu Ihrer Buchung",
|
||||||
"FAQ": "Häufig gestellte Fragen",
|
"FAQ": "Häufig gestellte Fragen",
|
||||||
"Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.",
|
"Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.",
|
||||||
@@ -485,6 +488,7 @@
|
|||||||
"Your level": "Dein level",
|
"Your level": "Dein level",
|
||||||
"Your points to spend": "Meine Punkte",
|
"Your points to spend": "Meine Punkte",
|
||||||
"Your room": "Ihr Zimmer",
|
"Your room": "Ihr Zimmer",
|
||||||
|
"Your selected bed type will be provided based on availability": "Ihre ausgewählte Bettart wird basierend auf der Verfügbarkeit bereitgestellt",
|
||||||
"Zip code": "PLZ",
|
"Zip code": "PLZ",
|
||||||
"Zoo": "Zoo",
|
"Zoo": "Zoo",
|
||||||
"Zoom in": "Vergrößern",
|
"Zoom in": "Vergrößern",
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
"Creative spaces for meetings": "Creative spaces for meetings",
|
"Creative spaces for meetings": "Creative spaces for meetings",
|
||||||
"Credit card": "Credit card",
|
"Credit card": "Credit card",
|
||||||
"Credit card deleted successfully": "Credit card deleted successfully",
|
"Credit card deleted successfully": "Credit card deleted successfully",
|
||||||
|
"Crib (child)": "Crib (child)",
|
||||||
"Currency Code": "EUR",
|
"Currency Code": "EUR",
|
||||||
"Current password": "Current password",
|
"Current password": "Current password",
|
||||||
"Customer service": "Customer service",
|
"Customer service": "Customer service",
|
||||||
@@ -150,6 +151,8 @@
|
|||||||
"Expires at the earliest": "Expires at the earliest {date}",
|
"Expires at the earliest": "Expires at the earliest {date}",
|
||||||
"Explore all levels and benefits": "Explore all levels and benefits",
|
"Explore all levels and benefits": "Explore all levels and benefits",
|
||||||
"Explore nearby": "Explore nearby",
|
"Explore nearby": "Explore nearby",
|
||||||
|
"Extra bed (child)": "Extra bed (child)",
|
||||||
|
"Extra bed will be provided additionally": "Extra bed will be provided additionally",
|
||||||
"Extras to your booking": "Extras to your booking",
|
"Extras to your booking": "Extras to your booking",
|
||||||
"FAQ": "FAQ",
|
"FAQ": "FAQ",
|
||||||
"Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.",
|
"Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.",
|
||||||
@@ -529,6 +532,7 @@
|
|||||||
"Your level": "Your level",
|
"Your level": "Your level",
|
||||||
"Your points to spend": "Your points to spend",
|
"Your points to spend": "Your points to spend",
|
||||||
"Your room": "Your room",
|
"Your room": "Your room",
|
||||||
|
"Your selected bed type will be provided based on availability": "Your selected bed type will be provided based on availability",
|
||||||
"Zip code": "Zip code",
|
"Zip code": "Zip code",
|
||||||
"Zoo": "Zoo",
|
"Zoo": "Zoo",
|
||||||
"Zoom in": "Zoom in",
|
"Zoom in": "Zoom in",
|
||||||
|
|||||||
@@ -105,6 +105,7 @@
|
|||||||
"Creative spaces for meetings": "Luovia tiloja kokouksille",
|
"Creative spaces for meetings": "Luovia tiloja kokouksille",
|
||||||
"Credit card": "Luottokortti",
|
"Credit card": "Luottokortti",
|
||||||
"Credit card deleted successfully": "Luottokortti poistettu onnistuneesti",
|
"Credit card deleted successfully": "Luottokortti poistettu onnistuneesti",
|
||||||
|
"Crib (child)": "Körkkä (lasta)",
|
||||||
"Currency Code": "EUR",
|
"Currency Code": "EUR",
|
||||||
"Current password": "Nykyinen salasana",
|
"Current password": "Nykyinen salasana",
|
||||||
"Customer service": "Asiakaspalvelu",
|
"Customer service": "Asiakaspalvelu",
|
||||||
@@ -142,6 +143,8 @@
|
|||||||
"Expires at the earliest": "Päättyy aikaisintaan {date}",
|
"Expires at the earliest": "Päättyy aikaisintaan {date}",
|
||||||
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
||||||
"Explore nearby": "Tutustu lähialueeseen",
|
"Explore nearby": "Tutustu lähialueeseen",
|
||||||
|
"Extra bed (child)": "Lisävuode (lasta)",
|
||||||
|
"Extra bed will be provided additionally": "Lisävuode toimitetaan erikseen",
|
||||||
"Extras to your booking": "Varauksessa lisäpalveluita",
|
"Extras to your booking": "Varauksessa lisäpalveluita",
|
||||||
"FAQ": "Usein kysytyt kysymykset",
|
"FAQ": "Usein kysytyt kysymykset",
|
||||||
"Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.",
|
"Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.",
|
||||||
@@ -412,7 +415,7 @@
|
|||||||
"Surprise!": "Yllätys!",
|
"Surprise!": "Yllätys!",
|
||||||
"TUI Points": "TUI Points",
|
"TUI Points": "TUI Points",
|
||||||
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.",
|
"Tell us what information and updates you'd like to receive, and how, by clicking the link below.": "Kerro meille, mitä tietoja ja päivityksiä haluat saada ja miten, napsauttamalla alla olevaa linkkiä.",
|
||||||
"Terms and conditions": "Käyttöehdot",
|
"Terms and conditions": "Säännöt ja ehdot",
|
||||||
"Thank you": "Kiitos",
|
"Thank you": "Kiitos",
|
||||||
"The new price is": "Uusi hinta on",
|
"The new price is": "Uusi hinta on",
|
||||||
"The price has increased": "Hinta on noussut",
|
"The price has increased": "Hinta on noussut",
|
||||||
@@ -484,6 +487,7 @@
|
|||||||
"Your level": "Tasosi",
|
"Your level": "Tasosi",
|
||||||
"Your points to spend": "Käytettävissä olevat pisteesi",
|
"Your points to spend": "Käytettävissä olevat pisteesi",
|
||||||
"Your room": "Sinun huoneesi",
|
"Your room": "Sinun huoneesi",
|
||||||
|
"Your selected bed type will be provided based on availability": "Valitun vuodetyypin toimitetaan saatavuuden mukaan",
|
||||||
"Zip code": "Postinumero",
|
"Zip code": "Postinumero",
|
||||||
"Zoo": "Eläintarha",
|
"Zoo": "Eläintarha",
|
||||||
"Zoom in": "Lähennä",
|
"Zoom in": "Lähennä",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"Country is required": "Land kreves",
|
"Country is required": "Land kreves",
|
||||||
"Creative spaces for meetings": "Kreative rom for møter",
|
"Creative spaces for meetings": "Kreative rom for møter",
|
||||||
"Credit card deleted successfully": "Kredittkort slettet",
|
"Credit card deleted successfully": "Kredittkort slettet",
|
||||||
|
"Crib (child)": "Kørestol (barn)",
|
||||||
"Currency Code": "NOK",
|
"Currency Code": "NOK",
|
||||||
"Current password": "Nåværende passord",
|
"Current password": "Nåværende passord",
|
||||||
"Customer service": "Kundeservice",
|
"Customer service": "Kundeservice",
|
||||||
@@ -141,6 +142,8 @@
|
|||||||
"Expires at the earliest": "Utløper tidligst {date}",
|
"Expires at the earliest": "Utløper tidligst {date}",
|
||||||
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
|
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
|
||||||
"Explore nearby": "Utforsk i nærheten",
|
"Explore nearby": "Utforsk i nærheten",
|
||||||
|
"Extra bed (child)": "Ekstra seng (barn)",
|
||||||
|
"Extra bed will be provided additionally": "Ekstra seng vil bli tilgjengelig",
|
||||||
"Extras to your booking": "Tilvalg til bestillingen din",
|
"Extras to your booking": "Tilvalg til bestillingen din",
|
||||||
"FAQ": "Ofte stilte spørsmål",
|
"FAQ": "Ofte stilte spørsmål",
|
||||||
"Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.",
|
"Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.",
|
||||||
@@ -484,6 +487,7 @@
|
|||||||
"Your level": "Ditt nivå",
|
"Your level": "Ditt nivå",
|
||||||
"Your points to spend": "Dine brukbare poeng",
|
"Your points to spend": "Dine brukbare poeng",
|
||||||
"Your room": "Rommet ditt",
|
"Your room": "Rommet ditt",
|
||||||
|
"Your selected bed type will be provided based on availability": "Din valgte sengtype vil blive stillet til rådighed baseret på tilgængelighed",
|
||||||
"Zip code": "Post kode",
|
"Zip code": "Post kode",
|
||||||
"Zoo": "Dyrehage",
|
"Zoo": "Dyrehage",
|
||||||
"Zoom in": "Zoom inn",
|
"Zoom in": "Zoom inn",
|
||||||
|
|||||||
@@ -104,6 +104,7 @@
|
|||||||
"Country is required": "Land är obligatoriskt",
|
"Country is required": "Land är obligatoriskt",
|
||||||
"Creative spaces for meetings": "Kreativa utrymmen för möten",
|
"Creative spaces for meetings": "Kreativa utrymmen för möten",
|
||||||
"Credit card deleted successfully": "Kreditkort har tagits bort",
|
"Credit card deleted successfully": "Kreditkort har tagits bort",
|
||||||
|
"Crib (child)": "Spjälsäng (barn)",
|
||||||
"Currency Code": "SEK",
|
"Currency Code": "SEK",
|
||||||
"Current password": "Nuvarande lösenord",
|
"Current password": "Nuvarande lösenord",
|
||||||
"Customer service": "Kundservice",
|
"Customer service": "Kundservice",
|
||||||
@@ -141,6 +142,8 @@
|
|||||||
"Expires at the earliest": "Löper ut tidigast {date}",
|
"Expires at the earliest": "Löper ut tidigast {date}",
|
||||||
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
|
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
|
||||||
"Explore nearby": "Utforska i närheten",
|
"Explore nearby": "Utforska i närheten",
|
||||||
|
"Extra bed (child)": "Extra säng (barn)",
|
||||||
|
"Extra bed will be provided additionally": "Extra säng kommer att tillhandahållas",
|
||||||
"Extras to your booking": "Extra tillval till din bokning",
|
"Extras to your booking": "Extra tillval till din bokning",
|
||||||
"FAQ": "FAQ",
|
"FAQ": "FAQ",
|
||||||
"Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.",
|
"Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.",
|
||||||
@@ -484,6 +487,7 @@
|
|||||||
"Your level": "Din nivå",
|
"Your level": "Din nivå",
|
||||||
"Your points to spend": "Dina spenderbara poäng",
|
"Your points to spend": "Dina spenderbara poäng",
|
||||||
"Your room": "Ditt rum",
|
"Your room": "Ditt rum",
|
||||||
|
"Your selected bed type will be provided based on availability": "Din valda sängtyp kommer att tillhandahållas baserat på tillgänglighet",
|
||||||
"Zip code": "Postnummer",
|
"Zip code": "Postnummer",
|
||||||
"Zoo": "Djurpark",
|
"Zoo": "Djurpark",
|
||||||
"Zoom in": "Zooma in",
|
"Zoom in": "Zooma in",
|
||||||
|
|||||||
@@ -30,20 +30,11 @@ const wrappedFetch = fetchRetry(fetch, {
|
|||||||
export async function get(
|
export async function get(
|
||||||
endpoint: Endpoint,
|
endpoint: Endpoint,
|
||||||
options: RequestOptionsWithOutBody,
|
options: RequestOptionsWithOutBody,
|
||||||
params: Record<string, any> = {}
|
params = {}
|
||||||
) {
|
) {
|
||||||
const url = new URL(env.API_BASEURL)
|
const url = new URL(env.API_BASEURL)
|
||||||
url.pathname = endpoint
|
url.pathname = endpoint
|
||||||
const searchParams = new URLSearchParams()
|
url.search = new URLSearchParams(params).toString()
|
||||||
Object.entries(params).forEach(([key, value]) => {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value.forEach((val) => searchParams.append(key, val))
|
|
||||||
} else {
|
|
||||||
searchParams.set(key, value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
url.search = searchParams.toString()
|
|
||||||
return wrappedFetch(
|
return wrappedFetch(
|
||||||
url,
|
url,
|
||||||
merge.all([defaultOptions, { method: "GET" }, options])
|
merge.all([defaultOptions, { method: "GET" }, options])
|
||||||
|
|||||||
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/CardsGrid.graphql"
|
||||||
#import "../../Fragments/Blocks/Content.graphql"
|
#import "../../Fragments/Blocks/Content.graphql"
|
||||||
#import "../../Fragments/Blocks/DynamicContent.graphql"
|
#import "../../Fragments/Blocks/DynamicContent.graphql"
|
||||||
|
#import "../../Fragments/Blocks/HotelListing.graphql"
|
||||||
#import "../../Fragments/Blocks/Shortcuts.graphql"
|
#import "../../Fragments/Blocks/Shortcuts.graphql"
|
||||||
#import "../../Fragments/Blocks/Table.graphql"
|
#import "../../Fragments/Blocks/Table.graphql"
|
||||||
#import "../../Fragments/Blocks/TextCols.graphql"
|
#import "../../Fragments/Blocks/TextCols.graphql"
|
||||||
@@ -65,6 +66,7 @@ query GetContentPageBlocksBatch2($locale: String!, $uid: String!) {
|
|||||||
content_page(uid: $uid, locale: $locale) {
|
content_page(uid: $uid, locale: $locale) {
|
||||||
blocks {
|
blocks {
|
||||||
__typename
|
__typename
|
||||||
|
...HotelListing_ContentPage
|
||||||
...Shortcuts_ContentPage
|
...Shortcuts_ContentPage
|
||||||
...Table_ContentPage
|
...Table_ContentPage
|
||||||
...TextCols_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"
|
} from "@/types/requests/packages"
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
import type {
|
import type {
|
||||||
|
GetHotelsInput,
|
||||||
GetRoomsAvailabilityInput,
|
GetRoomsAvailabilityInput,
|
||||||
GetSelectedRoomAvailabilityInput,
|
GetSelectedRoomAvailabilityInput,
|
||||||
HotelDataInput,
|
HotelDataInput,
|
||||||
@@ -62,6 +63,12 @@ export const getUserTracking = cache(async function getMemoizedUserTracking() {
|
|||||||
return serverClient().user.tracking()
|
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(
|
export const getHotelData = cache(async function getMemoizedHotelData(
|
||||||
input: HotelDataInput
|
input: HotelDataInput
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
dynamicContentRefsSchema,
|
dynamicContentRefsSchema,
|
||||||
dynamicContentSchema as blockDynamicContentSchema,
|
dynamicContentSchema as blockDynamicContentSchema,
|
||||||
} from "../schemas/blocks/dynamicContent"
|
} from "../schemas/blocks/dynamicContent"
|
||||||
|
import { hotelListingSchema } from "../schemas/blocks/hotelListing"
|
||||||
import {
|
import {
|
||||||
shortcutsRefsSchema,
|
shortcutsRefsSchema,
|
||||||
shortcutsSchema,
|
shortcutsSchema,
|
||||||
@@ -103,6 +104,12 @@ export const contentPageAccordion = z
|
|||||||
})
|
})
|
||||||
.merge(accordionSchema)
|
.merge(accordionSchema)
|
||||||
|
|
||||||
|
export const contentPageHotelListing = z
|
||||||
|
.object({
|
||||||
|
__typename: z.literal(ContentPageEnum.ContentStack.blocks.HotelListing),
|
||||||
|
})
|
||||||
|
.merge(hotelListingSchema)
|
||||||
|
|
||||||
export const blocksSchema = z.discriminatedUnion("__typename", [
|
export const blocksSchema = z.discriminatedUnion("__typename", [
|
||||||
contentPageAccordion,
|
contentPageAccordion,
|
||||||
contentPageCards,
|
contentPageCards,
|
||||||
@@ -112,6 +119,7 @@ export const blocksSchema = z.discriminatedUnion("__typename", [
|
|||||||
contentPageTable,
|
contentPageTable,
|
||||||
contentPageTextCols,
|
contentPageTextCols,
|
||||||
contentPageUspGrid,
|
contentPageUspGrid,
|
||||||
|
contentPageHotelListing,
|
||||||
])
|
])
|
||||||
|
|
||||||
export const contentPageSidebarContent = z
|
export const contentPageSidebarContent = z
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import { discriminatedUnionArray } from "@/lib/discriminatedUnion"
|
import { discriminatedUnionArray } from "@/lib/discriminatedUnion"
|
||||||
|
|
||||||
|
import { removeMultipleSlashes } from "@/utils/url"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
activitiesCardRefSchema,
|
activitiesCardRefSchema,
|
||||||
activitiesCardSchema,
|
activitiesCardSchema,
|
||||||
@@ -58,3 +60,26 @@ export const hotelPageRefsSchema = z.object({
|
|||||||
url: z.string(),
|
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 { GetHotelPage } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
import { request } from "@/lib/graphql/request"
|
||||||
import { notFound } from "@/server/errors/trpc"
|
import { notFound } from "@/server/errors/trpc"
|
||||||
@@ -8,20 +6,13 @@ import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
|||||||
import { generateTag } from "@/utils/generateTag"
|
import { generateTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
import { hotelPageSchema } from "./output"
|
import { hotelPageSchema } from "./output"
|
||||||
|
import {
|
||||||
|
getHotelPageCounter,
|
||||||
|
getHotelPageFailCounter,
|
||||||
|
getHotelPageSuccessCounter,
|
||||||
|
} from "./telemetry"
|
||||||
|
|
||||||
import { GetHotelPageData } from "@/types/trpc/routers/contentstack/hotelPage"
|
import type { 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"
|
|
||||||
)
|
|
||||||
|
|
||||||
export const hotelPageQueryRouter = router({
|
export const hotelPageQueryRouter = router({
|
||||||
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
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 { GetHotelPageRefs } from "@/lib/graphql/Query/HotelPage/HotelPage.graphql"
|
||||||
|
import { GetHotelPageUrl } from "@/lib/graphql/Query/HotelPage/HotelPageUrl.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
import { request } from "@/lib/graphql/request"
|
||||||
import { notFound } from "@/server/errors/trpc"
|
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 { HotelPageEnum } from "@/types/enums/hotelPage"
|
||||||
import { System } from "@/types/requests/system"
|
import type { System } from "@/types/requests/system"
|
||||||
import {
|
import type {
|
||||||
GetHotelPageRefsSchema,
|
GetHotelPageRefsSchema,
|
||||||
|
GetHotelPageUrlData,
|
||||||
HotelPageRefs,
|
HotelPageRefs,
|
||||||
} from "@/types/trpc/routers/contentstack/hotelPage"
|
} from "@/types/trpc/routers/contentstack/hotelPage"
|
||||||
|
import type { Lang } from "@/constants/languages"
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
export async function fetchHotelPageRefs(lang: Lang, uid: string) {
|
export async function fetchHotelPageRefs(lang: Lang, uid: string) {
|
||||||
getHotelPageRefsCounter.add(1, { lang, uid })
|
getHotelPageRefsCounter.add(1, { lang, uid })
|
||||||
@@ -140,3 +135,64 @@ export function getConnections({ hotel_page }: HotelPageRefs) {
|
|||||||
}
|
}
|
||||||
return connections
|
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 { z } from "zod"
|
||||||
|
|
||||||
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
import { RoomPackageCodeEnum } from "@/types/components/hotelReservation/selectRate/roomFilter"
|
||||||
|
import { Country } from "@/types/enums/country"
|
||||||
|
|
||||||
export const getHotelsAvailabilityInputSchema = z.object({
|
export const getHotelsAvailabilityInputSchema = z.object({
|
||||||
cityId: z.string(),
|
cityId: z.string(),
|
||||||
@@ -53,6 +54,18 @@ export const getHotelDataInputSchema = z.object({
|
|||||||
|
|
||||||
export type HotelDataInput = z.input<typeof getHotelDataInputSchema>
|
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({
|
export const getBreakfastPackageInputSchema = z.object({
|
||||||
adults: z.number().min(1, { message: "at least one adult is required" }),
|
adults: z.number().min(1, { message: "at least one adult is required" }),
|
||||||
fromDate: z
|
fromDate: z
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const hotelContentSchema = z.object({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const detailedFacilitySchema = z.object({
|
const detailedFacilitySchema = z.object({
|
||||||
id: z.nativeEnum(FacilityEnum),
|
id: z.number(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
public: z.boolean(),
|
public: z.boolean(),
|
||||||
sortOrder: z.number(),
|
sortOrder: z.number(),
|
||||||
@@ -389,6 +389,9 @@ const hotelFactsSchema = z.object({
|
|||||||
yearBuilt: z.string(),
|
yearBuilt: z.string(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
type DetailedFacility = { id: FacilityEnum } & z.infer<
|
||||||
|
typeof detailedFacilitySchema
|
||||||
|
>
|
||||||
export const hotelAttributesSchema = z.object({
|
export const hotelAttributesSchema = z.object({
|
||||||
accessibilityElevatorPitchText: z.string().optional(),
|
accessibilityElevatorPitchText: z.string().optional(),
|
||||||
address: addressSchema,
|
address: addressSchema,
|
||||||
@@ -396,11 +399,15 @@ export const hotelAttributesSchema = z.object({
|
|||||||
cityName: z.string(),
|
cityName: z.string(),
|
||||||
conferencesAndMeetings: facilitySchema.optional(),
|
conferencesAndMeetings: facilitySchema.optional(),
|
||||||
contactInformation: contactInformationSchema,
|
contactInformation: contactInformationSchema,
|
||||||
detailedFacilities: z
|
detailedFacilities: z.array(detailedFacilitySchema).transform(
|
||||||
.array(detailedFacilitySchema)
|
(facilities) =>
|
||||||
.transform((facilities) =>
|
facilities
|
||||||
facilities.sort((a, b) => b.sortOrder - a.sortOrder)
|
// Filter away facilities with ID:s that we don't recognize
|
||||||
),
|
.filter(
|
||||||
|
(f) => f.id !== undefined && f.id !== null && f.id in FacilityEnum
|
||||||
|
)
|
||||||
|
.sort((a, b) => b.sortOrder - a.sortOrder) as DetailedFacility[]
|
||||||
|
),
|
||||||
gallery: gallerySchema.optional(),
|
gallery: gallerySchema.optional(),
|
||||||
galleryImages: z.array(imageSchema).optional(),
|
galleryImages: z.array(imageSchema).optional(),
|
||||||
healthAndWellness: facilitySchema.optional(),
|
healthAndWellness: facilitySchema.optional(),
|
||||||
@@ -870,3 +877,14 @@ export const getRoomPackagesSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
.transform((data) => data.data?.attributes?.packages ?? [])
|
.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,9 @@
|
|||||||
import { metrics } from "@opentelemetry/api"
|
import { ApiLang } 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 { badRequestError } from "@/server/errors/trpc"
|
import { badRequestError } from "@/server/errors/trpc"
|
||||||
import {
|
import {
|
||||||
|
contentStackBaseWithServiceProcedure,
|
||||||
publicProcedure,
|
publicProcedure,
|
||||||
router,
|
router,
|
||||||
safeProtectedServiceProcedure,
|
safeProtectedServiceProcedure,
|
||||||
@@ -13,12 +13,14 @@ import { toApiLang } from "@/server/utils"
|
|||||||
|
|
||||||
import { cache } from "@/utils/cache"
|
import { cache } from "@/utils/cache"
|
||||||
|
|
||||||
|
import { getHotelPageUrl } from "../contentstack/hotelPage/utils"
|
||||||
import { getVerifiedUser, parsedUser } from "../user/query"
|
import { getVerifiedUser, parsedUser } from "../user/query"
|
||||||
import {
|
import {
|
||||||
getBreakfastPackageInputSchema,
|
getBreakfastPackageInputSchema,
|
||||||
getCityCoordinatesInputSchema,
|
getCityCoordinatesInputSchema,
|
||||||
getHotelDataInputSchema,
|
getHotelDataInputSchema,
|
||||||
getHotelsAvailabilityInputSchema,
|
getHotelsAvailabilityInputSchema,
|
||||||
|
getHotelsInput,
|
||||||
getRatesInputSchema,
|
getRatesInputSchema,
|
||||||
getRoomPackagesInputSchema,
|
getRoomPackagesInputSchema,
|
||||||
getRoomsAvailabilityInputSchema,
|
getRoomsAvailabilityInputSchema,
|
||||||
@@ -33,10 +35,35 @@ import {
|
|||||||
getRoomPackagesSchema,
|
getRoomPackagesSchema,
|
||||||
getRoomsAvailabilitySchema,
|
getRoomsAvailabilitySchema,
|
||||||
} from "./output"
|
} 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 tempRatesData from "./tempRatesData.json"
|
||||||
import {
|
import {
|
||||||
getCitiesByCountry,
|
getCitiesByCountry,
|
||||||
getCountries,
|
getCountries,
|
||||||
|
getHotelIdsByCityId,
|
||||||
|
getHotelIdsByCountry,
|
||||||
getLocations,
|
getLocations,
|
||||||
TWENTYFOUR_HOURS,
|
TWENTYFOUR_HOURS,
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
@@ -45,68 +72,21 @@ import type { BedTypeSelection } from "@/types/components/hotelReservation/enter
|
|||||||
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 { Hotel } from "@/types/hotel"
|
||||||
const meter = metrics.getMeter("trpc.hotels")
|
import type { HotelPageUrl } from "@/types/trpc/routers/contentstack/hotelPage"
|
||||||
const getHotelCounter = meter.createCounter("trpc.hotel.get")
|
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
const params: Record<string, string | string[]> = {
|
const includes = ["RoomCategories", "Restaurants"] // "RoomCategories","NearbyHotels","Restaurants","City",
|
||||||
|
const params = new URLSearchParams({
|
||||||
hotelId,
|
hotelId,
|
||||||
language,
|
language,
|
||||||
}
|
})
|
||||||
|
|
||||||
params.include = ["RoomCategories", "Restaurants"] // "RoomCategories","NearbyHotels","Restaurants","City",
|
includes.forEach((include) => params.append("include", include))
|
||||||
|
|
||||||
getHotelCounter.add(1, {
|
getHotelCounter.add(1, {
|
||||||
hotelId,
|
hotelId,
|
||||||
@@ -695,6 +675,202 @@ export const hotelQueryRouter = router({
|
|||||||
return getHotelData(input, ctx.serviceToken)
|
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 apiLang = toApiLang(language)
|
||||||
|
const options: RequestOptionsWithOutBody = {
|
||||||
|
// needs to clear default option as only
|
||||||
|
// cache or next.revalidate is permitted
|
||||||
|
cache: undefined,
|
||||||
|
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: apiLang,
|
||||||
|
})
|
||||||
|
const locations = await getLocations(
|
||||||
|
language,
|
||||||
|
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: apiLang,
|
||||||
|
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: ApiLang.En,
|
||||||
|
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({
|
locations: router({
|
||||||
get: serviceProcedure.query(async function ({ ctx }) {
|
get: serviceProcedure.query(async function ({ ctx }) {
|
||||||
const searchParams = new URLSearchParams()
|
const searchParams = new URLSearchParams()
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { dt } from "@/lib/dt"
|
|
||||||
import { AlertTypeEnum } from "@/types/enums/alert"
|
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { dt } from "@/lib/dt"
|
||||||
|
|
||||||
|
import { AlertTypeEnum } from "@/types/enums/alert"
|
||||||
|
|
||||||
const specialAlertSchema = z.object({
|
const specialAlertSchema = z.object({
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
@@ -16,10 +18,14 @@ export const specialAlertsSchema = z
|
|||||||
.transform((data) => {
|
.transform((data) => {
|
||||||
const now = dt().utc().format("YYYY-MM-DD")
|
const now = dt().utc().format("YYYY-MM-DD")
|
||||||
const filteredAlerts = data.filter((alert) => {
|
const filteredAlerts = data.filter((alert) => {
|
||||||
const shouldShowNow =
|
let shouldShowNow = true
|
||||||
alert.startDate && alert.endDate
|
|
||||||
? alert.startDate <= now && alert.endDate >= now
|
if (alert.startDate && alert.startDate > now) {
|
||||||
: true
|
shouldShowNow = false
|
||||||
|
}
|
||||||
|
if (alert.endDate && alert.endDate < now) {
|
||||||
|
shouldShowNow = false
|
||||||
|
}
|
||||||
const hasText = alert.description || alert.title
|
const hasText = alert.description || alert.title
|
||||||
return shouldShowNow && hasText
|
return shouldShowNow && hasText
|
||||||
})
|
})
|
||||||
|
|||||||
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,
|
apiLocationsSchema,
|
||||||
type CitiesGroupedByCountry,
|
type CitiesGroupedByCountry,
|
||||||
type Countries,
|
type Countries,
|
||||||
|
getHotelIdsByCityIdSchema,
|
||||||
} from "./output"
|
} from "./output"
|
||||||
|
import {
|
||||||
|
getHotelIdsCounter,
|
||||||
|
getHotelIdsFailCounter,
|
||||||
|
getHotelIdsSuccessCounter,
|
||||||
|
} from "./telemetry"
|
||||||
|
|
||||||
|
import type { Country } from "@/types/enums/country"
|
||||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||||
import { PointOfInterestGroupEnum } from "@/types/hotel"
|
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 { Lang } from "@/constants/languages"
|
||||||
import type { Endpoint } from "@/lib/api/endpoints"
|
import type { Endpoint } from "@/lib/api/endpoints"
|
||||||
|
|
||||||
@@ -258,3 +265,145 @@ export async function getLocations(
|
|||||||
{ revalidate: TWENTYFOUR_HOURS }
|
{ revalidate: TWENTYFOUR_HOURS }
|
||||||
)(params, citiesByCountry)
|
)(params, citiesByCountry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getHotelIdsByCityId(
|
||||||
|
cityId: string,
|
||||||
|
options: RequestOptionsWithOutBody,
|
||||||
|
params: URLSearchParams
|
||||||
|
) {
|
||||||
|
return unstable_cache(
|
||||||
|
async function (params: URLSearchParams) {
|
||||||
|
getHotelIdsCounter.add(1, { params: params.toString() })
|
||||||
|
console.info(
|
||||||
|
"api.hotel.hotel-ids start",
|
||||||
|
JSON.stringify({ params: params.toString() })
|
||||||
|
)
|
||||||
|
const apiResponse = await api.get(
|
||||||
|
api.endpoints.v1.Hotel.hotels,
|
||||||
|
options,
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!apiResponse.ok) {
|
||||||
|
const responseMessage = await apiResponse.text()
|
||||||
|
getHotelIdsFailCounter.add(1, {
|
||||||
|
params: params.toString(),
|
||||||
|
error_type: "http_error",
|
||||||
|
error: responseMessage,
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotel.hotel-ids fetch error",
|
||||||
|
JSON.stringify({
|
||||||
|
params: params.toString(),
|
||||||
|
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, {
|
||||||
|
params: params.toString(),
|
||||||
|
error_type: "validation_error",
|
||||||
|
error: JSON.stringify(validatedHotelIds.error),
|
||||||
|
})
|
||||||
|
console.error(
|
||||||
|
"api.hotel.hotel-ids validation error",
|
||||||
|
JSON.stringify({
|
||||||
|
params: params.toString(),
|
||||||
|
error: validatedHotelIds.error,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
getHotelIdsSuccessCounter.add(1, { cityId })
|
||||||
|
console.info(
|
||||||
|
"api.hotel.hotel-ids success",
|
||||||
|
JSON.stringify({ params: params.toString() })
|
||||||
|
)
|
||||||
|
|
||||||
|
return validatedHotelIds.data
|
||||||
|
},
|
||||||
|
[`hotelsByCityId`, 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
|
||||||
|
},
|
||||||
|
[`hotelsByCountry`, params.toString()],
|
||||||
|
{ revalidate: TWENTYFOUR_HOURS }
|
||||||
|
)(params)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { metrics } from "@opentelemetry/api"
|
import { metrics } from "@opentelemetry/api"
|
||||||
import { cache } from "react"
|
|
||||||
|
|
||||||
import * as api from "@/lib/api"
|
import * as api from "@/lib/api"
|
||||||
import { dt } from "@/lib/dt"
|
import { dt } from "@/lib/dt"
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
} from "@/server/trpc"
|
} from "@/server/trpc"
|
||||||
|
|
||||||
import { countries } from "@/components/TempDesignSystem/Form/Country/countries"
|
import { countries } from "@/components/TempDesignSystem/Form/Country/countries"
|
||||||
|
import { cache } from "@/utils/cache"
|
||||||
import * as maskValue from "@/utils/maskValue"
|
import * as maskValue from "@/utils/maskValue"
|
||||||
import { getMembership, getMembershipCards } from "@/utils/user"
|
import { getMembership, getMembershipCards } from "@/utils/user"
|
||||||
|
|
||||||
|
|||||||
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 = {
|
import type { Hotel } from "@/types/hotel"
|
||||||
imageUrl: string
|
import type { HotelListing } from "@/types/trpc/routers/contentstack/blocks"
|
||||||
altText: string
|
|
||||||
name: string
|
export interface HotelListingItemProps {
|
||||||
address: string
|
hotel: Hotel
|
||||||
distanceToCentre: number
|
contentType: HotelListing["contentType"]
|
||||||
description: string
|
url: string | null
|
||||||
link: string
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod"
|
import type { z } from "zod"
|
||||||
|
|
||||||
import {
|
import type {
|
||||||
bedTypeFormSchema,
|
bedTypeFormSchema,
|
||||||
bedTypeSchema,
|
bedTypeSchema,
|
||||||
} from "@/components/HotelReservation/EnterDetails/BedType/schema"
|
} from "@/components/HotelReservation/EnterDetails/BedType/schema"
|
||||||
@@ -20,3 +20,7 @@ export type BedTypeProps = {
|
|||||||
export interface BedTypeFormSchema extends z.output<typeof bedTypeFormSchema> {}
|
export interface BedTypeFormSchema extends z.output<typeof bedTypeFormSchema> {}
|
||||||
|
|
||||||
export type BedTypeSchema = z.output<typeof bedTypeSchema>["bedType"]
|
export type BedTypeSchema = z.output<typeof bedTypeSchema>["bedType"]
|
||||||
|
|
||||||
|
export type BedTypeInfoProps = {
|
||||||
|
hasMultipleBedTypes: boolean
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ export namespace BlocksEnums {
|
|||||||
TextCols = "TextCols",
|
TextCols = "TextCols",
|
||||||
TextContent = "TextContent",
|
TextContent = "TextContent",
|
||||||
UspGrid = "UspGrid",
|
UspGrid = "UspGrid",
|
||||||
|
HotelListing = "HotelListing",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export namespace ContentPageEnum {
|
|||||||
TextCols = "ContentPageBlocksTextCols",
|
TextCols = "ContentPageBlocksTextCols",
|
||||||
UspGrid = "ContentPageBlocksUspGrid",
|
UspGrid = "ContentPageBlocksUspGrid",
|
||||||
Table = "ContentPageBlocksTable",
|
Table = "ContentPageBlocksTable",
|
||||||
|
HotelListing = "ContentPageBlocksHotelListing",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum sidebar {
|
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,
|
cardsGridSchema,
|
||||||
teaserCardBlockSchema,
|
teaserCardBlockSchema,
|
||||||
} from "@/server/routers/contentstack/schemas/blocks/cardsGrid"
|
} from "@/server/routers/contentstack/schemas/blocks/cardsGrid"
|
||||||
import { contentSchema } from "@/server/routers/contentstack/schemas/blocks/content"
|
import type { contentSchema } from "@/server/routers/contentstack/schemas/blocks/content"
|
||||||
import { dynamicContentSchema } from "@/server/routers/contentstack/schemas/blocks/dynamicContent"
|
import type { dynamicContentSchema } from "@/server/routers/contentstack/schemas/blocks/dynamicContent"
|
||||||
import { shortcutsSchema } from "@/server/routers/contentstack/schemas/blocks/shortcuts"
|
import type { hotelListingSchema } from "@/server/routers/contentstack/schemas/blocks/hotelListing"
|
||||||
import { tableSchema } from "@/server/routers/contentstack/schemas/blocks/table"
|
import type { shortcutsSchema } from "@/server/routers/contentstack/schemas/blocks/shortcuts"
|
||||||
import { textColsSchema } from "@/server/routers/contentstack/schemas/blocks/textCols"
|
import type { tableSchema } from "@/server/routers/contentstack/schemas/blocks/table"
|
||||||
import { uspGridSchema } from "@/server/routers/contentstack/schemas/blocks/uspGrid"
|
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 TeaserCard extends z.output<typeof teaserCardBlockSchema> {}
|
||||||
export interface CardsGrid extends z.output<typeof cardsGridSchema> {}
|
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 type TableData = TableBlock["table"]
|
||||||
export interface TextCols extends z.output<typeof textColsSchema> {}
|
export interface TextCols extends z.output<typeof textColsSchema> {}
|
||||||
export interface UspGrid extends z.output<typeof uspGridSchema> {}
|
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,
|
contentBlock,
|
||||||
hotelPageRefsSchema,
|
hotelPageRefsSchema,
|
||||||
hotelPageSchema,
|
hotelPageSchema,
|
||||||
|
hotelPageUrlSchema,
|
||||||
} from "@/server/routers/contentstack/hotelPage/output"
|
} 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 GetHotelPageData extends z.input<typeof hotelPageSchema> {}
|
||||||
export interface HotelPage extends z.output<typeof hotelPageSchema> {}
|
export interface HotelPage extends z.output<typeof hotelPageSchema> {}
|
||||||
@@ -18,3 +19,7 @@ export interface GetHotelPageRefsSchema
|
|||||||
extends z.input<typeof hotelPageRefsSchema> {}
|
extends z.input<typeof hotelPageRefsSchema> {}
|
||||||
|
|
||||||
export interface HotelPageRefs extends z.output<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 { Edges } from "@/types/requests/utils/edges"
|
||||||
import type { NodeRefs } from "@/types/requests/utils/refs"
|
import type { NodeRefs } from "@/types/requests/utils/refs"
|
||||||
import type { Lang } from "@/constants/languages"
|
import type { Lang } from "@/constants/languages"
|
||||||
@@ -109,3 +109,14 @@ export function generateLoyaltyConfigTag(
|
|||||||
export function generateServiceTokenTag(scopes: string[]) {
|
export function generateServiceTokenTag(scopes: string[]) {
|
||||||
return `service_token:${scopes.join("-")}`
|
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