Merged in feat/SW-1751-destination-0-results (pull request #1448)

feat(SW-1751): Added alert if no results are found, also implemented default location data from Contentstack

* feat(SW-1751): Added alert if no results are found, also implemented default location data from Contentstack


Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-03-03 10:32:40 +00:00
parent c3e3fa62ec
commit 93187a9c33
31 changed files with 372 additions and 206 deletions

View File

@@ -6,6 +6,7 @@ import { useIntl } from "react-intl"
import { useDestinationDataStore } from "@/stores/destination-data"
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
import Alert from "@/components/TempDesignSystem/Alert"
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
@@ -16,6 +17,8 @@ import CityListingSkeleton from "./CityListingSkeleton"
import styles from "./cityListing.module.css"
import { AlertTypeEnum } from "@/types/enums/alert"
export default function CityListing() {
const intl = useIntl()
const scrollRef = useRef<HTMLElement>(null)
@@ -60,23 +63,35 @@ export default function CityListing() {
listType="city"
/>
</div>
<ul
className={`${styles.cityList} ${allCitiesVisible ? styles.allVisible : ""}`}
>
{activeCities.map((city) => (
<li key={city.system.uid}>
<CityListingItem city={city} />
</li>
))}
</ul>
{activeCities.length > 5 ? (
<ShowMoreButton
loadMoreData={handleShowMore}
showLess={allCitiesVisible}
{activeCities.length === 0 ? (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({ id: "No matching locations found" })}
text={intl.formatMessage({
id: "It looks like no location match your filters. Try adjusting your search to find the perfect stay.",
})}
/>
) : null}
{showBackToTop && (
<BackToTopButton position="center" onClick={scrollToTop} />
) : (
<>
<ul
className={`${styles.cityList} ${allCitiesVisible ? styles.allVisible : ""}`}
>
{activeCities.map((city) => (
<li key={city.system.uid}>
<CityListingItem city={city} />
</li>
))}
</ul>
{activeCities.length > 5 ? (
<ShowMoreButton
loadMoreData={handleShowMore}
showLess={allCitiesVisible}
/>
) : null}
{showBackToTop && (
<BackToTopButton position="center" onClick={scrollToTop} />
)}
</>
)}
</section>
)

View File

@@ -7,6 +7,7 @@ import { useIntl } from "react-intl"
import { useDestinationDataStore } from "@/stores/destination-data"
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
import Alert from "@/components/TempDesignSystem/Alert"
import Body from "@/components/TempDesignSystem/Text/Body"
import { debounce } from "@/utils/debounce"
@@ -16,6 +17,7 @@ import { getVisibleHotels } from "./utils"
import styles from "./hotelList.module.css"
import { AlertTypeEnum } from "@/types/enums/alert"
import type { HotelDataWithUrl } from "@/types/hotel"
export default function HotelList() {
@@ -71,13 +73,23 @@ export default function HotelList() {
listType="hotel"
/>
</div>
<ul className={styles.hotelList}>
{visibleHotels.map(({ hotel, url }) => (
<li key={hotel.operaId}>
<HotelListItem hotel={hotel} url={url} />
</li>
))}
</ul>
{activeHotels.length === 0 ? (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({ id: "No matching hotels found" })}
text={intl.formatMessage({
id: "It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.",
})}
/>
) : (
<ul className={styles.hotelList}>
{visibleHotels.map(({ hotel, url }) => (
<li key={hotel.operaId}>
<HotelListItem hotel={hotel} url={url} />
</li>
))}
</ul>
)}
</div>
)
}

View File

@@ -11,15 +11,22 @@ import HotelList from "./HotelList"
import styles from "./cityMap.module.css"
import type { MapLocation } from "@/types/components/mapLocation"
import type { CityLocation } from "@/types/trpc/routers/hotel/locations"
interface CityMapProps {
mapId: string
apiKey: string
city: CityLocation
defaultLocation: MapLocation
}
export default function CityMap({ mapId, apiKey, city }: CityMapProps) {
export default function CityMap({
mapId,
apiKey,
city,
defaultLocation,
}: CityMapProps) {
const intl = useIntl()
const { activeHotels, allFilters, activeFilters } = useDestinationDataStore(
(state) => ({
@@ -30,7 +37,13 @@ export default function CityMap({ mapId, apiKey, city }: CityMapProps) {
)
return (
<Map hotels={activeHotels} mapId={mapId} apiKey={apiKey} pageType="city">
<Map
hotels={activeHotels}
mapId={mapId}
apiKey={apiKey}
pageType="city"
defaultLocation={defaultLocation}
>
<Title
level="h2"
as="h3"

View File

@@ -69,7 +69,10 @@ export default async function DestinationCityPage() {
)}
{destination_settings.city && (
<StaticMap city={destination_settings.city} />
<StaticMap
city={destination_settings.city}
location={destination_settings.location}
/>
)}
</SidebarContentWrapper>
</aside>
@@ -78,6 +81,7 @@ export default async function DestinationCityPage() {
mapId={env.GOOGLE_DYNAMIC_MAP_ID}
apiKey={env.GOOGLE_STATIC_MAP_KEY}
city={city}
defaultLocation={destination_settings.location}
/>
</HotelDataContainer>
</Suspense>

View File

@@ -5,6 +5,7 @@ import { useIntl } from "react-intl"
import { useDestinationDataStore } from "@/stores/destination-data"
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
import Alert from "@/components/TempDesignSystem/Alert"
import Body from "@/components/TempDesignSystem/Text/Body"
import CityListItem from "../CityListItem"
@@ -12,6 +13,8 @@ import CityListSkeleton from "./CityListSkeleton"
import styles from "./cityList.module.css"
import { AlertTypeEnum } from "@/types/enums/alert"
export default function CityList() {
const intl = useIntl()
const { filters, sortItems, activeCities, isLoading } =
@@ -39,13 +42,23 @@ export default function CityList() {
listType="city"
/>
</div>
<ul className={styles.cityList}>
{activeCities.map((city) => (
<li key={city.system.uid}>
<CityListItem city={city} />
</li>
))}
</ul>
{activeCities.length === 0 ? (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({ id: "No matching locations found" })}
text={intl.formatMessage({
id: "It looks like no location match your filters. Try adjusting your search to find the perfect stay.",
})}
/>
) : (
<ul className={styles.cityList}>
{activeCities.map((city) => (
<li key={city.system.uid}>
<CityListItem city={city} />
</li>
))}
</ul>
)}
</div>
)
}

View File

@@ -12,16 +12,20 @@ import CityList from "./CityList"
import styles from "./countryMap.module.css"
import type { MapLocation } from "@/types/components/mapLocation"
interface CountryMapProps {
mapId: string
apiKey: string
country: string
defaultLocation: MapLocation
}
export default function CountryMap({
mapId,
apiKey,
country,
defaultLocation,
}: CountryMapProps) {
const intl = useIntl()
const { activeHotels, allFilters, activeFilters } = useDestinationDataStore(
@@ -33,7 +37,13 @@ export default function CountryMap({
)
return (
<Map hotels={activeHotels} mapId={mapId} apiKey={apiKey} pageType="country">
<Map
hotels={activeHotels}
mapId={mapId}
apiKey={apiKey}
pageType="country"
defaultLocation={defaultLocation}
>
<Title
level="h2"
as="h3"

View File

@@ -71,19 +71,22 @@ export default async function DestinationCountryPage() {
sidePeekContent={sidepeek_content}
/>
)}
<StaticMap country={destination_settings.country} />
<StaticMap
country={destination_settings.country}
location={destination_settings.location}
/>
</SidebarContentWrapper>
</aside>
</div>
<CountryMap
country={translatedCountry}
defaultLocation={destination_settings.location}
apiKey={env.GOOGLE_STATIC_MAP_KEY}
mapId={env.GOOGLE_DYNAMIC_MAP_ID}
/>
</CityDataContainer>
</Suspense>
<TrackingSDK pageData={tracking} />
<TrackingSDK pageData={tracking} />
</>
)
}

View File

@@ -7,7 +7,15 @@ import MapProvider from "../../Map/MapProvider"
import { getHotelMapMarkers, mapMarkerDataToGeoJson } from "../../Map/utils"
import InputForm from "./InputForm"
export default async function OverviewMapContainer() {
import type { MapLocation } from "@/types/components/mapLocation"
interface OverviewMapContainerProps {
defaultLocation: MapLocation
}
export default async function OverviewMapContainer({
defaultLocation,
}: OverviewMapContainerProps) {
const hotelData = await getAllHotels()
if (!hotelData) {
@@ -19,11 +27,23 @@ 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}>
<DynamicMap
mapId={googleMapId}
markers={markers}
defaultCoordinates={defaultCoordinates}
defaultZoom={defaultZoom}
>
<MapContent geojson={geoJson} />
</DynamicMap>
</MapProvider>

View File

@@ -26,7 +26,9 @@ export default async function DestinationOverviewPage() {
return (
<>
<div className={styles.mapContainer}>
<OverviewMapContainer />
<OverviewMapContainer
defaultLocation={destinationOverviewPage.location}
/>
</div>
<main className={styles.main}>
<div className={styles.blocks}>

View File

@@ -6,6 +6,7 @@ import { useIntl } from "react-intl"
import { useDestinationDataStore } from "@/stores/destination-data"
import DestinationFilterAndSort from "@/components/DestinationFilterAndSort"
import Alert from "@/components/TempDesignSystem/Alert"
import { BackToTopButton } from "@/components/TempDesignSystem/BackToTopButton"
import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
@@ -16,6 +17,8 @@ import HotelListingSkeleton from "./HotelListingSkeleton"
import styles from "./hotelListing.module.css"
import { AlertTypeEnum } from "@/types/enums/alert"
export default function HotelListing() {
const intl = useIntl()
const scrollRef = useRef<HTMLElement>(null)
@@ -60,23 +63,35 @@ export default function HotelListing() {
listType="hotel"
/>
</div>
<ul
className={`${styles.hotelList} ${allHotelsVisible ? styles.allVisible : ""}`}
>
{activeHotels.map(({ hotel, url }) => (
<li key={hotel.name}>
<HotelListingItem hotel={hotel} url={url} />
</li>
))}
</ul>
{activeHotels.length > 5 ? (
<ShowMoreButton
loadMoreData={handleShowMore}
showLess={allHotelsVisible}
{activeHotels.length === 0 ? (
<Alert
type={AlertTypeEnum.Info}
heading={intl.formatMessage({ id: "No matching hotels found" })}
text={intl.formatMessage({
id: "It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.",
})}
/>
) : null}
{showBackToTop && (
<BackToTopButton position="center" onClick={scrollToTop} />
) : (
<>
<ul
className={`${styles.hotelList} ${allHotelsVisible ? styles.allVisible : ""}`}
>
{activeHotels.map(({ hotel, url }) => (
<li key={hotel.name}>
<HotelListingItem hotel={hotel} url={url} />
</li>
))}
</ul>
{activeHotels.length > 5 ? (
<ShowMoreButton
loadMoreData={handleShowMore}
showLess={allHotelsVisible}
/>
) : null}
{showBackToTop && (
<BackToTopButton position="center" onClick={scrollToTop} />
)}
</>
)}
</section>
)

View File

@@ -14,17 +14,24 @@ import styles from "./dynamicMap.module.css"
import type { DestinationMarker } from "@/types/components/maps/destinationMarkers"
const BACKUP_COORDINATES = {
lat: 59.3293,
lng: 18.0686,
}
interface DynamicMapProps {
markers: DestinationMarker[]
mapId: string
onTilesLoaded?: () => void
defaultCoordinates: google.maps.LatLngLiteral | null
defaultZoom: number
onClose?: () => void
}
export default function DynamicMap({
markers,
mapId,
onTilesLoaded,
defaultCoordinates,
defaultZoom,
onClose,
children,
}: PropsWithChildren<DynamicMapProps>) {
@@ -33,13 +40,17 @@ export default function DynamicMap({
useEffect(() => {
if (map) {
const bounds = new google.maps.LatLngBounds()
markers.forEach((marker) => {
bounds.extend(marker.coordinates)
})
map.fitBounds(bounds)
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)
}
}
}, [map, markers])
}, [map, markers, defaultCoordinates])
useHandleKeyUp((event: KeyboardEvent) => {
if (event.key === "Escape" && onClose) {
@@ -61,13 +72,10 @@ export default function DynamicMap({
}
const mapOptions: MapProps = {
defaultCenter: markers[0]?.coordinates || {
lat: 59.3293,
lng: 18.0686,
}, // Default center will be overridden by the bounds
defaultCenter: defaultCoordinates || BACKUP_COORDINATES, // Default center will be overridden by the bounds
minZoom: 3,
maxZoom: 18,
defaultZoom: 5,
defaultZoom,
disableDefaultUI: true,
clickableIcons: false,
gestureHandling: "greedy",
@@ -76,9 +84,7 @@ export default function DynamicMap({
return (
<div className={styles.mapWrapper}>
<Map {...mapOptions} onTilesLoaded={onTilesLoaded}>
{children}
</Map>
<Map {...mapOptions}>{children}</Map>
<div className={styles.ctaButtons}>
{onClose && (
<Button

View File

@@ -20,6 +20,7 @@ import { getHotelMapMarkers, mapMarkerDataToGeoJson } from "./utils"
import styles from "./map.module.css"
import type { MapLocation } from "@/types/components/mapLocation"
import type { HotelDataWithUrl } from "@/types/hotel"
interface MapProps {
@@ -27,14 +28,16 @@ interface MapProps {
mapId: string
apiKey: string
pageType: "city" | "country"
defaultLocation: MapLocation
}
export default function Map({
hotels,
mapId,
apiKey,
children,
defaultLocation,
pageType,
children,
}: PropsWithChildren<MapProps>) {
const router = useRouter()
const searchParams = useSearchParams()
@@ -48,6 +51,14 @@ export default function Map({
const markers = getHotelMapMarkers(hotels)
const geoJson = mapMarkerDataToGeoJson(markers)
const defaultCoordinates = defaultLocation
? {
lat: defaultLocation.latitude,
lng: defaultLocation.longitude,
}
: null
const defaultZoom =
defaultLocation?.default_zoom ?? (pageType === "city" ? 10 : 3)
// Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget)
const handleMapHeight = useCallback(() => {
@@ -112,7 +123,13 @@ export default function Map({
aria-label={"Mapview"}
>
<aside className={styles.sidebar}>{children}</aside>
<DynamicMap markers={markers} mapId={mapId} onClose={handleClose}>
<DynamicMap
markers={markers}
mapId={mapId}
onClose={handleClose}
defaultCoordinates={defaultCoordinates}
defaultZoom={defaultZoom}
>
<MapContent geojson={geoJson} />
</DynamicMap>
</Dialog>

View File

@@ -5,29 +5,48 @@ import MapButton from "./MapButton"
import styles from "./staticMap.module.css"
import type { MapLocation } from "@/types/components/mapLocation"
interface StaticMapProps {
country?: string
city?: string
location: MapLocation
}
// Zoom levels on the static map are always more zoomed in than on the dynamic maps.
// One of the reasons is because of the smaller size of the map. This function
// adjusts the zoom level for the static map based on the default zoom level
// of the location and whether the location is a country or a city.
function getZoomLevel(defaultZoom: number | undefined, isCountry: boolean) {
if (defaultZoom) {
return isCountry ? defaultZoom - 3 : defaultZoom - 1
}
return isCountry ? 4 : 10
}
export default async function DestinationStaticMap({
country,
city,
location,
}: StaticMapProps) {
const intl = await getIntl()
const altText = city
? intl.formatMessage({ id: "Map of the city" })
: intl.formatMessage({ id: "Map of the country" })
const zoomLevel = city ? 10 : 3
const coordinates = location
? { lat: location.latitude, lng: location.longitude }
: undefined
return (
<div className={styles.mapWrapper}>
<StaticMap
country={country}
city={city}
coordinates={coordinates}
width={320}
height={200}
zoomLevel={zoomLevel}
zoomLevel={getZoomLevel(location?.default_zoom, !!country)}
altText={altText}
/>
<MapButton className={styles.button} />

View File

@@ -50,6 +50,15 @@
height: min(calc(80dvh - 180px), 500px);
}
.alertWrapper:not(:empty) {
padding: var(--Spacing-x2) var(--Spacing-x4) 0;
border-top: 1px solid var(--Base-Border-Subtle);
}
.alertWrapper:not(:empty) + .footer {
border-top: none;
}
.footer {
display: flex;
justify-content: space-between;

View File

@@ -17,6 +17,7 @@ import Divider from "@/components/TempDesignSystem/Divider"
import Footnote from "@/components/TempDesignSystem/Text/Footnote"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import Alert from "../TempDesignSystem/Alert"
import Filter from "./Filter"
import Sort from "./Sort"
@@ -26,6 +27,7 @@ import type {
CategorizedFilters,
SortItem,
} from "@/types/components/destinationFilterAndSort"
import { AlertTypeEnum } from "@/types/enums/alert"
interface HotelFilterAndSortProps {
filters: CategorizedFilters
@@ -62,12 +64,24 @@ export default function DestinationFilterAndSort({
resetPendingValues: state.actions.resetPendingValues,
setIsLoading: state.actions.setIsLoading,
}))
const alertHeading =
listType === "city"
? intl.formatMessage({ id: "No matching locations found" })
: intl.formatMessage({ id: "No matching hotels found" })
const alertText =
listType === "city"
? intl.formatMessage({
id: "It looks like no location match your filters. Try adjusting your search to find the perfect stay.",
})
: intl.formatMessage({
id: "It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.",
})
function submitAndClose(close: () => void) {
setIsLoading(true)
const sort = pendingSort
const filters = pendingFilters
const parsedUrl = new URL(window.location.href)
const searchParams = parsedUrl.searchParams
if (sort === defaultSort && searchParams.has("sort")) {
@@ -75,7 +89,6 @@ export default function DestinationFilterAndSort({
} else if (sort !== defaultSort) {
searchParams.set("sort", sort)
}
const [firstFilter, ...remainingFilters] = filters
parsedUrl.pathname = basePath
@@ -138,6 +151,16 @@ export default function DestinationFilterAndSort({
<Divider color="subtle" />
<Filter filters={filters} />
</div>
{pendingCount === 0 && (
<div className={styles.alertWrapper}>
<Alert
type={AlertTypeEnum.Warning}
heading={alertHeading}
text={alertText}
ariaRole="status"
/>
</div>
)}
<footer className={styles.footer}>
<Button
onClick={clearPendingFilters}
@@ -151,6 +174,7 @@ export default function DestinationFilterAndSort({
intent="tertiary"
size="large"
theme="base"
disabled={pendingCount === 0}
onClick={() => submitAndClose(close)}
>
{intl.formatMessage(

View File

@@ -1,4 +1,5 @@
import type { VariantProps } from "class-variance-authority"
import type { AriaRole } from "react"
import type { AlertTypeEnum } from "@/types/enums/alert"
import type { SidepeekContent } from "@/types/trpc/routers/contentstack/siteConfig"
@@ -21,4 +22,6 @@ export interface AlertProps extends VariantProps<typeof alertVariants> {
title: string
keepSearchParams?: boolean
} | null
ariaRole?: AriaRole
ariaLive?: "off" | "assertive" | "polite"
}

View File

@@ -1,3 +1,5 @@
"use client"
import Body from "@/components/TempDesignSystem/Text/Body"
import Link from "../Link"
@@ -19,6 +21,8 @@ export default function Alert({
phoneContact,
sidepeekCtaText,
sidepeekContent,
ariaLive,
ariaRole,
}: AlertProps) {
const classNames = alertVariants({
className,
@@ -31,7 +35,7 @@ export default function Alert({
return null
}
return (
<section className={classNames}>
<section className={classNames} role={ariaRole} aria-live={ariaLive}>
<div className={styles.content}>
<span className={styles.iconWrapper}>
<Icon className={styles.icon} width={24} height={24} />

View File

@@ -321,6 +321,7 @@
"Is there anything else you would like us to know before your arrival?": "Er der andet, du gerne vil have os til at vide, før din ankomst?",
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke muligt at administrere dine kommunikationspræferencer lige nu, prøv venligst igen senere eller kontakt support, hvis problemet fortsætter.",
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.": "Det ser ud til, at ingen hoteller matcher dine filtre. Prøv at justere din søgning for at finde det perfekte ophold.",
"It looks like no location match your filters. Try adjusting your search to find the perfect stay.": "Det ser ud til, at ingen placeringer matcher dine filtre. Prøv at justere din søgning for at finde det perfekte ophold.",
"Jacuzzi": "Jacuzzi",
"Join Scandic Friends": "Tilmeld dig Scandic Friends",
"Join at no cost": "Tilmeld dig uden omkostninger",
@@ -426,7 +427,8 @@
"No charges were made.": "Ingen gebyrer blev opkrævet.",
"No content published": "Intet indhold offentliggjort",
"No hotels match your filters": "Ingen rum matchede dine filtre.",
"No matching location found": "Der blev ikke fundet nogen matchende placering",
"No matching hotels found": "Der blev ikke fundet noget matchende hotel",
"No matching locations found": "Der blev ikke fundet nogen matchende placering",
"No membership benefits applied": "No membership benefits applied",
"No prices available": "Ingen tilgængelige priser",
"No results": "Ingen resultater",

View File

@@ -322,6 +322,7 @@
"Is there anything else you would like us to know before your arrival?": "Gibt es noch etwas, das Sie uns vor Ihrer Ankunft mitteilen möchten?",
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Es ist derzeit nicht möglich, Ihre Kommunikationseinstellungen zu verwalten. Bitte versuchen Sie es später erneut oder wenden Sie sich an den Support, wenn das Problem weiterhin besteht.",
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.": "Es scheint, dass keine Hotels Ihren Filtern entsprechen. Versuchen Sie, Ihre Suche anzupassen, um den perfekten Aufenthalt zu finden.",
"It looks like no location match your filters. Try adjusting your search to find the perfect stay.": "Es scheint, dass kein Standort Ihren Filtern entspricht. Versuchen Sie, Ihre Suche anzupassen, um den perfekten Aufenthalt zu finden.",
"Jacuzzi": "Whirlpool",
"Join Scandic Friends": "Treten Sie Scandic Friends bei",
"Join at no cost": "Kostenlos beitreten",
@@ -428,7 +429,8 @@
"No charges were made.": "Es wurden keine Gebühren erhoben.",
"No content published": "Kein Inhalt veröffentlicht",
"No hotels match your filters": "Kein Zimmer entspricht Ihren Filtern.",
"No matching location found": "Kein passender Standort gefunden",
"No matching hotels found": "Kein passendes Hotel gefunden",
"No matching locations found": "Kein passender Standort gefunden",
"No membership benefits applied": "No membership benefits applied",
"No prices available": "Keine Preise verfügbar",
"No results": "Keine Ergebnisse",

View File

@@ -323,6 +323,7 @@
"Is there anything else you would like us to know before your arrival?": "Is there anything else you would like us to know before your arrival?",
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.",
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.": "It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.",
"It looks like no location match your filters. Try adjusting your search to find the perfect stay.": "It looks like no location match your filters. Try adjusting your search to find the perfect stay.",
"Jacuzzi": "Jacuzzi",
"Join Scandic Friends": "Join Scandic Friends",
"Join at no cost": "Join at no cost",
@@ -429,7 +430,8 @@
"No charges were made.": "No charges were made.",
"No content published": "No content published",
"No hotels match your filters": "No hotels match your filters",
"No matching location found": "No matching location found",
"No matching hotels found": "No matching hotels found",
"No matching locations found": "No matching locations found",
"No membership benefits applied": "No membership benefits applied",
"No prices available": "No prices available",
"No results": "No results",

View File

@@ -321,6 +321,7 @@
"Is there anything else you would like us to know before your arrival?": "Onko jotain muuta, mitä haluaisit meidän tietävän ennen saapumistasi?",
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Viestintäasetuksiasi ei voi hallita juuri nyt. Yritä myöhemmin uudelleen tai ota yhteyttä tukeen, jos ongelma jatkuu.",
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.": "Näyttää siltä, että mikään hotelli ei vastaa suodattimiasi. Yritä muokata hakuasi löytääksesi täydellisen oleskelun.",
"It looks like no location match your filters. Try adjusting your search to find the perfect stay.": "Näyttää siltä, että mikään sijainti ei vastaa suodattimiasi. Yritä muokata hakuasi löytääksesi täydellisen oleskelun.",
"Jacuzzi": "Poreallas",
"Join Scandic Friends": "Liity jäseneksi",
"Join at no cost": "Liity maksutta",
@@ -427,7 +428,8 @@
"No charges were made.": "Ei maksuja tehty.",
"No content published": "Ei julkaistua sisältöä",
"No hotels match your filters": "Yksikään huone ei vastannut suodattimiasi",
"No matching location found": "Vastaavaa sijaintia ei löytynyt",
"No matching hotels found": "Vastaavaa hotellia ei löytynyt",
"No matching locations found": "Vastaavaa sijaintia ei löytynyt",
"No membership benefits applied": "No membership benefits applied",
"No prices available": "Hintoja ei ole saatavilla",
"No results": "Ei tuloksia",

View File

@@ -320,6 +320,7 @@
"Is there anything else you would like us to know before your arrival?": "Er det noe annet du vil at vi skal vite før ankomsten din?",
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det er ikke mulig å administrere kommunikasjonspreferansene dine akkurat nå, prøv igjen senere eller kontakt support hvis problemet vedvarer.",
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.": "Det ser ut til at ingen hoteller samsvarer med filtrene dine. Prøv å justere søket for å finne det perfekte oppholdet.",
"It looks like no location match your filters. Try adjusting your search to find the perfect stay.": "Det ser ut til at ingen steder samsvarer med filtrene dine. Prøv å justere søket for å finne det perfekte oppholdet.",
"Jacuzzi": "Boblebad",
"Join Scandic Friends": "Bli med i Scandic Friends",
"Join at no cost": "Bli med uten kostnad",
@@ -426,7 +427,8 @@
"No charges were made.": "Ingen gebyrer blev opkrævet.",
"No content published": "Ingen innhold publisert",
"No hotels match your filters": "Ingen rom samsvarte med filtrene dine",
"No matching location found": "Fant ingen samsvarende plassering",
"No matching hotels found": "Fant ingen samsvarende hotell",
"No matching locations found": "Fant ingen samsvarende plassering",
"No membership benefits applied": "No membership benefits applied",
"No prices available": "Ingen priser tilgjengelig",
"No results": "Ingen resultater",

View File

@@ -320,6 +320,7 @@
"Is there anything else you would like us to know before your arrival?": "Är det något mer du vill att vi ska veta innan din ankomst?",
"It is not posible to manage your communication preferences right now, please try again later or contact support if the problem persists.": "Det gick inte att hantera dina kommunikationsinställningar just nu, försök igen senare eller kontakta supporten om problemet kvarstår.",
"It looks like no hotels match your filters. Try adjusting your search to find the perfect stay.": "Det verkar som att inga hotell matchar dina filter. Prova att justera din sökning för att hitta den perfekta vistelsen.",
"It looks like no location match your filters. Try adjusting your search to find the perfect stay.": "Det verkar som att ingen plats matchar dina filter. Prova att justera din sökning för att hitta den perfekta vistelsen.",
"Jacuzzi": "Jacuzzi",
"Join Scandic Friends": "Gå med i Scandic Friends",
"Join at no cost": "Gå med utan kostnad",
@@ -426,7 +427,8 @@
"No charges were made.": "Inga avgifter har debiterats.",
"No content published": "Inget innehåll publicerat",
"No hotels match your filters": "Inga rum matchade dina filter",
"No matching location found": "Ingen matchande plats hittades",
"No matching hotels found": "Inget matchande hotell hittades",
"No matching locations found": "Ingen matchande plats hittades",
"No membership benefits applied": "No membership benefits applied",
"No prices available": "Inga priser tillgängliga",
"No results": "Inga resultat",

View File

@@ -33,6 +33,11 @@ query GetDestinationCityPage($locale: String!, $uid: String!) {
city_norway
city_poland
city_sweden
location {
longitude
latitude
default_zoom
}
}
heading
preamble

View File

@@ -28,6 +28,11 @@ query GetDestinationCountryPage($locale: String!, $uid: String!) {
title
destination_settings {
country
location {
longitude
latitude
default_zoom
}
}
heading
preamble

View File

@@ -10,6 +10,11 @@ query GetDestinationOverviewPage($locale: String!, $uid: String!) {
__typename
...CardGallery_DestinationOverviewPage
}
location {
longitude
latitude
default_zoom
}
system {
...System
created_at

View File

@@ -10,6 +10,7 @@ import {
} from "../schemas/blocks/accordion"
import { contentRefsSchema, contentSchema } from "../schemas/blocks/content"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { mapLocationSchema } from "../schemas/mapLocation"
import {
linkRefsUnionSchema,
linkUnionSchema,
@@ -24,6 +25,39 @@ import {
} from "@/types/components/tracking"
import { DestinationCityPageEnum } from "@/types/enums/destinationCityPage"
const destinationCityPageDestinationSettingsSchema = z
.object({
city_denmark: z.string().optional().nullable(),
city_finland: z.string().optional().nullable(),
city_germany: z.string().optional().nullable(),
city_poland: z.string().optional().nullable(),
city_norway: z.string().optional().nullable(),
city_sweden: z.string().optional().nullable(),
location: mapLocationSchema,
})
.transform(
({
city_denmark,
city_finland,
city_germany,
city_norway,
city_poland,
city_sweden,
location,
}) => {
const cities = [
city_denmark,
city_finland,
city_germany,
city_poland,
city_norway,
city_sweden,
].filter((city): city is string => Boolean(city))
return { city: cities[0], location }
}
)
export const destinationCityListDataSchema = z
.object({
all_destination_city_page: z.object({
@@ -31,36 +65,7 @@ export const destinationCityListDataSchema = z
z
.object({
heading: z.string(),
destination_settings: z
.object({
city_denmark: z.string().optional().nullable(),
city_finland: z.string().optional().nullable(),
city_germany: z.string().optional().nullable(),
city_poland: z.string().optional().nullable(),
city_norway: z.string().optional().nullable(),
city_sweden: z.string().optional().nullable(),
})
.transform(
({
city_denmark,
city_finland,
city_germany,
city_norway,
city_poland,
city_sweden,
}) => {
const cities = [
city_denmark,
city_finland,
city_germany,
city_poland,
city_norway,
city_sweden,
].filter((city): city is string => Boolean(city))
return { city: cities[0] }
}
),
destination_settings: destinationCityPageDestinationSettingsSchema,
sort_order: z.number().nullable(),
preamble: z.string(),
experiences: z
@@ -112,71 +117,11 @@ export const blocksSchema = z.discriminatedUnion("__typename", [
destinationCityPageContent,
])
export const destinationCityPageDestinationSettingsSchema = z
.object({
city_denmark: z.string().optional().nullable(),
city_finland: z.string().optional().nullable(),
city_germany: z.string().optional().nullable(),
city_poland: z.string().optional().nullable(),
city_norway: z.string().optional().nullable(),
city_sweden: z.string().optional().nullable(),
})
.transform(
({
city_denmark,
city_finland,
city_germany,
city_norway,
city_poland,
city_sweden,
}) => {
const cities = [
city_denmark,
city_finland,
city_germany,
city_poland,
city_norway,
city_sweden,
].filter((city): city is string => Boolean(city))
return { city: cities[0] }
}
)
export const destinationCityPageSchema = z
.object({
destination_city_page: z.object({
title: z.string(),
destination_settings: z
.object({
city_denmark: z.string().optional().nullable(),
city_finland: z.string().optional().nullable(),
city_germany: z.string().optional().nullable(),
city_poland: z.string().optional().nullable(),
city_norway: z.string().optional().nullable(),
city_sweden: z.string().optional().nullable(),
})
.transform(
({
city_denmark,
city_finland,
city_germany,
city_norway,
city_poland,
city_sweden,
}) => {
const cities = [
city_denmark,
city_finland,
city_germany,
city_poland,
city_norway,
city_sweden,
].filter((city): city is string => Boolean(city))
return { city: cities[0] }
}
),
destination_settings: destinationCityPageDestinationSettingsSchema,
heading: z.string(),
preamble: z.string(),
experiences: z
@@ -254,36 +199,7 @@ export const cityPageUrlsSchema = z
z
.object({
url: z.string(),
destination_settings: z
.object({
city_denmark: z.string().optional().nullable(),
city_finland: z.string().optional().nullable(),
city_germany: z.string().optional().nullable(),
city_poland: z.string().optional().nullable(),
city_norway: z.string().optional().nullable(),
city_sweden: z.string().optional().nullable(),
})
.transform(
({
city_denmark,
city_finland,
city_germany,
city_norway,
city_poland,
city_sweden,
}) => {
const cities = [
city_denmark,
city_finland,
city_germany,
city_poland,
city_norway,
city_sweden,
].filter((city): city is string => Boolean(city))
return { city: cities[0] }
}
),
destination_settings: destinationCityPageDestinationSettingsSchema,
system: systemSchema,
})
.transform((data) => {

View File

@@ -10,6 +10,7 @@ import {
} from "../schemas/blocks/accordion"
import { contentRefsSchema, contentSchema } from "../schemas/blocks/content"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { mapLocationSchema } from "../schemas/mapLocation"
import {
linkRefsUnionSchema,
linkUnionSchema,
@@ -52,6 +53,7 @@ export const destinationCountryPageSchema = z
title: z.string(),
destination_settings: z.object({
country: z.nativeEnum(Country),
location: mapLocationSchema,
}),
heading: z.string(),
preamble: z.string(),

View File

@@ -6,6 +6,7 @@ import {
cardGalleryRefsSchema,
cardGallerySchema,
} from "../schemas/blocks/cardGallery"
import { mapLocationSchema } from "../schemas/mapLocation"
import { systemSchema } from "../schemas/system"
import { DestinationOverviewPageEnum } from "@/types/enums/destinationOverviewPage"
@@ -26,6 +27,7 @@ export const destinationOverviewPageSchema = z.object({
destination_overview_page: z.object({
title: z.string(),
blocks: discriminatedUnionArray(blocksSchema.options),
location: mapLocationSchema,
system: systemSchema.merge(
z.object({
created_at: z.string(),

View File

@@ -0,0 +1,25 @@
import { z } from "zod"
export const mapLocationSchema = z
.object({
longitude: z.number().nullable(),
latitude: z.number().nullable(),
default_zoom: z.number().nullable(),
})
.nullish()
.transform((val) => {
if (val) {
const longitude = val.longitude
const latitude = val.latitude
const default_zoom = val.default_zoom || 3
if (longitude !== null && latitude !== null) {
return {
longitude,
latitude,
default_zoom: Math.round(default_zoom),
}
}
}
return null
})

View File

@@ -0,0 +1,5 @@
import type { z } from "zod"
import type { mapLocationSchema } from "@/server/routers/contentstack/schemas/mapLocation"
export type MapLocation = z.output<typeof mapLocationSchema>