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:
Erik Tiekstra
2025-01-29 10:09:51 +00:00
parent a7468cd958
commit ca42876eb8
25 changed files with 496 additions and 57 deletions

View File

@@ -0,0 +1,39 @@
.container {
background-color: var(--Base-Surface-Primary-light-Normal);
border: 1px solid var(--Base-Border-Subtle);
border-radius: var(--Corner-radius-Medium);
overflow: hidden;
}
.image {
width: 100%;
max-height: 200px;
object-fit: cover;
}
.content {
display: grid;
gap: var(--Spacing-x2);
padding: var(--Spacing-x2) var(--Spacing-x3);
}
@media screen and (min-width: 768px) {
.container {
display: grid;
grid-template-columns: minmax(250px, 350px) auto;
}
.image {
max-height: none;
height: 100%;
}
.ctaWrapper {
display: flex;
justify-content: flex-end;
}
.button {
width: min(100%, 200px);
}
}

View File

@@ -0,0 +1,61 @@
import Link from "next/link"
import Image from "@/components/Image"
import Button from "@/components/TempDesignSystem/Button"
import Divider from "@/components/TempDesignSystem/Divider"
import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n"
import ExperienceList from "../../ExperienceList"
import styles from "./cityListingItem.module.css"
import type { DestinationCityListItem } from "@/types/trpc/routers/contentstack/destinationCityPage"
interface CityListingItemProps {
city: DestinationCityListItem
}
export default async function CityListingItem({ city }: CityListingItemProps) {
const intl = await getIntl()
const firstImage = city.images[0]
return (
<article className={styles.container}>
<Image
src={firstImage.url}
alt={firstImage.meta.alt || firstImage.meta.caption || ""}
width={300}
height={200}
className={styles.image}
/>
<section className={styles.content}>
<Subtitle asChild>
<h3>{city.heading}</h3>
</Subtitle>
<ExperienceList experiences={city.experiences} />
<Body>{city.preamble}</Body>
<Divider variant="horizontal" color="primaryLightSubtle" />
<div className={styles.ctaWrapper}>
<Button
intent="tertiary"
theme="base"
size="small"
className={styles.button}
asChild
>
<Link href={city.url}>
{intl.formatMessage(
{ id: "Explore {city}" },
{ city: city.cityName }
)}
</Link>
</Button>
</div>
</section>
</article>
)
}

View File

@@ -0,0 +1,10 @@
.container {
display: grid;
gap: var(--Spacing-x2);
}
.cityList {
list-style: none;
display: grid;
gap: var(--Spacing-x2);
}

View File

@@ -0,0 +1,36 @@
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import { getIntl } from "@/i18n"
import CityListingItem from "./CityListingItem"
import styles from "./cityListing.module.css"
import type { DestinationCityListItem } from "@/types/trpc/routers/contentstack/destinationCityPage"
interface CityListingProps {
cities: DestinationCityListItem[]
}
export default async function CityListing({ cities }: CityListingProps) {
const intl = await getIntl()
return (
<section className={styles.container}>
<div className={styles.listHeader}>
<Subtitle type="two">
{intl.formatMessage(
{ id: `{count} Locations` },
{ count: cities.length }
)}
</Subtitle>
</div>
<ul className={styles.cityList}>
{cities.map((city) => (
<li key={city.system.uid}>
<CityListingItem city={city} />
</li>
))}
</ul>
</section>
)
}

View File

@@ -4,17 +4,16 @@ import { getDestinationCityPage } from "@/lib/trpc/memoizedRequests"
import Breadcrumbs from "@/components/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import Chip from "@/components/TempDesignSystem/Chip"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n"
import ExperienceList from "../ExperienceList"
import SidebarContentWrapper from "../SidebarContentWrapper"
import DestinationPageSidePeek from "../Sidepeek"
import StaticMap from "../StaticMap"
import TopImages from "../TopImages"
import { mapExperiencesToListData } from "../utils"
import styles from "./destinationCityPage.module.css"
@@ -41,7 +40,6 @@ export default async function DestinationCityPage() {
sidepeek_content,
destination_settings,
} = destinationCityPage
const experiencesList = await mapExperiencesToListData(experiences)
return (
<>
@@ -60,16 +58,7 @@ export default async function DestinationCityPage() {
<SidebarContentWrapper>
<Title level="h2">{heading}</Title>
<Body color="uiTextMediumContrast">{preamble}</Body>
<ul className={styles.experienceList}>
{experiencesList.map(({ Icon, name }) => (
<li key={name}>
<Chip variant="tag">
<Icon width={20} height={20} />
{name}
</Chip>
</li>
))}
</ul>
<ExperienceList experiences={experiences} />
{has_sidepeek && (
<DestinationPageSidePeek
buttonText={sidepeek_button_text}

View File

@@ -16,7 +16,7 @@
.mainSection {
grid-area: mainSection;
padding-bottom: var(--Spacing-x7);
min-height: 500px; /* This is a temporary value because of no content atm */
max-width: var(--max-width-page);
}
.sidebar {

View File

@@ -4,17 +4,17 @@ import { getDestinationCountryPage } from "@/lib/trpc/memoizedRequests"
import Breadcrumbs from "@/components/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import Chip from "@/components/TempDesignSystem/Chip"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n"
import CityListing from "../CityListing"
import ExperienceList from "../ExperienceList"
import SidebarContentWrapper from "../SidebarContentWrapper"
import DestinationPageSidePeek from "../Sidepeek"
import StaticMap from "../StaticMap"
import TopImages from "../TopImages"
import { mapExperiencesToListData } from "../utils"
import styles from "./destinationCountryPage.module.css"
@@ -30,7 +30,7 @@ export default async function DestinationCountryPage() {
return null
}
const { tracking, destinationCountryPage } = pageData
const { tracking, destinationCountryPage, cities } = pageData
const {
images,
heading,
@@ -41,7 +41,6 @@ export default async function DestinationCountryPage() {
sidepeek_content,
destination_settings,
} = destinationCountryPage
const experiencesList = await mapExperiencesToListData(experiences)
return (
<>
@@ -53,23 +52,13 @@ export default async function DestinationCountryPage() {
<TopImages images={images} />
</header>
<main className={styles.mainSection}>
{/* TODO: Add city listing by cityIdentifier */}
{">>>> MAIN CONTENT <<<<"}
<CityListing cities={cities} />
</main>
<aside className={styles.sidebar}>
<SidebarContentWrapper>
<Title level="h2">{heading}</Title>
<Body color="uiTextMediumContrast">{preamble}</Body>
<ul className={styles.experienceList}>
{experiencesList.map(({ Icon, name }) => (
<li key={name}>
<Chip variant="tag">
<Icon width={20} height={20} />
{name}
</Chip>
</li>
))}
</ul>
<ExperienceList experiences={experiences} />
{has_sidepeek && (
<DestinationPageSidePeek
buttonText={sidepeek_button_text}

View File

@@ -0,0 +1,6 @@
.experienceList {
list-style: none;
display: flex;
gap: var(--Spacing-x1);
flex-wrap: wrap;
}

View File

@@ -0,0 +1,30 @@
import Chip from "@/components/TempDesignSystem/Chip"
import { getIntl } from "@/i18n"
import { mapExperiencesToListData } from "./utils"
import styles from "./experienceList.module.css"
interface ExperienceListProps {
experiences: string[]
}
export default async function ExperienceList({
experiences,
}: ExperienceListProps) {
const intl = await getIntl()
const experienceList = mapExperiencesToListData(experiences, intl)
return (
<ul className={styles.experienceList}>
{experienceList.map(({ Icon, name }) => (
<li key={name}>
<Chip variant="tag">
<Icon width={20} height={20} />
{name}
</Chip>
</li>
))}
</ul>
)
}

View File

@@ -7,17 +7,16 @@ import {
NightlifeIcon,
StarFilledIcon,
} from "@/components/Icons"
import { getIntl } from "@/i18n"
import type { FC } from "react"
import type { IntlShape } from "react-intl"
import type { IconProps } from "@/types/components/icon"
export async function mapExperiencesToListData(
experiences: string[]
): Promise<{ Icon: FC<IconProps>; name: string }[]> {
const intl = await getIntl()
export function mapExperiencesToListData(
experiences: string[],
intl: IntlShape
): { Icon: FC<IconProps>; name: string }[] {
return experiences.map((experience) => {
switch (experience) {
case "Hiking":

View File

@@ -154,6 +154,7 @@
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
"Explore nearby": "Udforsk i nærheden",
"Explore {city}": "Udforsk {city}",
"Extra bed (child) × {count}": "Ekstra seng (barn) × {count}",
"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",
@@ -554,6 +555,7 @@
"{card} ending with {cardno}": "{card} slutter med {cardno}",
"{checkInDate} from {checkInTime}": "{checkInDate} fra {checkInTime}",
"{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}",
"{count} Locations": "{count} steder",
"{count} lowercase letter": "{count} lille bogstav",
"{count} number": "{count} nummer",
"{count} special character": "{count} speciel karakter",

View File

@@ -153,6 +153,7 @@
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
"Explore nearby": "Erkunden Sie die Umgebung",
"Explore {city}": "Erkunden Sie {city}",
"Extra bed (child) × {count}": "Ekstra seng (Kind) × {count}",
"Extra bed will be provided additionally": "Ein zusätzliches Bett wird bereitgestellt",
"Extras to your booking": "Extras zu Ihrer Buchung",
@@ -552,6 +553,7 @@
"{card} ending with {cardno}": "{card} endet mit {cardno}",
"{checkInDate} from {checkInTime}": "{checkInDate} aus {checkInTime}",
"{checkOutDate} from {checkOutTime}": "{checkOutDate} aus {checkOutTime}",
"{count} Locations": "{count} Standorte",
"{count} lowercase letter": "{count} Kleinbuchstabe",
"{count} number": "{count} nummer",
"{count} special character": "{count} sonderzeichen",

View File

@@ -166,6 +166,7 @@
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
"Explore all levels and benefits": "Explore all levels and benefits",
"Explore nearby": "Explore nearby",
"Explore {city}": "Explore {city}",
"Extra bed (child) × {count}": "Extra bed (child) × {count}",
"Extra bed will be provided additionally": "Extra bed will be provided additionally",
"Extras to your booking": "Extras to your booking",
@@ -601,6 +602,7 @@
"{card} ending with {cardno}": "{card} ending with {cardno}",
"{checkInDate} from {checkInTime}": "{checkInDate} from {checkInTime}",
"{checkOutDate} from {checkOutTime}": "{checkOutDate} from {checkOutTime}",
"{count} Locations": "{count} Locations",
"{count} lowercase letter": "{count} lowercase letter",
"{count} number": "{count} number",
"{count} special character": "{count} special character",

View File

@@ -154,6 +154,7 @@
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
"Explore nearby": "Tutustu lähialueeseen",
"Explore {city}": "Tutustu {city}",
"Extra bed (child) × {count}": "Lisävuode (lasta) × {count}",
"Extra bed will be provided additionally": "Lisävuode toimitetaan erikseen",
"Extras to your booking": "Varauksessa lisäpalveluita",
@@ -551,6 +552,7 @@
"{card} ending with {cardno}": "{card} päättyen {cardno}",
"{checkInDate} from {checkInTime}": "{checkInDate} alkaen {checkInTime}",
"{checkOutDate} from {checkOutTime}": "{checkOutDate} alkaen {checkOutTime}",
"{count} Locations": "{count} sijaintia",
"{count} lowercase letter": "{count} pien kirjain",
"{count} number": "{count} määrä",
"{count} special character": "{count} erikoishahmo",

View File

@@ -153,6 +153,7 @@
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
"Explore nearby": "Utforsk i nærheten",
"Explore {city}": "Utforsk {city}",
"Extra bed (child) × {count}": "Ekstra seng (barn) × {count}",
"Extra bed will be provided additionally": "Ekstra seng vil bli tilgjengelig",
"Extras to your booking": "Tilvalg til bestillingen din",
@@ -552,6 +553,7 @@
"{card} ending with {cardno}": "{card} slutter med {cardno}",
"{checkInDate} from {checkInTime}": "{checkInDate} fra {checkInTime}",
"{checkOutDate} from {checkOutTime}": "{checkOutDate} fra {checkOutTime}",
"{count} Locations": "{count} steder",
"{count} lowercase letter": "{count} liten bokstav",
"{count} number": "{count} antall",
"{count} special character": "{count} spesiell karakter",

View File

@@ -153,6 +153,7 @@
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
"Explore nearby": "Utforska i närheten",
"Explore {city}": "Utforska {city}",
"Extra bed (child) × {count}": "Extra säng (barn) × {count}",
"Extra bed will be provided additionally": "Extra säng kommer att tillhandahållas",
"Extras to your booking": "Extra tillval till din bokning",
@@ -554,6 +555,7 @@
"{card} ending with {cardno}": "{card} som slutar på {cardno}",
"{checkInDate} from {checkInTime}": "{checkInDate} från {checkInTime}",
"{checkOutDate} from {checkOutTime}": "{checkOutDate} från {checkOutTime}",
"{count} Locations": "{count} platser",
"{count} lowercase letter": "{count} liten bokstav",
"{count} number": "{count} nummer",
"{count} special character": "{count} speciell karaktär",

View File

@@ -0,0 +1,28 @@
#import "../../Fragments/System.graphql"
query GetDestinationCityListData($locale: String!, $cityIdentifier: String!) {
all_destination_city_page(
where: {
OR: [
{ destination_settings: { city_sweden: $cityIdentifier } }
{ destination_settings: { city_denmark: $cityIdentifier } }
]
}
locale: $locale
) {
items {
heading
preamble
images {
image
}
experiences {
destination_experiences
}
url
system {
...System
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
import { Lang } from "@/constants/languages"
export enum Country {
Denmark = "Denmark",
Finland = "Finland",
@@ -6,3 +8,54 @@ export enum Country {
Poland = "Poland",
Sweden = "Sweden",
}
export const ApiCountry: Record<Lang, Record<Country, string>> = {
[Lang.da]: {
[Country.Denmark]: "Danmark",
[Country.Finland]: "Finland",
[Country.Germany]: "Tyskland",
[Country.Norway]: "Norge",
[Country.Poland]: "Polen",
[Country.Sweden]: "Sverige",
},
[Lang.de]: {
[Country.Denmark]: "Dänemark",
[Country.Finland]: "Finnland",
[Country.Germany]: "Deutschland",
[Country.Norway]: "Norwegen",
[Country.Poland]: "Polen",
[Country.Sweden]: "Schweden",
},
[Lang.en]: {
[Country.Denmark]: "Denmark",
[Country.Finland]: "Finland",
[Country.Germany]: "Germany",
[Country.Norway]: "Norway",
[Country.Poland]: "Poland",
[Country.Sweden]: "Sweden",
},
[Lang.fi]: {
[Country.Denmark]: "Tanska",
[Country.Finland]: "Suomi",
[Country.Germany]: "Saksa",
[Country.Norway]: "Norja",
[Country.Poland]: "Puola",
[Country.Sweden]: "Ruotsi",
},
[Lang.no]: {
[Country.Denmark]: "Danmark",
[Country.Finland]: "Finland",
[Country.Germany]: "Tyskland",
[Country.Norway]: "Norge",
[Country.Poland]: "Polen",
[Country.Sweden]: "Sverige",
},
[Lang.sv]: {
[Country.Denmark]: "Danmark",
[Country.Finland]: "Finland",
[Country.Germany]: "Tyskland",
[Country.Norway]: "Norge",
[Country.Poland]: "Polen",
[Country.Sweden]: "Sverige",
},
}

View File

@@ -1,16 +1,27 @@
import type { z } from "zod"
import type {
destinationCityListDataSchema,
destinationCityPageRefsSchema,
destinationCityPageSchema,
} from "@/server/routers/contentstack/destinationCityPage/output"
export interface GetDestinationCityPageData
extends z.input<typeof destinationCityPageSchema> {}
interface DestinationCityPage
export interface DestinationCityPage
extends z.output<typeof destinationCityPageSchema> {}
export type DestinationCityPageData = DestinationCityPage["destinationCityPage"]
export interface GetDestinationCityListDataResponse
extends z.input<typeof destinationCityListDataSchema> {}
export interface DestinationCityListData
extends z.output<typeof destinationCityListDataSchema> {}
export interface DestinationCityListItem extends DestinationCityListData {
cityName: string
}
export interface GetDestinationCityPageRefsSchema
extends z.input<typeof destinationCityPageRefsSchema> {}