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:
Fredrik Thorsson
2025-02-04 14:17:12 +00:00
parent 4ed4b3585b
commit b85a3a57ec
27 changed files with 489 additions and 21 deletions

View File

@@ -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
}

View File

@@ -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({

View File

@@ -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))
}),
}),
})

View File

@@ -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"
)

View 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
}