fix: improve loading on destination overview page
- Only load data from Contentstack - Use static JSON for destination list - Some logic improvements to data handling and types
This commit is contained in:
@@ -12,7 +12,6 @@ import DestinationCityPage from "@/components/ContentType/DestinationPage/Destin
|
||||
import DestinationCityPageSkeleton from "@/components/ContentType/DestinationPage/DestinationCityPage/DestinationCityPageSkeleton"
|
||||
import DestinationCountryPage from "@/components/ContentType/DestinationPage/DestinationCountryPage"
|
||||
import DestinationCountryPageSkeleton from "@/components/ContentType/DestinationPage/DestinationCountryPage/DestinationCountryPageSkeleton"
|
||||
import DestinationOverviewPage from "@/components/ContentType/DestinationPage/DestinationOverviewPage"
|
||||
import HotelPage from "@/components/ContentType/HotelPage"
|
||||
import HotelSubpage from "@/components/ContentType/HotelSubpage"
|
||||
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
|
||||
@@ -23,8 +22,8 @@ import { getLang } from "@/i18n/serverContext"
|
||||
import { isValidSession } from "@/utils/session"
|
||||
|
||||
import type {
|
||||
ContentTypeParams,
|
||||
LangParams,
|
||||
NonAppRouterContentTypeParams,
|
||||
PageArgs,
|
||||
UIDParams,
|
||||
} from "@/types/params"
|
||||
@@ -36,7 +35,7 @@ export default async function ContentTypePage({
|
||||
params,
|
||||
searchParams,
|
||||
}: PageArgs<
|
||||
LangParams & ContentTypeParams & UIDParams,
|
||||
LangParams & NonAppRouterContentTypeParams & UIDParams,
|
||||
{ subpage?: string; filterFromUrl?: string }
|
||||
>) {
|
||||
const pathname = headers().get("x-pathname") || ""
|
||||
@@ -62,11 +61,6 @@ export default async function ContentTypePage({
|
||||
}
|
||||
case PageContentTypeEnum.loyaltyPage:
|
||||
return <LoyaltyPage />
|
||||
case PageContentTypeEnum.destinationOverviewPage:
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
return notFound()
|
||||
}
|
||||
return <DestinationOverviewPage />
|
||||
case PageContentTypeEnum.destinationCountryPage:
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
return notFound()
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export default function DestinationOverviewPageBreadcrumbs() {
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "../../../[contentType]/[uid]/@preview/loading"
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "../../../[contentType]/[uid]/@preview/page"
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client"
|
||||
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
import { useEffect } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { DestinationOverviewPageError } from "@/components/ContentType/DestinationPage/DestinationOverviewPage/error"
|
||||
|
||||
export default function Error({
|
||||
error,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
}) {
|
||||
const intl = useIntl()
|
||||
|
||||
useEffect(() => {
|
||||
if (!error) return
|
||||
|
||||
console.error(error)
|
||||
Sentry.captureException(error)
|
||||
}, [error])
|
||||
|
||||
return (
|
||||
<DestinationOverviewPageError>
|
||||
<p>
|
||||
<strong>
|
||||
{intl.formatMessage(
|
||||
{ id: "An error occurred ({errorId})" },
|
||||
{
|
||||
errorId: `${error.digest}@${Date.now()}`,
|
||||
}
|
||||
)}
|
||||
</strong>
|
||||
</p>
|
||||
</DestinationOverviewPageError>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "../../[contentType]/[uid]/layout"
|
||||
@@ -0,0 +1 @@
|
||||
export { DestinationOverviewPageLoading as default } from "@/components/ContentType/DestinationPage/DestinationOverviewPage"
|
||||
@@ -0,0 +1,15 @@
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import DestinationOverviewPage from "@/components/ContentType/DestinationPage/DestinationOverviewPage"
|
||||
|
||||
export { generateMetadata } from "@/utils/generateMetadata"
|
||||
|
||||
export default function DestinationOverviewPagePage() {
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
return notFound()
|
||||
}
|
||||
|
||||
return <DestinationOverviewPage />
|
||||
}
|
||||
@@ -27,21 +27,23 @@ export default async function Destination({
|
||||
<AccordionItem title={country} subtitle={accordionSubtitle}>
|
||||
<div className={styles.container}>
|
||||
<ul className={styles.citiesList}>
|
||||
{cities.map((city) => (
|
||||
<li key={city.id}>
|
||||
{city.url ? (
|
||||
<Link
|
||||
href={city.url}
|
||||
color="baseTextMediumContrast"
|
||||
textDecoration="underline"
|
||||
>
|
||||
{`${city.name} (${city.hotelCount})`}
|
||||
</Link>
|
||||
) : (
|
||||
<Body>{`${city.name} (${city.hotelCount})`}</Body>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
{cities.map((city) =>
|
||||
city.hotelCount > 0 ? (
|
||||
<li key={city.id}>
|
||||
{city.url ? (
|
||||
<Link
|
||||
href={city.url}
|
||||
color="baseTextMediumContrast"
|
||||
textDecoration="underline"
|
||||
>
|
||||
{`${city.name} (${city.hotelCount})`}
|
||||
</Link>
|
||||
) : (
|
||||
<Body>{`${city.name} (${city.hotelCount})`}</Body>
|
||||
)}
|
||||
</li>
|
||||
) : null
|
||||
)}
|
||||
</ul>
|
||||
{countryUrl && (
|
||||
<Link href={countryUrl} variant="icon" color="burgundy" weight="bold">
|
||||
|
||||
@@ -7,9 +7,7 @@ import styles from "./destinationsList.module.css"
|
||||
|
||||
import type { DestinationsListProps } from "@/types/components/destinationOverviewPage/destinationsList/destinationsData"
|
||||
|
||||
export default function DestinationsList({
|
||||
destinations,
|
||||
}: DestinationsListProps) {
|
||||
export function DestinationsList({ destinations }: DestinationsListProps) {
|
||||
const middleIndex = Math.ceil(destinations.length / 2)
|
||||
const accordionLeft = destinations.slice(0, middleIndex)
|
||||
const accordionRight = destinations.slice(middleIndex)
|
||||
@@ -17,27 +15,31 @@ export default function DestinationsList({
|
||||
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}
|
||||
/>
|
||||
))}
|
||||
{accordionLeft.map((data) =>
|
||||
data.numberOfHotels > 0 ? (
|
||||
<Destination
|
||||
key={data.country}
|
||||
country={data.country}
|
||||
countryUrl={data.countryUrl}
|
||||
numberOfHotels={data.numberOfHotels}
|
||||
cities={data.cities}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</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}
|
||||
/>
|
||||
))}
|
||||
{accordionRight.map((data) =>
|
||||
data.numberOfHotels > 0 ? (
|
||||
<Destination
|
||||
key={data.country}
|
||||
country={data.country}
|
||||
countryUrl={data.countryUrl}
|
||||
numberOfHotels={data.numberOfHotels}
|
||||
cities={data.cities}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
</Accordion>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import { getDestinationsList } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { getIntl } from "@/i18n"
|
||||
|
||||
import DestinationsList from "./DestinationsList"
|
||||
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) {
|
||||
export default async function HotelsSection() {
|
||||
const intl = await getIntl()
|
||||
const destinations = await getDestinationsList()
|
||||
|
||||
if (destinations.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={styles.container}>
|
||||
|
||||
@@ -6,17 +6,8 @@ import MapContent from "../../Map/MapContent"
|
||||
import MapProvider from "../../Map/MapProvider"
|
||||
import { getHotelMapMarkers, mapMarkerDataToGeoJson } from "../../Map/utils"
|
||||
import ActiveMapCard from "./ActiveMapCard"
|
||||
import InputForm from "./InputForm"
|
||||
|
||||
import type { MapLocation } from "@/types/components/mapLocation"
|
||||
|
||||
interface OverviewMapContainerProps {
|
||||
defaultLocation: MapLocation
|
||||
}
|
||||
|
||||
export default async function OverviewMapContainer({
|
||||
defaultLocation,
|
||||
}: OverviewMapContainerProps) {
|
||||
export default async function OverviewMapContainer() {
|
||||
const hotelData = await getAllHotels()
|
||||
|
||||
if (!hotelData) {
|
||||
@@ -28,22 +19,13 @@ export default async function OverviewMapContainer({
|
||||
|
||||
const markers = getHotelMapMarkers(hotelData)
|
||||
const geoJson = mapMarkerDataToGeoJson(markers)
|
||||
const defaultCoordinates = defaultLocation
|
||||
? {
|
||||
lat: defaultLocation.latitude,
|
||||
lng: defaultLocation.longitude,
|
||||
}
|
||||
: null
|
||||
const defaultZoom = defaultLocation?.default_zoom ?? 3
|
||||
|
||||
return (
|
||||
<MapProvider apiKey={googleMapsApiKey} pageType="overview">
|
||||
<InputForm />
|
||||
<DynamicMap
|
||||
mapId={googleMapId}
|
||||
markers={markers}
|
||||
defaultCoordinates={defaultCoordinates}
|
||||
defaultZoom={defaultZoom}
|
||||
fitBounds
|
||||
gestureHandling="cooperative"
|
||||
>
|
||||
<MapContent geojson={geoJson} />
|
||||
|
||||
@@ -2,11 +2,21 @@
|
||||
position: relative;
|
||||
display: grid;
|
||||
width: 100%;
|
||||
max-width: var(--max-width);
|
||||
height: 700px;
|
||||
height: 610px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.mapContainer {
|
||||
height: 580px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1367px) {
|
||||
.mapContainer {
|
||||
height: 560px;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x9);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import styles from "./destinationOverviewPage.module.css"
|
||||
|
||||
import type { PropsWithChildren } from "react"
|
||||
|
||||
export function DestinationOverviewPageError({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<div className={styles.blocks}>{children}</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import {
|
||||
getDestinationOverviewPage,
|
||||
getDestinationsList,
|
||||
} from "@/lib/trpc/memoizedRequests"
|
||||
import { getDestinationOverviewPage } from "@/lib/trpc/memoizedRequests"
|
||||
|
||||
import Blocks from "@/components/Blocks"
|
||||
import SkeletonShimmer from "@/components/SkeletonShimmer"
|
||||
import TrackingSDK from "@/components/TrackingSDK"
|
||||
|
||||
import HotelsSection from "./HotelsSection"
|
||||
@@ -12,10 +10,7 @@ import OverviewMapContainer from "./OverviewMapContainer"
|
||||
import styles from "./destinationOverviewPage.module.css"
|
||||
|
||||
export default async function DestinationOverviewPage() {
|
||||
const [pageData, destinationsData] = await Promise.all([
|
||||
getDestinationOverviewPage(),
|
||||
getDestinationsList(),
|
||||
])
|
||||
const pageData = await getDestinationOverviewPage()
|
||||
|
||||
if (!pageData) {
|
||||
return null
|
||||
@@ -26,21 +21,35 @@ export default async function DestinationOverviewPage() {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.mapContainer}>
|
||||
<OverviewMapContainer
|
||||
defaultLocation={destinationOverviewPage.location}
|
||||
/>
|
||||
<OverviewMapContainer />
|
||||
</div>
|
||||
<main className={styles.main}>
|
||||
<div className={styles.blocks}>
|
||||
<Blocks blocks={destinationOverviewPage.blocks} />
|
||||
</div>
|
||||
</main>
|
||||
{destinationsData && (
|
||||
<aside className={styles.hotelsAccordions}>
|
||||
<HotelsSection destinations={destinationsData} />
|
||||
</aside>
|
||||
)}
|
||||
<aside className={styles.hotelsAccordions}>
|
||||
<HotelsSection />
|
||||
</aside>
|
||||
<TrackingSDK pageData={tracking} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function DestinationOverviewPageLoading() {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.mapContainer}>
|
||||
<SkeletonShimmer width={"100%"} height={"100%"} />
|
||||
</div>
|
||||
<main className={styles.main}>
|
||||
<div className={styles.blocks}>
|
||||
<SkeletonShimmer width={"100%"} height={"100%"} />
|
||||
</div>
|
||||
</main>
|
||||
<aside className={styles.hotelsAccordions}>
|
||||
<SkeletonShimmer width={"100%"} height={"100%"} />
|
||||
</aside>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.mapWrapper::after {
|
||||
.mapWrapperWithCloseButton:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import "client-only"
|
||||
|
||||
import { Map, type MapProps, useMap } from "@vis.gl/react-google-maps"
|
||||
import { cx } from "class-variance-authority"
|
||||
import { type PropsWithChildren, useEffect, useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
@@ -28,8 +29,8 @@ const BACKUP_COORDINATES = {
|
||||
interface DynamicMapProps {
|
||||
markers: DestinationMarker[]
|
||||
mapId: string
|
||||
defaultCoordinates: google.maps.LatLngLiteral | null
|
||||
defaultZoom: number
|
||||
defaultCenter?: google.maps.LatLngLiteral
|
||||
defaultZoom?: number
|
||||
fitBounds?: boolean
|
||||
gestureHandling?: "greedy" | "cooperative" | "auto" | "none"
|
||||
onClose?: () => void
|
||||
@@ -38,8 +39,8 @@ interface DynamicMapProps {
|
||||
export default function DynamicMap({
|
||||
markers,
|
||||
mapId,
|
||||
defaultCoordinates,
|
||||
defaultZoom,
|
||||
defaultCenter = BACKUP_COORDINATES,
|
||||
defaultZoom = 3,
|
||||
fitBounds = true,
|
||||
onClose,
|
||||
gestureHandling = "auto",
|
||||
@@ -61,18 +62,14 @@ export default function DynamicMap({
|
||||
}, [activeMarker, pageType])
|
||||
|
||||
useEffect(() => {
|
||||
if (map && fitBounds) {
|
||||
if (markers.length) {
|
||||
const bounds = new google.maps.LatLngBounds()
|
||||
markers.forEach((marker) => {
|
||||
bounds.extend(marker.coordinates)
|
||||
})
|
||||
map.fitBounds(bounds, 100)
|
||||
} else if (defaultCoordinates) {
|
||||
map.setCenter(defaultCoordinates)
|
||||
}
|
||||
if (map && fitBounds && markers?.length) {
|
||||
const bounds = new google.maps.LatLngBounds()
|
||||
markers.forEach((marker) => {
|
||||
bounds.extend(marker.coordinates)
|
||||
})
|
||||
map.fitBounds(bounds, 100)
|
||||
}
|
||||
}, [map, markers, defaultCoordinates, fitBounds])
|
||||
}, [map, fitBounds, markers])
|
||||
|
||||
useHandleKeyUp((event: KeyboardEvent) => {
|
||||
if (event.key === "Escape" && onClose) {
|
||||
@@ -94,7 +91,7 @@ export default function DynamicMap({
|
||||
}
|
||||
|
||||
const mapOptions: MapProps = {
|
||||
defaultCenter: defaultCoordinates || BACKUP_COORDINATES, // Default center will be overridden by the bounds
|
||||
defaultCenter, // Default center will be overridden by the bounds
|
||||
minZoom: 3,
|
||||
maxZoom: 18,
|
||||
defaultZoom,
|
||||
@@ -105,7 +102,13 @@ export default function DynamicMap({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.mapWrapper} ref={ref}>
|
||||
<div
|
||||
className={cx(
|
||||
styles.mapWrapper,
|
||||
onClose && styles.mapWrapperWithCloseButton
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<ErrorBoundary fallback={<h2>Unable to display map</h2>}>
|
||||
<Map {...mapOptions}>{children}</Map>
|
||||
</ErrorBoundary>
|
||||
|
||||
@@ -71,7 +71,7 @@ export default function Map({
|
||||
|
||||
const markers = getHotelMapMarkers(hotels)
|
||||
const geoJson = mapMarkerDataToGeoJson(markers)
|
||||
const defaultCoordinates = activeHotel
|
||||
const defaultCenter = activeHotel
|
||||
? {
|
||||
lat: activeHotel.hotel.location.latitude,
|
||||
lng: activeHotel.hotel.location.longitude,
|
||||
@@ -81,7 +81,7 @@ export default function Map({
|
||||
lat: defaultLocation.latitude,
|
||||
lng: defaultLocation.longitude,
|
||||
}
|
||||
: null
|
||||
: undefined
|
||||
const defaultZoom = activeHotel
|
||||
? 15
|
||||
: (defaultLocation?.default_zoom ?? (pageType === "city" ? 10 : 3))
|
||||
@@ -171,7 +171,7 @@ export default function Map({
|
||||
markers={markers}
|
||||
mapId={mapId}
|
||||
onClose={handleClose}
|
||||
defaultCoordinates={defaultCoordinates}
|
||||
defaultCenter={defaultCenter}
|
||||
defaultZoom={defaultZoom}
|
||||
fitBounds={!activeHotel}
|
||||
gestureHandling="greedy"
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"Amenities": "Faciliteter",
|
||||
"Amusement park": "Forlystelsespark",
|
||||
"An account with this email already exists. Please try signing in instead.": "Der findes allerede en konto med denne e-mailadresse. Log venligst ind i stedet.",
|
||||
"An error occurred ({errorId})": "Der opstod en fejl ({errorId})",
|
||||
"An error occurred trying to manage your preferences, please try again later.": "Der opstod en fejl under forsøget på at administrere dine præferencer. Prøv venligst igen senere.",
|
||||
"An error occurred when adding a credit card, please try again later.": "Der opstod en fejl under tilføjelse af et kreditkort. Prøv venligst igen senere.",
|
||||
"An error occurred when trying to update profile.": "Der opstod en fejl under forsøg på at opdatere profilen.",
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"Amenities": "Annehmlichkeiten",
|
||||
"Amusement park": "Vergnügungspark",
|
||||
"An account with this email already exists. Please try signing in instead.": "Ein Konto mit dieser E-Mail-Adresse existiert bereits. Bitte melden Sie sich stattdessen an.",
|
||||
"An error occurred ({errorId})": "Ein Fehler ist aufgetreten ({errorId})",
|
||||
"An error occurred trying to manage your preferences, please try again later.": "Beim Versuch, Ihre Einstellungen zu verwalten, ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
|
||||
"An error occurred when adding a credit card, please try again later.": "Beim Hinzufügen einer Kreditkarte ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.",
|
||||
"An error occurred when trying to update profile.": "Beim Versuch, das Profil zu aktualisieren, ist ein Fehler aufgetreten.",
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"Amenities": "Amenities",
|
||||
"Amusement park": "Amusement park",
|
||||
"An account with this email already exists. Please try signing in instead.": "An account with this email already exists. Please try signing in instead.",
|
||||
"An error occurred ({errorId})": "An error occurred ({errorId})",
|
||||
"An error occurred trying to manage your preferences, please try again later.": "An error occurred trying to manage your preferences, please try again later.",
|
||||
"An error occurred when adding a credit card, please try again later.": "An error occurred when adding a credit card, please try again later.",
|
||||
"An error occurred when trying to update profile.": "An error occurred when trying to update profile.",
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"Amenities": "Mukavuudet",
|
||||
"Amusement park": "Huvipuisto",
|
||||
"An account with this email already exists. Please try signing in instead.": "Tällä sähköpostiosoitteella on jo olemassa tili. Joten käytä sitä kirjautuaksesi sisään.",
|
||||
"An error occurred ({errorId})": "Tapahtui virhe ({errorId})",
|
||||
"An error occurred trying to manage your preferences, please try again later.": "Asetusten hallinnassa tapahtui virhe. Yritä myöhemmin uudelleen.",
|
||||
"An error occurred when adding a credit card, please try again later.": "Luottokorttia lisättäessä tapahtui virhe. Yritä myöhemmin uudelleen.",
|
||||
"An error occurred when trying to update profile.": "Profiilia päivitettäessä tapahtui virhe.",
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"Amenities": "Fasiliteter",
|
||||
"Amusement park": "Tivoli",
|
||||
"An account with this email already exists. Please try signing in instead.": "En konto med denne e-postadressen eksisterer allerede. Vennligst logg inn i stedet.",
|
||||
"An error occurred ({errorId})": "Det oppstod en feil ({errorId})",
|
||||
"An error occurred trying to manage your preferences, please try again later.": "Det oppstod en feil under forsøket på å administrere innstillingene dine. Prøv igjen senere.",
|
||||
"An error occurred when adding a credit card, please try again later.": "Det oppstod en feil ved å legge til et kredittkort. Prøv igjen senere.",
|
||||
"An error occurred when trying to update profile.": "Det oppstod en feil under forsøk på å oppdatere profilen.",
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
"Amenities": "Bekvämligheter",
|
||||
"Amusement park": "Nöjespark",
|
||||
"An account with this email already exists. Please try signing in instead.": "Ett konto med denna mailadress finns redan. Vänligen försök logga in istället.",
|
||||
"An error occurred ({errorId})": "Ett fel uppstod ({errorId})",
|
||||
"An error occurred trying to manage your preferences, please try again later.": "Ett fel uppstod när du försökte hantera dina inställningar, försök igen senare.",
|
||||
"An error occurred when adding a credit card, please try again later.": "Ett fel uppstod när ett kreditkort lades till, försök igen senare.",
|
||||
"An error occurred when trying to update profile.": "Ett fel uppstod när du försökte uppdatera profilen.",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
query GetDestinationOverviewPage($locale: String!, $uid: String!) {
|
||||
destination_overview_page(uid: $uid, locale: $locale) {
|
||||
title
|
||||
heading
|
||||
url
|
||||
blocks {
|
||||
__typename
|
||||
|
||||
@@ -181,6 +181,7 @@ export const getDestinationOverviewPage = cache(
|
||||
return serverClient().contentstack.destinationOverviewPage.get()
|
||||
}
|
||||
)
|
||||
|
||||
export const getDestinationsList = cache(
|
||||
async function getMemoizedDestinationsList() {
|
||||
return serverClient().contentstack.destinationOverviewPage.destinations.get()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@ export const blocksSchema = z.discriminatedUnion("__typename", [
|
||||
|
||||
export const destinationOverviewPageSchema = z.object({
|
||||
destination_overview_page: z.object({
|
||||
title: z.string(),
|
||||
heading: z.string().nullish(),
|
||||
blocks: discriminatedUnionArray(blocksSchema.options),
|
||||
location: mapLocationSchema,
|
||||
system: systemSchema.merge(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Lang } from "@/constants/languages"
|
||||
import {
|
||||
GetDestinationOverviewPage,
|
||||
GetDestinationOverviewPageRefs,
|
||||
@@ -20,6 +21,12 @@ import {
|
||||
} from "../../hotels/utils"
|
||||
import { getCityPageUrls } from "../destinationCityPage/utils"
|
||||
import { getCountryPageUrls } from "../destinationCountryPage/utils"
|
||||
import destinationsDataDa from "./destinations-da.json" with { assert: "json" }
|
||||
import destinationsDataDe from "./destinations-de.json" with { assert: "json" }
|
||||
import destinationsDataEn from "./destinations-en.json" with { assert: "json" }
|
||||
import destinationsDataFi from "./destinations-fi.json" with { assert: "json" }
|
||||
import destinationsDataNo from "./destinations-no.json" with { assert: "json" }
|
||||
import destinationsDataSv from "./destinations-sv.json" with { assert: "json" }
|
||||
import {
|
||||
destinationOverviewPageRefsSchema,
|
||||
destinationOverviewPageSchema,
|
||||
@@ -200,80 +207,123 @@ export const destinationOverviewPageQueryRouter = router({
|
||||
}
|
||||
}),
|
||||
destinations: router({
|
||||
get: serviceProcedure.query(async function ({ ctx }) {
|
||||
const countries = await getCountries({
|
||||
lang: ctx.lang,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
get: serviceProcedure.query(async function ({
|
||||
ctx,
|
||||
}): Promise<DestinationsData> {
|
||||
// For go live we are using static data here, as it rarely changes.
|
||||
// This also improves operational reliance as we are not hammering
|
||||
// a lot of endpoints for a lot of data.
|
||||
// Re-implement once we have better API support and established caching
|
||||
// patterns and mechanisms.
|
||||
|
||||
const countryPages = await getCountryPageUrls(ctx.lang)
|
||||
// NOTE: To update the static data set `useStaticData = false`.
|
||||
// Then go to the "Hotels & Destinations" page and visit every language.
|
||||
// At the time of commit http://localhost:3000/en/destinations.
|
||||
// This will update the JSON file locally, each page load for each language,
|
||||
// if all data loads correctly.
|
||||
// Set back `useStaticData = true` again and test with the updated JSON file.
|
||||
// Add, commit and push the updated JSON files with useStaticData = true here.
|
||||
const useStaticData = true
|
||||
|
||||
if (!countries) {
|
||||
return null
|
||||
if (useStaticData) {
|
||||
switch (ctx.lang) {
|
||||
case Lang.da:
|
||||
return destinationsDataDa
|
||||
case Lang.de:
|
||||
return destinationsDataDe
|
||||
case Lang.fi:
|
||||
return destinationsDataFi
|
||||
case Lang.en:
|
||||
return destinationsDataEn
|
||||
case Lang.no:
|
||||
return destinationsDataNo
|
||||
case Lang.sv:
|
||||
return destinationsDataSv
|
||||
default:
|
||||
return []
|
||||
}
|
||||
} else {
|
||||
return await updateJSONOnDisk()
|
||||
}
|
||||
|
||||
const countryNames = countries.data.map((country) => country.name)
|
||||
async function updateJSONOnDisk() {
|
||||
const { lang } = ctx
|
||||
|
||||
const citiesByCountry = await getCitiesByCountry({
|
||||
lang: ctx.lang,
|
||||
countries: countryNames,
|
||||
serviceToken: ctx.serviceToken,
|
||||
onlyPublished: true,
|
||||
})
|
||||
const countries = await getCountries({
|
||||
lang,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
|
||||
const cityPages = await getCityPageUrls(ctx.lang)
|
||||
if (!countries) {
|
||||
return []
|
||||
}
|
||||
|
||||
const destinations: DestinationsData = await Promise.all(
|
||||
Object.entries(citiesByCountry).map(async ([country, cities]) => {
|
||||
const citiesWithHotelCount = await Promise.all(
|
||||
cities.map(async (city) => {
|
||||
const [hotels] = await safeTry(
|
||||
getHotelIdsByCityId({
|
||||
cityId: city.id,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
)
|
||||
const countryNames = countries.data.map((country) => country.name)
|
||||
|
||||
const cityPage = cityPages.find(
|
||||
(cityPage) => cityPage.city === city.cityIdentifier
|
||||
)
|
||||
const citiesByCountry = await getCitiesByCountry({
|
||||
lang,
|
||||
countries: countryNames,
|
||||
serviceToken: ctx.serviceToken,
|
||||
onlyPublished: true,
|
||||
})
|
||||
|
||||
if (!cityPage) {
|
||||
return null
|
||||
}
|
||||
const cityPages = await getCityPageUrls(lang)
|
||||
|
||||
return {
|
||||
id: city.id,
|
||||
name: city.name,
|
||||
hotelIds: hotels || [],
|
||||
hotelCount: hotels?.length ?? 0,
|
||||
url: cityPage.url,
|
||||
}
|
||||
})
|
||||
)
|
||||
const destinations = await Promise.all(
|
||||
Object.entries(citiesByCountry).map(async ([country, cities]) => {
|
||||
const activeCitiesWithHotelCount: Cities = await Promise.all(
|
||||
cities.map(async (city) => {
|
||||
const [hotels] = await safeTry(
|
||||
getHotelIdsByCityId({
|
||||
cityId: city.id,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
)
|
||||
|
||||
const activeCitiesWithHotelCount: Cities =
|
||||
citiesWithHotelCount.filter(
|
||||
(city): city is Cities[number] => !!city
|
||||
const cityPage = cityPages.find(
|
||||
(cityPage) => cityPage.city === city.cityIdentifier
|
||||
)
|
||||
|
||||
return {
|
||||
id: city.id,
|
||||
name: city.name,
|
||||
hotelIds: hotels || [],
|
||||
hotelCount: hotels ? hotels.length : 0,
|
||||
url: cityPage?.url,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const countryPage = countryPages.find(
|
||||
(countryPage) => countryPage.country === country
|
||||
)
|
||||
const countryPages = await getCountryPageUrls(lang)
|
||||
const countryPage = countryPages.find(
|
||||
(countryPage) => countryPage.country === country
|
||||
)
|
||||
|
||||
return {
|
||||
country,
|
||||
countryUrl: countryPage?.url,
|
||||
numberOfHotels: activeCitiesWithHotelCount.reduce(
|
||||
(acc, city) => acc + city.hotelCount,
|
||||
0
|
||||
),
|
||||
cities: activeCitiesWithHotelCount,
|
||||
return {
|
||||
country,
|
||||
countryUrl: countryPage?.url,
|
||||
numberOfHotels: activeCitiesWithHotelCount.reduce(
|
||||
(acc, city) => acc + city.hotelCount,
|
||||
0
|
||||
),
|
||||
cities: activeCitiesWithHotelCount,
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const data = destinations.sort((a, b) =>
|
||||
a.country.localeCompare(b.country)
|
||||
)
|
||||
const fs = await import("node:fs")
|
||||
fs.writeFileSync(
|
||||
`./server/routers/contentstack/destinationOverviewPage/destinations-${lang}.json`,
|
||||
JSON.stringify(data),
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return destinations.sort((a, b) => a.country.localeCompare(b.country))
|
||||
)
|
||||
return data
|
||||
}
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CancellationRuleEnum } from "@/constants/booking"
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
import { dt } from "@/lib/dt"
|
||||
@@ -79,7 +80,6 @@ import type {
|
||||
} from "@/types/trpc/routers/hotel/availability"
|
||||
import type { HotelInput } from "@/types/trpc/routers/hotel/hotel"
|
||||
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
|
||||
import type { Lang } from "@/constants/routes/hotelReservation"
|
||||
|
||||
export const getHotel = cache(
|
||||
async (input: HotelInput, serviceToken: string) => {
|
||||
@@ -1198,7 +1198,9 @@ export const hotelQueryRouter = router({
|
||||
}) {
|
||||
const lang = input?.lang ?? ctx.lang
|
||||
const countries = await getCountries({
|
||||
lang: lang,
|
||||
// Countries need to be in English regardless of incoming lang because
|
||||
// we use the names as input for API endpoints.
|
||||
lang: Lang.en,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
export type DestinationsData = {
|
||||
country: string
|
||||
countryUrl: string | undefined
|
||||
numberOfHotels: number
|
||||
cities: {
|
||||
id: string
|
||||
name: string
|
||||
hotelIds: string[]
|
||||
hotelCount: number
|
||||
url: string
|
||||
}[]
|
||||
}[]
|
||||
export type City = {
|
||||
id: string
|
||||
name: string
|
||||
hotelIds: string[]
|
||||
hotelCount: number
|
||||
url?: string
|
||||
}
|
||||
|
||||
export type Cities = DestinationsData[number]["cities"]
|
||||
export type DestinationCountry = {
|
||||
country: string
|
||||
countryUrl?: string
|
||||
numberOfHotels: number
|
||||
cities: City[]
|
||||
}
|
||||
|
||||
export type DestinationsData = DestinationCountry[]
|
||||
|
||||
export type Cities = DestinationCountry["cities"]
|
||||
|
||||
export type HotelsSectionProps = {
|
||||
destinations: DestinationsData
|
||||
|
||||
@@ -29,6 +29,21 @@ export type ContentTypeParams = {
|
||||
| PageContentTypeEnum.startPage
|
||||
}
|
||||
|
||||
// This is purely for use in `apps/scandic-web/app/[lang]/(live)/(public)/[contentType]/[uid]/page.tsx`
|
||||
// It is meant as an interim solution while we move away from the switch-case
|
||||
// approach into routing these based on App router.
|
||||
// Folders in `apps/scandic-web/app/[lang]/(live)/(public)` should not be in this type.
|
||||
export type NonAppRouterContentTypeParams = {
|
||||
contentType:
|
||||
| PageContentTypeEnum.loyaltyPage
|
||||
| PageContentTypeEnum.contentPage
|
||||
| PageContentTypeEnum.hotelPage
|
||||
| PageContentTypeEnum.collectionPage
|
||||
| PageContentTypeEnum.destinationCountryPage
|
||||
| PageContentTypeEnum.destinationCityPage
|
||||
| PageContentTypeEnum.startPage
|
||||
}
|
||||
|
||||
export type ContentTypeWebviewParams = {
|
||||
contentType: "loyalty-page" | "account-page"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user