Merged in feat/SW-1453-city-listing-on-country-page (pull request #1222)
feat(SW-1453): added city listing component * feat(SW-1453): added city listing component Approved-by: Christian Andolf Approved-by: Fredrik Thorsson
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { removeMultipleSlashes } from "@/utils/url"
|
||||
|
||||
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
|
||||
import {
|
||||
linkRefsUnionSchema,
|
||||
@@ -14,6 +16,44 @@ import {
|
||||
type TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
|
||||
export const destinationCityListDataSchema = z
|
||||
.object({
|
||||
all_destination_city_page: z.object({
|
||||
items: z.array(
|
||||
z
|
||||
.object({
|
||||
heading: z.string(),
|
||||
preamble: z.string(),
|
||||
experiences: z
|
||||
.object({
|
||||
destination_experiences: z.array(z.string()),
|
||||
})
|
||||
.transform(
|
||||
({ destination_experiences }) => destination_experiences
|
||||
),
|
||||
images: z
|
||||
.array(z.object({ image: tempImageVaultAssetSchema }))
|
||||
.transform((images) =>
|
||||
images
|
||||
.map((image) => image.image)
|
||||
.filter((image): image is ImageVaultAsset => !!image)
|
||||
),
|
||||
url: z.string(),
|
||||
system: systemSchema,
|
||||
})
|
||||
.transform((data) => {
|
||||
return {
|
||||
...data,
|
||||
url: removeMultipleSlashes(`/${data.system.locale}/${data.url}`),
|
||||
}
|
||||
})
|
||||
),
|
||||
}),
|
||||
})
|
||||
.transform(
|
||||
({ all_destination_city_page }) => all_destination_city_page.items?.[0]
|
||||
)
|
||||
|
||||
export const destinationCityPageSchema = z
|
||||
.object({
|
||||
destination_city_page: z.object({
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { env } from "@/env/server"
|
||||
import {
|
||||
GetDestinationCountryPage,
|
||||
GetDestinationCountryPageRefs,
|
||||
} from "@/lib/graphql/Query/DestinationCountryPage/DestinationCountryPage.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||
import { contentStackUidWithServiceProcedure, router } from "@/server/trpc"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
import { generateTag } from "@/utils/generateTag"
|
||||
|
||||
import { getCitiesByCountry } from "../../hotels/utils"
|
||||
import {
|
||||
destinationCountryPageRefsSchema,
|
||||
destinationCountryPageSchema,
|
||||
@@ -20,16 +23,19 @@ import {
|
||||
getDestinationCountryPageRefsSuccessCounter,
|
||||
getDestinationCountryPageSuccessCounter,
|
||||
} from "./telemetry"
|
||||
import { generatePageTags } from "./utils"
|
||||
import { generatePageTags, getCityListDataByCityIdentifier } from "./utils"
|
||||
|
||||
import { ApiCountry } from "@/types/enums/country"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
import type {
|
||||
GetDestinationCountryPageData,
|
||||
GetDestinationCountryPageRefsSchema,
|
||||
} from "@/types/trpc/routers/contentstack/destinationCountryPage"
|
||||
|
||||
export const destinationCountryPageQueryRouter = router({
|
||||
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
||||
const { lang, uid } = ctx
|
||||
get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => {
|
||||
const { lang, uid, serviceToken } = ctx
|
||||
const apiLang = toApiLang(lang)
|
||||
|
||||
getDestinationCountryPageRefsCounter.add(1, { lang, uid })
|
||||
console.info(
|
||||
@@ -128,27 +134,64 @@ export const destinationCountryPageQueryRouter = router({
|
||||
throw notFoundError
|
||||
}
|
||||
|
||||
const destinationCountryPage = destinationCountryPageSchema.safeParse(
|
||||
const validatedResponse = destinationCountryPageSchema.safeParse(
|
||||
response.data
|
||||
)
|
||||
|
||||
if (!destinationCountryPage.success) {
|
||||
if (!validatedResponse.success) {
|
||||
getDestinationCountryPageFailCounter.add(1, {
|
||||
lang,
|
||||
uid: `${uid}`,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(destinationCountryPage.error),
|
||||
error: JSON.stringify(validatedResponse.error),
|
||||
})
|
||||
console.error(
|
||||
"contentstack.destinationCountryPage validation error",
|
||||
JSON.stringify({
|
||||
query: { lang, uid },
|
||||
error: destinationCountryPage.error,
|
||||
error: validatedResponse.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
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 ${serviceToken}`,
|
||||
},
|
||||
next: {
|
||||
revalidate: env.CACHE_TIME_HOTELS,
|
||||
},
|
||||
}
|
||||
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 cityPages = await Promise.all(
|
||||
publishedCities.map(async (city) => {
|
||||
if (!city.cityIdentifier) {
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await getCityListDataByCityIdentifier(
|
||||
lang,
|
||||
city.cityIdentifier
|
||||
)
|
||||
return data ? { ...data, cityName: city.name } : null
|
||||
})
|
||||
)
|
||||
|
||||
getDestinationCountryPageSuccessCounter.add(1, { lang, uid: `${uid}` })
|
||||
console.info(
|
||||
"contentstack.destinationCountryPage success",
|
||||
@@ -157,6 +200,11 @@ export const destinationCountryPageQueryRouter = router({
|
||||
})
|
||||
)
|
||||
|
||||
return destinationCountryPage.data
|
||||
return {
|
||||
...validatedResponse.data,
|
||||
cities: cityPages
|
||||
.flat()
|
||||
.filter((city): city is NonNullable<typeof city> => !!city),
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -21,3 +21,13 @@ export const getDestinationCountryPageSuccessCounter = meter.createCounter(
|
||||
export const getDestinationCountryPageFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.destinationCountryPage.get-fail"
|
||||
)
|
||||
|
||||
export const getCityListDataCounter = meter.createCounter(
|
||||
"trpc.contentstack.cityListData.get"
|
||||
)
|
||||
export const getCityListDataSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.cityListData.get-success"
|
||||
)
|
||||
export const getCityListDataFailCounter = meter.createCounter(
|
||||
"trpc.contentstack.cityListData.get-fail"
|
||||
)
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
import { GetDestinationCityListData } from "@/lib/graphql/Query/DestinationCityPage/DestinationCityListData.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
|
||||
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
||||
|
||||
import { destinationCityListDataSchema } from "../destinationCityPage/output"
|
||||
import {
|
||||
getCityListDataCounter,
|
||||
getCityListDataFailCounter,
|
||||
getCityListDataSuccessCounter,
|
||||
} from "./telemetry"
|
||||
|
||||
import type { System } from "@/types/requests/system"
|
||||
import type { GetDestinationCityListDataResponse } from "@/types/trpc/routers/contentstack/destinationCityPage"
|
||||
import type { GetDestinationCountryPageRefsSchema } from "@/types/trpc/routers/contentstack/destinationCountryPage"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
@@ -30,3 +41,70 @@ export function getConnections({
|
||||
|
||||
return connections
|
||||
}
|
||||
|
||||
export async function getCityListDataByCityIdentifier(
|
||||
lang: Lang,
|
||||
cityIdentifier: string
|
||||
) {
|
||||
getCityListDataCounter.add(1, { lang, cityIdentifier })
|
||||
console.info(
|
||||
"contentstack.cityListData start",
|
||||
JSON.stringify({ query: { lang, cityIdentifier } })
|
||||
)
|
||||
const tag = `${lang}:city_list_data:${cityIdentifier}`
|
||||
const response = await request<GetDestinationCityListDataResponse>(
|
||||
GetDestinationCityListData,
|
||||
{
|
||||
locale: lang,
|
||||
cityIdentifier,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [tag],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
getCityListDataFailCounter.add(1, {
|
||||
lang,
|
||||
cityIdentifier,
|
||||
error_type: "not_found",
|
||||
error: `Destination city page not found for cityIdentifier: ${cityIdentifier}`,
|
||||
})
|
||||
console.error(
|
||||
"contentstack.cityListData not found error",
|
||||
JSON.stringify({ query: { lang, cityIdentifier } })
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const validatedResponse = destinationCityListDataSchema.safeParse(
|
||||
response.data
|
||||
)
|
||||
|
||||
if (!validatedResponse.success) {
|
||||
getCityListDataFailCounter.add(1, {
|
||||
lang,
|
||||
cityIdentifier,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedResponse.error),
|
||||
})
|
||||
console.error(
|
||||
"contentstack.cityListData validation error",
|
||||
JSON.stringify({
|
||||
query: { lang, cityIdentifier },
|
||||
error: validatedResponse.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
getCityListDataSuccessCounter.add(1, { lang, cityIdentifier })
|
||||
console.info(
|
||||
"contentstack.cityListData success",
|
||||
JSON.stringify({ query: { lang, cityIdentifier } })
|
||||
)
|
||||
|
||||
return validatedResponse.data
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import { cache } from "@/utils/cache"
|
||||
|
||||
import { getHotelPageUrl } from "../contentstack/hotelPage/utils"
|
||||
import { getVerifiedUser, parsedUser } from "../user/query"
|
||||
import { additionalDataSchema } from "./schemas/additionalData"
|
||||
import {
|
||||
getAdditionalDataInputSchema,
|
||||
getBreakfastPackageInputSchema,
|
||||
@@ -43,6 +42,7 @@ import {
|
||||
getRoomPackagesSchema,
|
||||
getRoomsAvailabilitySchema,
|
||||
} from "./output"
|
||||
import { additionalDataSchema } from "./schemas/additionalData"
|
||||
import {
|
||||
additionalDataCounter,
|
||||
additionalDataFailCounter,
|
||||
@@ -1132,9 +1132,9 @@ export const hotelQueryRouter = router({
|
||||
if (!countries) {
|
||||
return null
|
||||
}
|
||||
|
||||
const countryNames = countries.data.map((country) => country.name)
|
||||
const citiesByCountry = await getCitiesByCountry(
|
||||
countries,
|
||||
countryNames,
|
||||
options,
|
||||
searchParams,
|
||||
ctx.lang
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
apiCountriesSchema,
|
||||
apiLocationsSchema,
|
||||
type CitiesGroupedByCountry,
|
||||
type Countries,
|
||||
getHotelIdsByCityIdSchema,
|
||||
} from "./output"
|
||||
import {
|
||||
@@ -124,22 +123,23 @@ export async function getCountries(
|
||||
}
|
||||
|
||||
export async function getCitiesByCountry(
|
||||
countries: Countries,
|
||||
countries: string[],
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams,
|
||||
lang: Lang
|
||||
lang: Lang,
|
||||
affix: string = locationsAffix
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (
|
||||
searchParams: URLSearchParams,
|
||||
searchedCountries: Countries
|
||||
searchedCountries: string[]
|
||||
) {
|
||||
const citiesGroupedByCountry: CitiesGroupedByCountry = {}
|
||||
|
||||
await Promise.all(
|
||||
searchedCountries.data.map(async (country) => {
|
||||
searchedCountries.map(async (country) => {
|
||||
const countryResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.Cities.country(country.name),
|
||||
api.endpoints.v1.Hotel.Cities.country(country),
|
||||
options,
|
||||
searchParams
|
||||
)
|
||||
@@ -157,7 +157,7 @@ export async function getCitiesByCountry(
|
||||
return null
|
||||
}
|
||||
|
||||
citiesGroupedByCountry[country.name] = citiesByCountry.data.data
|
||||
citiesGroupedByCountry[country] = citiesByCountry.data.data
|
||||
return true
|
||||
})
|
||||
)
|
||||
@@ -165,7 +165,7 @@ export async function getCitiesByCountry(
|
||||
return citiesGroupedByCountry
|
||||
},
|
||||
[
|
||||
`${lang}:${locationsAffix}:cities-by-country`,
|
||||
`${lang}:${affix}:cities-by-country`,
|
||||
params.toString(),
|
||||
JSON.stringify(countries),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user