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:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.container {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.cityList {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
36
components/ContentType/DestinationPage/CityListing/index.tsx
Normal file
36
components/ContentType/DestinationPage/CityListing/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.experienceList {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
gap: var(--Spacing-x1);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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":
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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> {}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user