Merged in feat/SW-1444-destination-page-add-destination-list-component (pull request #1240)
feat/SW-1444 destination page add destination list component * feat(SW-1444): add list component * feat(SW-1444): add subtitle to accordion * feat(SW-1444): refactor component structure * feat(SW-1444): add desktop breakpoint * feat(SW-1444): fix typo * feat(SW-1444): add props * feat(SW-1444): add query * feat(SW-1444): updated query * feat(SW-1444): display data * feat(SW-1444): fix merge hickup * feat(SW-1444): change var name * feat(SW-1444): remove unsued translations * feat(SW-1444): use country as title * feat(SW-1444): sort hotels in query * feat(SW-1444): make responsive * feat(SW-1444): fetch country url * feat(SW-1444): update logging * feat(SW-1444): remove spread Approved-by: Erik Tiekstra
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.citiesList {
|
||||
column-count: 2;
|
||||
list-style-type: none;
|
||||
margin-bottom: var(--Spacing-x-half);
|
||||
}
|
||||
.citiesList li {
|
||||
margin-bottom: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.citiesList {
|
||||
column-count: 3;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import styles from "./destination.module.css"
|
||||
|
||||
import type { DestinationProps } from "@/types/components/destinationOverviewPage/destinationsList/destinationsData"
|
||||
|
||||
export default async function Destination({
|
||||
country,
|
||||
countryUrl,
|
||||
numberOfHotels,
|
||||
cities,
|
||||
}: DestinationProps) {
|
||||
const intl = await getIntl()
|
||||
const accordionSubtitle = intl.formatMessage(
|
||||
{
|
||||
id: "{amount, plural, one {# hotel} other {# hotels}}",
|
||||
},
|
||||
{ amount: numberOfHotels }
|
||||
)
|
||||
|
||||
return (
|
||||
<AccordionItem title={country} subtitle={accordionSubtitle}>
|
||||
<div className={styles.container}>
|
||||
<ul className={styles.citiesList}>
|
||||
{cities.map((city) => (
|
||||
<li key={city.id}>
|
||||
<Link
|
||||
href={city.url ? city.url : ""}
|
||||
color="baseTextMediumContrast"
|
||||
textDecoration="underline"
|
||||
>
|
||||
{`${city.name} (${city.hotelCount})`}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{countryUrl && (
|
||||
<Link href={countryUrl} variant="icon" color="burgundy" weight="bold">
|
||||
{intl.formatMessage({ id: "See destination" })}
|
||||
<ChevronRightSmallIcon color="burgundy" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</AccordionItem>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
.listContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
}
|
||||
|
||||
.accordion {
|
||||
flex: 1;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.listContainer {
|
||||
gap: var(--Spacing-x3);
|
||||
background-color: transparent;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.accordion {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import Accordion from "@/components/TempDesignSystem/Accordion"
|
||||
import Divider from "@/components/TempDesignSystem/Divider"
|
||||
|
||||
import Destination from "./Destination"
|
||||
|
||||
import styles from "./destinationsList.module.css"
|
||||
|
||||
import type { DestinationsListProps } from "@/types/components/destinationOverviewPage/destinationsList/destinationsData"
|
||||
|
||||
export default function DestinationsList({
|
||||
destinations,
|
||||
}: DestinationsListProps) {
|
||||
const middleIndex = Math.ceil(destinations.length / 2)
|
||||
const accordionLeft = destinations.slice(0, middleIndex)
|
||||
const accordionRight = destinations.slice(middleIndex)
|
||||
|
||||
return (
|
||||
<div className={styles.listContainer}>
|
||||
<Accordion className={styles.accordion}>
|
||||
{accordionLeft.map((data) => (
|
||||
<Destination
|
||||
key={data.country}
|
||||
country={data.country}
|
||||
countryUrl={data.countryUrl}
|
||||
numberOfHotels={data.numberOfHotels}
|
||||
cities={data.cities}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
<Divider color="subtle" className={styles.divider} />
|
||||
<Accordion className={styles.accordion}>
|
||||
{accordionRight.map((data) => (
|
||||
<Destination
|
||||
key={data.country}
|
||||
country={data.country}
|
||||
countryUrl={data.countryUrl}
|
||||
numberOfHotels={data.numberOfHotels}
|
||||
cities={data.cities}
|
||||
/>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x4);
|
||||
padding: var(--Spacing-x5) var(--Spacing-x2) var(--Spacing-x7);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
gap: var(--Spacing-x7);
|
||||
padding: var(--Spacing-x5) 9.625rem var(--Spacing-x9) 9.625rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import DestinationsList from "./DestinationsList"
|
||||
|
||||
import styles from "./hotelsSection.module.css"
|
||||
|
||||
import type { HotelsSectionProps } from "@/types/components/destinationOverviewPage/destinationsList/destinationsData"
|
||||
|
||||
export default async function HotelsSection({
|
||||
destinations,
|
||||
}: HotelsSectionProps) {
|
||||
const intl = await getIntl()
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
<Title level="h4" as="h2" textAlign="center">
|
||||
{intl.formatMessage({ id: "Explore all our hotels" })}
|
||||
</Title>
|
||||
<DestinationsList destinations={destinations} />
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
.pageContainer {
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
import { Suspense } from "react"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import { getDestinationOverviewPage } from "@/lib/trpc/memoizedRequests"
|
||||
import {
|
||||
getDestinationOverviewPage,
|
||||
getDestinationsList,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
|
||||
import HotelsSection from "./HotelsSection"
|
||||
import OverviewMapContainer from "./OverviewMapContainer"
|
||||
|
||||
import styles from "./destinationOverviewPage.module.css"
|
||||
|
||||
export default async function DestinationOverviewPage() {
|
||||
const pageData = await getDestinationOverviewPage()
|
||||
const [pageData, destinationsData] = await Promise.all([
|
||||
getDestinationOverviewPage(),
|
||||
getDestinationsList(),
|
||||
])
|
||||
|
||||
if (!pageData) {
|
||||
return null
|
||||
@@ -27,7 +34,7 @@ export default async function DestinationOverviewPage() {
|
||||
{googleMapsApiKey ? (
|
||||
<OverviewMapContainer apiKey={googleMapsApiKey} mapId={googleMapId} />
|
||||
) : null}
|
||||
<h1>Destination Overview Page</h1>
|
||||
{destinationsData && <HotelsSection destinations={destinationsData} />}
|
||||
</div>
|
||||
<Suspense fallback={null}>
|
||||
<TrackingSDK pageData={tracking} />
|
||||
|
||||
@@ -9,4 +9,5 @@ export interface AccordionItemProps
|
||||
title: string
|
||||
icon?: IconName
|
||||
trackingId?: string
|
||||
subtitle?: string
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export default function AccordionItem({
|
||||
variant,
|
||||
className,
|
||||
trackingId,
|
||||
subtitle,
|
||||
}: AccordionItemProps) {
|
||||
const contentRef = useRef<HTMLDivElement>(null)
|
||||
const detailsRef = useRef<HTMLDetailsElement>(null)
|
||||
@@ -55,9 +56,7 @@ export default function AccordionItem({
|
||||
<li className={accordionItemVariants({ className, variant, theme })}>
|
||||
<details ref={detailsRef} onToggle={toggleAccordion}>
|
||||
<summary className={styles.summary}>
|
||||
{IconComp && (
|
||||
<IconComp className={styles.icon} color="baseTextHighcontrast" />
|
||||
)}
|
||||
{IconComp && <IconComp color="baseTextHighcontrast" />}
|
||||
{variant === "sidepeek" ? (
|
||||
<Subtitle
|
||||
className={styles.title}
|
||||
@@ -67,13 +66,18 @@ export default function AccordionItem({
|
||||
{title}
|
||||
</Subtitle>
|
||||
) : (
|
||||
<Body
|
||||
textTransform="bold"
|
||||
color="baseTextHighContrast"
|
||||
className={styles.title}
|
||||
>
|
||||
{title}
|
||||
</Body>
|
||||
<div className={styles.title}>
|
||||
{subtitle ? (
|
||||
<Subtitle type="two" color="baseTextHighContrast">
|
||||
{title}
|
||||
</Subtitle>
|
||||
) : (
|
||||
<Body textTransform="bold" color="baseTextHighContrast">
|
||||
{title}
|
||||
</Body>
|
||||
)}
|
||||
{subtitle && <Body color="baseTextHighContrast">{subtitle}</Body>}
|
||||
</div>
|
||||
)}
|
||||
<ChevronDownIcon
|
||||
className={styles.chevron}
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Events that make an impression": "Events that make an impression",
|
||||
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
||||
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
|
||||
"Explore all our hotels": "Udforsk alle vores hoteller",
|
||||
"Explore nearby": "Udforsk i nærheden",
|
||||
"Explore {city}": "Udforsk {city}",
|
||||
"Extra bed (child) × {count}": "Ekstra seng (barn) × {count}",
|
||||
@@ -396,6 +397,7 @@
|
||||
"Search": "Søge",
|
||||
"See all FAQ": "Se alle FAQ",
|
||||
"See all photos": "Se alle billeder",
|
||||
"See destination": "Se destination",
|
||||
"See details": "Se detaljer",
|
||||
"See hotel details": "Se hoteloplysninger",
|
||||
"See less FAQ": "Se mindre FAQ",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"Events that make an impression": "Events that make an impression",
|
||||
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
||||
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
|
||||
"Explore all our hotels": "Entdecken Sie alle unsere Hotels",
|
||||
"Explore nearby": "Erkunden Sie die Umgebung",
|
||||
"Explore {city}": "Erkunden Sie {city}",
|
||||
"Extra bed (child) × {count}": "Ekstra seng (Kind) × {count}",
|
||||
@@ -395,6 +396,7 @@
|
||||
"Search": "Suchen",
|
||||
"See all FAQ": "Siehe alle FAQ",
|
||||
"See all photos": "Alle Fotos ansehen",
|
||||
"See destination": "Siehe Ziel",
|
||||
"See details": "Siehe Einzelheiten",
|
||||
"See hotel details": "Hotelinformationen ansehen",
|
||||
"See less FAQ": "Weniger anzeigen FAQ",
|
||||
|
||||
@@ -166,6 +166,7 @@
|
||||
"Events that make an impression": "Events that make an impression",
|
||||
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
||||
"Explore all levels and benefits": "Explore all levels and benefits",
|
||||
"Explore all our hotels": "Explore all our hotels",
|
||||
"Explore nearby": "Explore nearby",
|
||||
"Explore {city}": "Explore {city}",
|
||||
"Extra bed (child) × {count}": "Extra bed (child) × {count}",
|
||||
@@ -438,6 +439,7 @@
|
||||
"See all FAQ": "See all FAQ",
|
||||
"See all photos": "See all photos",
|
||||
"See alternative hotels": "See alternative hotels",
|
||||
"See destination": "See destination",
|
||||
"See details": "See details",
|
||||
"See hotel details": "See hotel details",
|
||||
"See less FAQ": "See less FAQ",
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
"Events that make an impression": "Events that make an impression",
|
||||
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
||||
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
||||
"Explore all our hotels": "Tutustu kaikkiin hotelleihimme",
|
||||
"Explore nearby": "Tutustu lähialueeseen",
|
||||
"Explore {city}": "Tutustu {city}",
|
||||
"Extra bed (child) × {count}": "Lisävuode (lasta) × {count}",
|
||||
@@ -398,6 +399,7 @@
|
||||
"Search": "Haku",
|
||||
"See all FAQ": "Katso kaikki UKK",
|
||||
"See all photos": "Katso kaikki kuvat",
|
||||
"See destination": "Katso kohde",
|
||||
"See details": "Katso tiedot",
|
||||
"See hotel details": "Katso hotellin tiedot",
|
||||
"See less FAQ": "Katso vähemmän UKK",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"Events that make an impression": "Events that make an impression",
|
||||
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
||||
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
|
||||
"Explore all our hotels": "Utforsk alle våre hoteller",
|
||||
"Explore nearby": "Utforsk i nærheten",
|
||||
"Explore {city}": "Utforsk {city}",
|
||||
"Extra bed (child) × {count}": "Ekstra seng (barn) × {count}",
|
||||
@@ -396,6 +397,7 @@
|
||||
"Search": "Søk",
|
||||
"See all FAQ": "Se alle FAQ",
|
||||
"See all photos": "Se alle bilder",
|
||||
"See destination": "Se destinasjon",
|
||||
"See details": "Se detaljer",
|
||||
"See hotel details": "Se hotellinformasjon",
|
||||
"See less FAQ": "Se mindre FAQ",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"Events that make an impression": "Events that make an impression",
|
||||
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
||||
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
|
||||
"Explore all our hotels": "Utforska alla våra hotell",
|
||||
"Explore nearby": "Utforska i närheten",
|
||||
"Explore {city}": "Utforska {city}",
|
||||
"Extra bed (child) × {count}": "Extra säng (barn) × {count}",
|
||||
@@ -396,6 +397,7 @@
|
||||
"Search": "Sök",
|
||||
"See all FAQ": "Se alla FAQ",
|
||||
"See all photos": "Se alla foton",
|
||||
"See destination": "Se destination",
|
||||
"See details": "Se detaljer",
|
||||
"See hotel details": "Se hotellinformation",
|
||||
"See less FAQ": "See färre FAQ",
|
||||
|
||||
@@ -23,6 +23,20 @@ query GetDestinationOverviewPageRefs($locale: String!, $uid: String!) {
|
||||
}
|
||||
}
|
||||
|
||||
query GetCountryPageUrl($locale: String!, $country: String!) {
|
||||
all_destination_country_page(
|
||||
where: { destination_settings: { country: $country } }
|
||||
locale: $locale
|
||||
) {
|
||||
items {
|
||||
url
|
||||
system {
|
||||
...System
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query GetDaDeEnUrlsDestinationOverviewPage($uid: String!) {
|
||||
de: destination_overview_page(locale: "de", uid: $uid) {
|
||||
url
|
||||
|
||||
@@ -188,6 +188,11 @@ export const getDestinationOverviewPage = cache(
|
||||
return serverClient().contentstack.destinationOverviewPage.get()
|
||||
}
|
||||
)
|
||||
export const getDestinationsList = cache(
|
||||
async function getMemoizedDestinationsList() {
|
||||
return serverClient().contentstack.destinationOverviewPage.destinations.get()
|
||||
}
|
||||
)
|
||||
export const getDestinationCountryPage = cache(
|
||||
async function getMemoizedDestinationCountryPage() {
|
||||
return serverClient().contentstack.destinationCountryPage.get()
|
||||
|
||||
@@ -172,14 +172,17 @@ export const destinationCountryPageQueryRouter = router({
|
||||
const selectedCountry =
|
||||
validatedResponse.data.destinationCountryPage.destination_settings.country
|
||||
const apiCountry = ApiCountry[lang][selectedCountry]
|
||||
const cities = await getCitiesByCountry([apiCountry], options, params, lang)
|
||||
|
||||
const publishedCities = cities[apiCountry].filter(
|
||||
(city) => city.isPublished
|
||||
const cities = await getCitiesByCountry(
|
||||
[apiCountry],
|
||||
options,
|
||||
params,
|
||||
lang,
|
||||
true,
|
||||
"destinationCountryPage"
|
||||
)
|
||||
|
||||
const cityPages = await Promise.all(
|
||||
publishedCities.map(async (city) => {
|
||||
cities[apiCountry].map(async (city) => {
|
||||
if (!city.cityIdentifier) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { removeMultipleSlashes } from "@/utils/url"
|
||||
|
||||
import { systemSchema } from "../schemas/system"
|
||||
|
||||
export const destinationOverviewPageSchema = z.object({
|
||||
@@ -17,6 +19,27 @@ export const destinationOverviewPageSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
export const countryPageUrlSchema = z
|
||||
.object({
|
||||
all_destination_country_page: z.object({
|
||||
items: z.array(
|
||||
z
|
||||
.object({
|
||||
url: z.string(),
|
||||
system: systemSchema,
|
||||
})
|
||||
.transform((data) => {
|
||||
return {
|
||||
url: removeMultipleSlashes(`/${data.system.locale}/${data.url}`),
|
||||
}
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
.transform(
|
||||
({ all_destination_country_page }) => all_destination_country_page.items[0]
|
||||
)
|
||||
|
||||
/** REFS */
|
||||
export const destinationOverviewPageRefsSchema = z.object({
|
||||
destination_overview_page: z.object({
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { env } from "@/env/server"
|
||||
import {
|
||||
GetDestinationOverviewPage,
|
||||
GetDestinationOverviewPageRefs,
|
||||
} from "@/lib/graphql/Query/DestinationOverviewPage/DestinationOverviewPage.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||
import {
|
||||
contentstackExtendedProcedureUID,
|
||||
router,
|
||||
serviceProcedure,
|
||||
} from "@/server/trpc"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
import { generateTag } from "@/utils/generateTag"
|
||||
|
||||
import {
|
||||
getCitiesByCountry,
|
||||
getCountries,
|
||||
getHotelIdsByCityId,
|
||||
} from "../../hotels/utils"
|
||||
import { getCityListDataByCityIdentifier } from "../destinationCountryPage/utils"
|
||||
import {
|
||||
destinationOverviewPageRefsSchema,
|
||||
destinationOverviewPageSchema,
|
||||
@@ -20,11 +32,14 @@ import {
|
||||
getDestinationOverviewPageRefsSuccessCounter,
|
||||
getDestinationOverviewPageSuccessCounter,
|
||||
} from "./telemetry"
|
||||
import { getCountryPageUrl } from "./utils"
|
||||
|
||||
import type { DestinationsData } from "@/types/components/destinationOverviewPage/destinationsList/destinationsData"
|
||||
import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
import type {
|
||||
GetDestinationOverviewPageData,
|
||||
GetDestinationOverviewPageRefsSchema,
|
||||
@@ -187,4 +202,90 @@ export const destinationOverviewPageQueryRouter = router({
|
||||
tracking,
|
||||
}
|
||||
}),
|
||||
destinations: router({
|
||||
get: serviceProcedure.query(async function ({ ctx }) {
|
||||
const apiLang = toApiLang(ctx.lang)
|
||||
const params = new URLSearchParams({
|
||||
language: apiLang,
|
||||
})
|
||||
|
||||
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: env.CACHE_TIME_HOTELS,
|
||||
},
|
||||
}
|
||||
|
||||
const countries = await getCountries(options, params, ctx.lang)
|
||||
|
||||
if (!countries) {
|
||||
return null
|
||||
}
|
||||
|
||||
const countryNames = countries.data.map((country) => country.name)
|
||||
|
||||
const citiesByCountry = await getCitiesByCountry(
|
||||
countryNames,
|
||||
options,
|
||||
params,
|
||||
ctx.lang,
|
||||
true
|
||||
)
|
||||
|
||||
const destinations: DestinationsData = await Promise.all(
|
||||
Object.entries(citiesByCountry).map(async ([country, cities]) => {
|
||||
const citiesWithHotelCount = await Promise.all(
|
||||
cities.map(async (city) => {
|
||||
const hotelIdsParams = new URLSearchParams({
|
||||
language: apiLang,
|
||||
city: city.id,
|
||||
onlyBasicInfo: "true",
|
||||
})
|
||||
|
||||
const hotels = await getHotelIdsByCityId(
|
||||
city.id,
|
||||
options,
|
||||
hotelIdsParams
|
||||
)
|
||||
|
||||
let cityUrl
|
||||
if (city.cityIdentifier) {
|
||||
cityUrl = await getCityListDataByCityIdentifier(
|
||||
ctx.lang,
|
||||
city.cityIdentifier
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
id: city.id,
|
||||
name: city.name,
|
||||
hotelIds: hotels,
|
||||
hotelCount: hotels?.length ?? 0,
|
||||
url: cityUrl?.url,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const countryUrl = await getCountryPageUrl(ctx.lang, country)
|
||||
|
||||
return {
|
||||
country,
|
||||
countryUrl: countryUrl?.url,
|
||||
numberOfHotels: citiesWithHotelCount.reduce(
|
||||
(acc, city) => acc + city.hotelCount,
|
||||
0
|
||||
),
|
||||
cities: citiesWithHotelCount,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return destinations.sort((a, b) => a.country.localeCompare(b.country))
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -21,3 +21,15 @@ export const getDestinationOverviewPageSuccessCounter = meter.createCounter(
|
||||
export const getDestinationOverviewPageFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.destinationOverviewPage.get-fail"
|
||||
)
|
||||
|
||||
export const getCountryPageUrlCounter = meter.createCounter(
|
||||
"trpc.contentstack.getCountryPageUrl"
|
||||
)
|
||||
|
||||
export const getCountryPageUrlSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.getCountryPageUrl-success"
|
||||
)
|
||||
|
||||
export const getCountryPageUrlFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.getCountryPageUrl-fail"
|
||||
)
|
||||
|
||||
76
server/routers/contentstack/destinationOverviewPage/utils.ts
Normal file
76
server/routers/contentstack/destinationOverviewPage/utils.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { GetCountryPageUrl } from "@/lib/graphql/Query/DestinationOverviewPage/DestinationOverviewPage.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
|
||||
import { countryPageUrlSchema } from "./output"
|
||||
import {
|
||||
getCountryPageUrlCounter,
|
||||
getCountryPageUrlFailCounter,
|
||||
getCountryPageUrlSuccessCounter,
|
||||
} from "./telemetry"
|
||||
|
||||
import type { GetCountryPageUrlData } from "@/types/trpc/routers/contentstack/destinationOverviewPage"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
export async function getCountryPageUrl(lang: Lang, country: string) {
|
||||
getCountryPageUrlCounter.add(1, { lang, country })
|
||||
console.info(
|
||||
"contentstack.countryPageUrl start",
|
||||
JSON.stringify({ query: { lang, country } })
|
||||
)
|
||||
|
||||
const tag = `${lang}:country_page_url:${country}`
|
||||
const response = await request<GetCountryPageUrlData>(
|
||||
GetCountryPageUrl,
|
||||
{
|
||||
locale: lang,
|
||||
country,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [tag],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
getCountryPageUrlFailCounter.add(1, {
|
||||
lang,
|
||||
country,
|
||||
error_type: "not_found",
|
||||
error: `Country page not found for country: ${country}`,
|
||||
})
|
||||
console.error(
|
||||
"contentstack.countryPageUrl not found error",
|
||||
JSON.stringify({ query: { lang, country } })
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const validatedCountryPageUrl = countryPageUrlSchema.safeParse(response.data)
|
||||
|
||||
if (!validatedCountryPageUrl.success) {
|
||||
getCountryPageUrlFailCounter.add(1, {
|
||||
lang,
|
||||
country,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedCountryPageUrl.error),
|
||||
})
|
||||
console.error(
|
||||
"contentstack.countryPageUrl validation error",
|
||||
JSON.stringify({
|
||||
query: { lang, country },
|
||||
error: validatedCountryPageUrl.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
getCountryPageUrlSuccessCounter.add(1, { lang, country })
|
||||
console.info(
|
||||
"contentstack.countryPageUrl success",
|
||||
JSON.stringify({ query: { lang, country } })
|
||||
)
|
||||
|
||||
return validatedCountryPageUrl.data
|
||||
}
|
||||
@@ -52,7 +52,6 @@ import {
|
||||
getHotelIdsByCityId,
|
||||
getHotelIdsByCountry,
|
||||
getLocations,
|
||||
TWENTYFOUR_HOURS,
|
||||
} from "./utils"
|
||||
|
||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||
|
||||
@@ -126,6 +126,7 @@ export async function getCitiesByCountry(
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams,
|
||||
lang: Lang,
|
||||
onlyPublished = false, // false by default as it might be used in other places
|
||||
affix: string = locationsAffix
|
||||
) {
|
||||
return unstable_cache(
|
||||
@@ -155,7 +156,10 @@ export async function getCitiesByCountry(
|
||||
return null
|
||||
}
|
||||
|
||||
citiesGroupedByCountry[country] = citiesByCountry.data.data
|
||||
const cities = onlyPublished
|
||||
? citiesByCountry.data.data.filter((city) => city.isPublished)
|
||||
: citiesByCountry.data.data
|
||||
citiesGroupedByCountry[country] = cities
|
||||
return true
|
||||
})
|
||||
)
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
export type DestinationsData = {
|
||||
country: string
|
||||
countryUrl: string | undefined
|
||||
numberOfHotels: number
|
||||
cities: {
|
||||
id: string
|
||||
name: string
|
||||
hotelIds: string[] | null
|
||||
hotelCount: number
|
||||
url: string | undefined
|
||||
}[]
|
||||
}[]
|
||||
|
||||
export type HotelsSectionProps = {
|
||||
destinations: DestinationsData
|
||||
}
|
||||
|
||||
export type DestinationsListProps = {
|
||||
destinations: DestinationsData
|
||||
}
|
||||
|
||||
export type DestinationProps = {
|
||||
country: string
|
||||
countryUrl: string | undefined
|
||||
numberOfHotels: number
|
||||
cities: DestinationsData[number]["cities"]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { z } from "zod"
|
||||
|
||||
import type {
|
||||
countryPageUrlSchema,
|
||||
destinationOverviewPageRefsSchema,
|
||||
destinationOverviewPageSchema,
|
||||
} from "@/server/routers/contentstack/destinationOverviewPage/output"
|
||||
@@ -15,3 +16,6 @@ export interface GetDestinationOverviewPageRefsSchema
|
||||
|
||||
export interface DestinationOverviewPageRefs
|
||||
extends z.output<typeof destinationOverviewPageRefsSchema> {}
|
||||
|
||||
export interface GetCountryPageUrlData
|
||||
extends z.input<typeof countryPageUrlSchema> {}
|
||||
|
||||
Reference in New Issue
Block a user