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 {
|
.pageContainer {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
max-width: var(--max-width);
|
max-width: var(--max-width);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,23 @@
|
|||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
|
|
||||||
import { env } from "@/env/server"
|
import { env } from "@/env/server"
|
||||||
import { getDestinationOverviewPage } from "@/lib/trpc/memoizedRequests"
|
import {
|
||||||
|
getDestinationOverviewPage,
|
||||||
|
getDestinationsList,
|
||||||
|
} from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import TrackingSDK from "@/components/TrackingSDK"
|
import TrackingSDK from "@/components/TrackingSDK"
|
||||||
|
|
||||||
|
import HotelsSection from "./HotelsSection"
|
||||||
import OverviewMapContainer from "./OverviewMapContainer"
|
import OverviewMapContainer from "./OverviewMapContainer"
|
||||||
|
|
||||||
import styles from "./destinationOverviewPage.module.css"
|
import styles from "./destinationOverviewPage.module.css"
|
||||||
|
|
||||||
export default async function DestinationOverviewPage() {
|
export default async function DestinationOverviewPage() {
|
||||||
const pageData = await getDestinationOverviewPage()
|
const [pageData, destinationsData] = await Promise.all([
|
||||||
|
getDestinationOverviewPage(),
|
||||||
|
getDestinationsList(),
|
||||||
|
])
|
||||||
|
|
||||||
if (!pageData) {
|
if (!pageData) {
|
||||||
return null
|
return null
|
||||||
@@ -27,7 +34,7 @@ export default async function DestinationOverviewPage() {
|
|||||||
{googleMapsApiKey ? (
|
{googleMapsApiKey ? (
|
||||||
<OverviewMapContainer apiKey={googleMapsApiKey} mapId={googleMapId} />
|
<OverviewMapContainer apiKey={googleMapsApiKey} mapId={googleMapId} />
|
||||||
) : null}
|
) : null}
|
||||||
<h1>Destination Overview Page</h1>
|
{destinationsData && <HotelsSection destinations={destinationsData} />}
|
||||||
</div>
|
</div>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<TrackingSDK pageData={tracking} />
|
<TrackingSDK pageData={tracking} />
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ export interface AccordionItemProps
|
|||||||
title: string
|
title: string
|
||||||
icon?: IconName
|
icon?: IconName
|
||||||
trackingId?: string
|
trackingId?: string
|
||||||
|
subtitle?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export default function AccordionItem({
|
|||||||
variant,
|
variant,
|
||||||
className,
|
className,
|
||||||
trackingId,
|
trackingId,
|
||||||
|
subtitle,
|
||||||
}: AccordionItemProps) {
|
}: AccordionItemProps) {
|
||||||
const contentRef = useRef<HTMLDivElement>(null)
|
const contentRef = useRef<HTMLDivElement>(null)
|
||||||
const detailsRef = useRef<HTMLDetailsElement>(null)
|
const detailsRef = useRef<HTMLDetailsElement>(null)
|
||||||
@@ -55,9 +56,7 @@ export default function AccordionItem({
|
|||||||
<li className={accordionItemVariants({ className, variant, theme })}>
|
<li className={accordionItemVariants({ className, variant, theme })}>
|
||||||
<details ref={detailsRef} onToggle={toggleAccordion}>
|
<details ref={detailsRef} onToggle={toggleAccordion}>
|
||||||
<summary className={styles.summary}>
|
<summary className={styles.summary}>
|
||||||
{IconComp && (
|
{IconComp && <IconComp color="baseTextHighcontrast" />}
|
||||||
<IconComp className={styles.icon} color="baseTextHighcontrast" />
|
|
||||||
)}
|
|
||||||
{variant === "sidepeek" ? (
|
{variant === "sidepeek" ? (
|
||||||
<Subtitle
|
<Subtitle
|
||||||
className={styles.title}
|
className={styles.title}
|
||||||
@@ -67,13 +66,18 @@ export default function AccordionItem({
|
|||||||
{title}
|
{title}
|
||||||
</Subtitle>
|
</Subtitle>
|
||||||
) : (
|
) : (
|
||||||
<Body
|
<div className={styles.title}>
|
||||||
textTransform="bold"
|
{subtitle ? (
|
||||||
color="baseTextHighContrast"
|
<Subtitle type="two" color="baseTextHighContrast">
|
||||||
className={styles.title}
|
{title}
|
||||||
>
|
</Subtitle>
|
||||||
{title}
|
) : (
|
||||||
</Body>
|
<Body textTransform="bold" color="baseTextHighContrast">
|
||||||
|
{title}
|
||||||
|
</Body>
|
||||||
|
)}
|
||||||
|
{subtitle && <Body color="baseTextHighContrast">{subtitle}</Body>}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<ChevronDownIcon
|
<ChevronDownIcon
|
||||||
className={styles.chevron}
|
className={styles.chevron}
|
||||||
|
|||||||
@@ -154,6 +154,7 @@
|
|||||||
"Events that make an impression": "Events that make an impression",
|
"Events that make an impression": "Events that make an impression",
|
||||||
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
||||||
"Explore all levels and benefits": "Udforsk alle niveauer og fordele",
|
"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 nearby": "Udforsk i nærheden",
|
||||||
"Explore {city}": "Udforsk {city}",
|
"Explore {city}": "Udforsk {city}",
|
||||||
"Extra bed (child) × {count}": "Ekstra seng (barn) × {count}",
|
"Extra bed (child) × {count}": "Ekstra seng (barn) × {count}",
|
||||||
@@ -396,6 +397,7 @@
|
|||||||
"Search": "Søge",
|
"Search": "Søge",
|
||||||
"See all FAQ": "Se alle FAQ",
|
"See all FAQ": "Se alle FAQ",
|
||||||
"See all photos": "Se alle billeder",
|
"See all photos": "Se alle billeder",
|
||||||
|
"See destination": "Se destination",
|
||||||
"See details": "Se detaljer",
|
"See details": "Se detaljer",
|
||||||
"See hotel details": "Se hoteloplysninger",
|
"See hotel details": "Se hoteloplysninger",
|
||||||
"See less FAQ": "Se mindre FAQ",
|
"See less FAQ": "Se mindre FAQ",
|
||||||
|
|||||||
@@ -153,6 +153,7 @@
|
|||||||
"Events that make an impression": "Events that make an impression",
|
"Events that make an impression": "Events that make an impression",
|
||||||
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
||||||
"Explore all levels and benefits": "Entdecken Sie alle Levels und Vorteile",
|
"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 nearby": "Erkunden Sie die Umgebung",
|
||||||
"Explore {city}": "Erkunden Sie {city}",
|
"Explore {city}": "Erkunden Sie {city}",
|
||||||
"Extra bed (child) × {count}": "Ekstra seng (Kind) × {count}",
|
"Extra bed (child) × {count}": "Ekstra seng (Kind) × {count}",
|
||||||
@@ -395,6 +396,7 @@
|
|||||||
"Search": "Suchen",
|
"Search": "Suchen",
|
||||||
"See all FAQ": "Siehe alle FAQ",
|
"See all FAQ": "Siehe alle FAQ",
|
||||||
"See all photos": "Alle Fotos ansehen",
|
"See all photos": "Alle Fotos ansehen",
|
||||||
|
"See destination": "Siehe Ziel",
|
||||||
"See details": "Siehe Einzelheiten",
|
"See details": "Siehe Einzelheiten",
|
||||||
"See hotel details": "Hotelinformationen ansehen",
|
"See hotel details": "Hotelinformationen ansehen",
|
||||||
"See less FAQ": "Weniger anzeigen FAQ",
|
"See less FAQ": "Weniger anzeigen FAQ",
|
||||||
|
|||||||
@@ -166,6 +166,7 @@
|
|||||||
"Events that make an impression": "Events that make an impression",
|
"Events that make an impression": "Events that make an impression",
|
||||||
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
||||||
"Explore all levels and benefits": "Explore all levels and benefits",
|
"Explore all levels and benefits": "Explore all levels and benefits",
|
||||||
|
"Explore all our hotels": "Explore all our hotels",
|
||||||
"Explore nearby": "Explore nearby",
|
"Explore nearby": "Explore nearby",
|
||||||
"Explore {city}": "Explore {city}",
|
"Explore {city}": "Explore {city}",
|
||||||
"Extra bed (child) × {count}": "Extra bed (child) × {count}",
|
"Extra bed (child) × {count}": "Extra bed (child) × {count}",
|
||||||
@@ -438,6 +439,7 @@
|
|||||||
"See all FAQ": "See all FAQ",
|
"See all FAQ": "See all FAQ",
|
||||||
"See all photos": "See all photos",
|
"See all photos": "See all photos",
|
||||||
"See alternative hotels": "See alternative hotels",
|
"See alternative hotels": "See alternative hotels",
|
||||||
|
"See destination": "See destination",
|
||||||
"See details": "See details",
|
"See details": "See details",
|
||||||
"See hotel details": "See hotel details",
|
"See hotel details": "See hotel details",
|
||||||
"See less FAQ": "See less FAQ",
|
"See less FAQ": "See less FAQ",
|
||||||
|
|||||||
@@ -154,6 +154,7 @@
|
|||||||
"Events that make an impression": "Events that make an impression",
|
"Events that make an impression": "Events that make an impression",
|
||||||
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
||||||
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
"Explore all levels and benefits": "Tutustu kaikkiin tasoihin ja etuihin",
|
||||||
|
"Explore all our hotels": "Tutustu kaikkiin hotelleihimme",
|
||||||
"Explore nearby": "Tutustu lähialueeseen",
|
"Explore nearby": "Tutustu lähialueeseen",
|
||||||
"Explore {city}": "Tutustu {city}",
|
"Explore {city}": "Tutustu {city}",
|
||||||
"Extra bed (child) × {count}": "Lisävuode (lasta) × {count}",
|
"Extra bed (child) × {count}": "Lisävuode (lasta) × {count}",
|
||||||
@@ -398,6 +399,7 @@
|
|||||||
"Search": "Haku",
|
"Search": "Haku",
|
||||||
"See all FAQ": "Katso kaikki UKK",
|
"See all FAQ": "Katso kaikki UKK",
|
||||||
"See all photos": "Katso kaikki kuvat",
|
"See all photos": "Katso kaikki kuvat",
|
||||||
|
"See destination": "Katso kohde",
|
||||||
"See details": "Katso tiedot",
|
"See details": "Katso tiedot",
|
||||||
"See hotel details": "Katso hotellin tiedot",
|
"See hotel details": "Katso hotellin tiedot",
|
||||||
"See less FAQ": "Katso vähemmän UKK",
|
"See less FAQ": "Katso vähemmän UKK",
|
||||||
|
|||||||
@@ -153,6 +153,7 @@
|
|||||||
"Events that make an impression": "Events that make an impression",
|
"Events that make an impression": "Events that make an impression",
|
||||||
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
||||||
"Explore all levels and benefits": "Utforsk alle nivåer og fordeler",
|
"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 nearby": "Utforsk i nærheten",
|
||||||
"Explore {city}": "Utforsk {city}",
|
"Explore {city}": "Utforsk {city}",
|
||||||
"Extra bed (child) × {count}": "Ekstra seng (barn) × {count}",
|
"Extra bed (child) × {count}": "Ekstra seng (barn) × {count}",
|
||||||
@@ -396,6 +397,7 @@
|
|||||||
"Search": "Søk",
|
"Search": "Søk",
|
||||||
"See all FAQ": "Se alle FAQ",
|
"See all FAQ": "Se alle FAQ",
|
||||||
"See all photos": "Se alle bilder",
|
"See all photos": "Se alle bilder",
|
||||||
|
"See destination": "Se destinasjon",
|
||||||
"See details": "Se detaljer",
|
"See details": "Se detaljer",
|
||||||
"See hotel details": "Se hotellinformasjon",
|
"See hotel details": "Se hotellinformasjon",
|
||||||
"See less FAQ": "Se mindre FAQ",
|
"See less FAQ": "Se mindre FAQ",
|
||||||
|
|||||||
@@ -153,6 +153,7 @@
|
|||||||
"Events that make an impression": "Events that make an impression",
|
"Events that make an impression": "Events that make an impression",
|
||||||
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
"Expiration Date: {expirationDate}": "Expiration Date: {expirationDate}",
|
||||||
"Explore all levels and benefits": "Utforska alla nivåer och fördelar",
|
"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 nearby": "Utforska i närheten",
|
||||||
"Explore {city}": "Utforska {city}",
|
"Explore {city}": "Utforska {city}",
|
||||||
"Extra bed (child) × {count}": "Extra säng (barn) × {count}",
|
"Extra bed (child) × {count}": "Extra säng (barn) × {count}",
|
||||||
@@ -396,6 +397,7 @@
|
|||||||
"Search": "Sök",
|
"Search": "Sök",
|
||||||
"See all FAQ": "Se alla FAQ",
|
"See all FAQ": "Se alla FAQ",
|
||||||
"See all photos": "Se alla foton",
|
"See all photos": "Se alla foton",
|
||||||
|
"See destination": "Se destination",
|
||||||
"See details": "Se detaljer",
|
"See details": "Se detaljer",
|
||||||
"See hotel details": "Se hotellinformation",
|
"See hotel details": "Se hotellinformation",
|
||||||
"See less FAQ": "See färre FAQ",
|
"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!) {
|
query GetDaDeEnUrlsDestinationOverviewPage($uid: String!) {
|
||||||
de: destination_overview_page(locale: "de", uid: $uid) {
|
de: destination_overview_page(locale: "de", uid: $uid) {
|
||||||
url
|
url
|
||||||
|
|||||||
@@ -188,6 +188,11 @@ export const getDestinationOverviewPage = cache(
|
|||||||
return serverClient().contentstack.destinationOverviewPage.get()
|
return serverClient().contentstack.destinationOverviewPage.get()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
export const getDestinationsList = cache(
|
||||||
|
async function getMemoizedDestinationsList() {
|
||||||
|
return serverClient().contentstack.destinationOverviewPage.destinations.get()
|
||||||
|
}
|
||||||
|
)
|
||||||
export const getDestinationCountryPage = cache(
|
export const getDestinationCountryPage = cache(
|
||||||
async function getMemoizedDestinationCountryPage() {
|
async function getMemoizedDestinationCountryPage() {
|
||||||
return serverClient().contentstack.destinationCountryPage.get()
|
return serverClient().contentstack.destinationCountryPage.get()
|
||||||
|
|||||||
@@ -172,14 +172,17 @@ export const destinationCountryPageQueryRouter = router({
|
|||||||
const selectedCountry =
|
const selectedCountry =
|
||||||
validatedResponse.data.destinationCountryPage.destination_settings.country
|
validatedResponse.data.destinationCountryPage.destination_settings.country
|
||||||
const apiCountry = ApiCountry[lang][selectedCountry]
|
const apiCountry = ApiCountry[lang][selectedCountry]
|
||||||
const cities = await getCitiesByCountry([apiCountry], options, params, lang)
|
const cities = await getCitiesByCountry(
|
||||||
|
[apiCountry],
|
||||||
const publishedCities = cities[apiCountry].filter(
|
options,
|
||||||
(city) => city.isPublished
|
params,
|
||||||
|
lang,
|
||||||
|
true,
|
||||||
|
"destinationCountryPage"
|
||||||
)
|
)
|
||||||
|
|
||||||
const cityPages = await Promise.all(
|
const cityPages = await Promise.all(
|
||||||
publishedCities.map(async (city) => {
|
cities[apiCountry].map(async (city) => {
|
||||||
if (!city.cityIdentifier) {
|
if (!city.cityIdentifier) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { removeMultipleSlashes } from "@/utils/url"
|
||||||
|
|
||||||
import { systemSchema } from "../schemas/system"
|
import { systemSchema } from "../schemas/system"
|
||||||
|
|
||||||
export const destinationOverviewPageSchema = z.object({
|
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 */
|
/** REFS */
|
||||||
export const destinationOverviewPageRefsSchema = z.object({
|
export const destinationOverviewPageRefsSchema = z.object({
|
||||||
destination_overview_page: z.object({
|
destination_overview_page: z.object({
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
|
import { env } from "@/env/server"
|
||||||
import {
|
import {
|
||||||
GetDestinationOverviewPage,
|
GetDestinationOverviewPage,
|
||||||
GetDestinationOverviewPageRefs,
|
GetDestinationOverviewPageRefs,
|
||||||
} from "@/lib/graphql/Query/DestinationOverviewPage/DestinationOverviewPage.graphql"
|
} from "@/lib/graphql/Query/DestinationOverviewPage/DestinationOverviewPage.graphql"
|
||||||
import { request } from "@/lib/graphql/request"
|
import { request } from "@/lib/graphql/request"
|
||||||
import { notFound } from "@/server/errors/trpc"
|
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 { generateTag } from "@/utils/generateTag"
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCitiesByCountry,
|
||||||
|
getCountries,
|
||||||
|
getHotelIdsByCityId,
|
||||||
|
} from "../../hotels/utils"
|
||||||
|
import { getCityListDataByCityIdentifier } from "../destinationCountryPage/utils"
|
||||||
import {
|
import {
|
||||||
destinationOverviewPageRefsSchema,
|
destinationOverviewPageRefsSchema,
|
||||||
destinationOverviewPageSchema,
|
destinationOverviewPageSchema,
|
||||||
@@ -20,11 +32,14 @@ import {
|
|||||||
getDestinationOverviewPageRefsSuccessCounter,
|
getDestinationOverviewPageRefsSuccessCounter,
|
||||||
getDestinationOverviewPageSuccessCounter,
|
getDestinationOverviewPageSuccessCounter,
|
||||||
} from "./telemetry"
|
} from "./telemetry"
|
||||||
|
import { getCountryPageUrl } from "./utils"
|
||||||
|
|
||||||
|
import type { DestinationsData } from "@/types/components/destinationOverviewPage/destinationsList/destinationsData"
|
||||||
import {
|
import {
|
||||||
TrackingChannelEnum,
|
TrackingChannelEnum,
|
||||||
type TrackingSDKPageData,
|
type TrackingSDKPageData,
|
||||||
} from "@/types/components/tracking"
|
} from "@/types/components/tracking"
|
||||||
|
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||||
import type {
|
import type {
|
||||||
GetDestinationOverviewPageData,
|
GetDestinationOverviewPageData,
|
||||||
GetDestinationOverviewPageRefsSchema,
|
GetDestinationOverviewPageRefsSchema,
|
||||||
@@ -187,4 +202,90 @@ export const destinationOverviewPageQueryRouter = router({
|
|||||||
tracking,
|
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(
|
export const getDestinationOverviewPageFailCounter = meter.createCounter(
|
||||||
"trpc.contentstack.destinationOverviewPage.get-fail"
|
"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,
|
getHotelIdsByCityId,
|
||||||
getHotelIdsByCountry,
|
getHotelIdsByCountry,
|
||||||
getLocations,
|
getLocations,
|
||||||
TWENTYFOUR_HOURS,
|
|
||||||
} from "./utils"
|
} from "./utils"
|
||||||
|
|
||||||
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
import type { BedTypeSelection } from "@/types/components/hotelReservation/enterDetails/bedType"
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export async function getCitiesByCountry(
|
|||||||
options: RequestOptionsWithOutBody,
|
options: RequestOptionsWithOutBody,
|
||||||
params: URLSearchParams,
|
params: URLSearchParams,
|
||||||
lang: Lang,
|
lang: Lang,
|
||||||
|
onlyPublished = false, // false by default as it might be used in other places
|
||||||
affix: string = locationsAffix
|
affix: string = locationsAffix
|
||||||
) {
|
) {
|
||||||
return unstable_cache(
|
return unstable_cache(
|
||||||
@@ -155,7 +156,10 @@ export async function getCitiesByCountry(
|
|||||||
return null
|
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
|
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 { z } from "zod"
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
countryPageUrlSchema,
|
||||||
destinationOverviewPageRefsSchema,
|
destinationOverviewPageRefsSchema,
|
||||||
destinationOverviewPageSchema,
|
destinationOverviewPageSchema,
|
||||||
} from "@/server/routers/contentstack/destinationOverviewPage/output"
|
} from "@/server/routers/contentstack/destinationOverviewPage/output"
|
||||||
@@ -15,3 +16,6 @@ export interface GetDestinationOverviewPageRefsSchema
|
|||||||
|
|
||||||
export interface DestinationOverviewPageRefs
|
export interface DestinationOverviewPageRefs
|
||||||
extends z.output<typeof destinationOverviewPageRefsSchema> {}
|
extends z.output<typeof destinationOverviewPageRefsSchema> {}
|
||||||
|
|
||||||
|
export interface GetCountryPageUrlData
|
||||||
|
extends z.input<typeof countryPageUrlSchema> {}
|
||||||
|
|||||||
Reference in New Issue
Block a user