Merged in feat/SW-1450-destination-page-cs-components (pull request #1204)

feat(SW-1450): added components in destination pages from cs

* feat(SW-1450): added components in destination pages from cs

* feat(SW-1450): added correct refs and removed classNames


Approved-by: Fredrik Thorsson
This commit is contained in:
Erik Tiekstra
2025-01-24 12:06:43 +00:00
parent 7343d873c2
commit a88a033e30
45 changed files with 1237 additions and 195 deletions

View File

@@ -7,12 +7,18 @@ import { setLang } from "@/i18n/serverContext"
import type { ContentTypeParams, LangParams, PageArgs } from "@/types/params"
import { PageContentTypeEnum } from "@/types/requests/contentType"
const IGNORED_CONTENT_TYPES = [
PageContentTypeEnum.hotelPage,
PageContentTypeEnum.destinationCityPage,
PageContentTypeEnum.destinationCountryPage,
]
export default function PageBreadcrumbs({
params,
}: PageArgs<LangParams & ContentTypeParams>) {
setLang(params.lang)
if (params.contentType === PageContentTypeEnum.hotelPage) {
if (IGNORED_CONTENT_TYPES.includes(params.contentType)) {
return null
}

View File

@@ -5,9 +5,9 @@ import { isSignupPage } from "@/constants/routes/signup"
import { env } from "@/env/server"
import { getHotelPage } from "@/lib/trpc/memoizedRequests"
import DestinationCityPage from "@/components/ContentType/DestinationCityPage"
import DestinationCountryPage from "@/components/ContentType/DestinationCountryPage"
import DestinationOverviewPage from "@/components/ContentType/DestinationOverviewPage"
import DestinationCityPage from "@/components/ContentType/DestinationPage/DestinationCityPage"
import DestinationCountryPage from "@/components/ContentType/DestinationPage/DestinationCountryPage"
import HotelPage from "@/components/ContentType/HotelPage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
import StartPage from "@/components/ContentType/StartPage"

View File

@@ -1,10 +0,0 @@
.pageContainer {
display: grid;
max-width: var(--max-width);
}
@media screen and (min-width: 768px) {
.pageContainer {
margin: 0 auto;
}
}

View File

@@ -1,28 +0,0 @@
import { Suspense } from "react"
import { getDestinationCityPage } from "@/lib/trpc/memoizedRequests"
import TrackingSDK from "@/components/TrackingSDK"
import styles from "./destinationCityPage.module.css"
export default async function DestinationCityPage() {
const pageData = await getDestinationCityPage()
if (!pageData) {
return null
}
const { tracking, destinationCityPage } = pageData
return (
<>
<div className={styles.pageContainer}>
<h1>Destination City Page</h1>
</div>
<Suspense fallback={null}>
<TrackingSDK pageData={tracking} />
</Suspense>
</>
)
}

View File

@@ -1,10 +0,0 @@
.pageContainer {
display: grid;
max-width: var(--max-width);
}
@media screen and (min-width: 768px) {
.pageContainer {
margin: 0 auto;
}
}

View File

@@ -1,28 +0,0 @@
import { Suspense } from "react"
import { getDestinationCountryPage } from "@/lib/trpc/memoizedRequests"
import TrackingSDK from "@/components/TrackingSDK"
import styles from "./destinationCountryPage.module.css"
export default async function DestinationCountryPage() {
const pageData = await getDestinationCountryPage()
if (!pageData) {
return null
}
const { tracking, destinationCountryPage } = pageData
return (
<>
<div className={styles.pageContainer}>
<h1>Destination Country Page</h1>
</div>
<Suspense fallback={null}>
<TrackingSDK pageData={tracking} />
</Suspense>
</>
)
}

View File

@@ -0,0 +1,64 @@
.pageContainer {
--map-desktop-width: 23.75rem;
display: grid;
grid-template-areas:
"header"
"sidebar"
"mainSection";
width: 100%;
gap: var(--Spacing-x4);
}
.header {
grid-area: header;
}
.mainSection {
grid-area: mainSection;
padding-bottom: var(--Spacing-x7);
min-height: 500px; /* This is a temporary value because of no content atm */
}
.sidebar {
grid-area: sidebar;
width: 100%;
height: 100%;
background-color: var(--Base-Surface-Subtle-Normal);
}
.experienceList {
list-style: none;
display: flex;
gap: var(--Spacing-x1);
flex-wrap: wrap;
}
.mapWrapper {
width: 100%;
height: 200px;
display: flex;
justify-content: center;
align-items: center;
}
.mapWrapper img {
border-radius: var(--Corner-radius-Large);
overflow: hidden;
}
@media screen and (min-width: 768px) {
.pageContainer {
max-width: var(--max-width-page);
margin: 0 auto;
gap: var(--Spacing-x4);
}
}
@media screen and (min-width: 1367px) {
.pageContainer {
grid-template-areas:
"header sidebar"
"mainSection sidebar";
grid-template-columns: 1fr var(--map-desktop-width);
}
}

View File

@@ -0,0 +1,100 @@
import { Suspense } from "react"
import { getDestinationCityPage } from "@/lib/trpc/memoizedRequests"
import Breadcrumbs from "@/components/Breadcrumbs"
import StaticMap from "@/components/Maps/StaticMap"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import Chip from "@/components/TempDesignSystem/Chip"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n"
import SidebarContentWrapper from "../SidebarContentWrapper"
import DestinationPageSidePeek from "../Sidepeek"
import TopImages from "../TopImages"
import { mapExperiencesToListData } from "../utils"
import styles from "./destinationCityPage.module.css"
import { PageContentTypeEnum } from "@/types/requests/contentType"
export default async function DestinationCityPage() {
const [intl, pageData] = await Promise.all([
getIntl(),
getDestinationCityPage(),
])
if (!pageData) {
return null
}
const { tracking, destinationCityPage } = pageData
const {
images,
heading,
preamble,
experiences,
has_sidepeek,
sidepeek_button_text,
sidepeek_content,
destination_settings,
} = destinationCityPage
const experiencesList = await mapExperiencesToListData(experiences)
return (
<>
<div className={styles.pageContainer}>
<header className={styles.header}>
<Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs variant={PageContentTypeEnum.destinationCityPage} />
</Suspense>
<TopImages images={images} />
</header>
<main className={styles.mainSection}>
{/* TODO: Add hotel listing by cityIdentifier */}
{">>>> MAIN CONTENT <<<<"}
</main>
<aside className={styles.sidebar}>
<SidebarContentWrapper>
<Title level="h2">{heading}</Title>
<Body color="uiTextMediumContrast">{preamble}</Body>
<ul className={styles.experienceList}>
{experiencesList.map(({ Icon, name }) => (
<li key={name}>
<Chip variant="tag">
<Icon width={20} height={20} />
{name}
</Chip>
</li>
))}
</ul>
{has_sidepeek && (
<DestinationPageSidePeek
buttonText={sidepeek_button_text}
sidePeekContent={sidepeek_content}
/>
)}
{destination_settings.city && (
<div className={styles.mapWrapper}>
<StaticMap
city={destination_settings.city}
country={destination_settings.country}
width={320}
height={200}
zoomLevel={10}
altText={intl.formatMessage({ id: "Map of the city center" })}
/>
</div>
)}
</SidebarContentWrapper>
</aside>
</div>
<Suspense fallback={null}>
<TrackingSDK pageData={tracking} />
</Suspense>
</>
)
}

View File

@@ -0,0 +1,64 @@
.pageContainer {
--map-desktop-width: 23.75rem;
display: grid;
grid-template-areas:
"header"
"sidebar"
"mainSection";
width: 100%;
gap: var(--Spacing-x4);
}
.header {
grid-area: header;
}
.mainSection {
grid-area: mainSection;
padding-bottom: var(--Spacing-x7);
min-height: 500px; /* This is a temporary value because of no content atm */
}
.sidebar {
grid-area: sidebar;
width: 100%;
height: 100%;
background-color: var(--Base-Surface-Subtle-Normal);
}
.experienceList {
list-style: none;
display: flex;
gap: var(--Spacing-x1);
flex-wrap: wrap;
}
.mapWrapper {
width: 100%;
height: 200px;
display: flex;
justify-content: center;
align-items: center;
}
.mapWrapper img {
border-radius: var(--Corner-radius-Large);
overflow: hidden;
}
@media screen and (min-width: 768px) {
.pageContainer {
max-width: var(--max-width-page);
margin: 0 auto;
gap: var(--Spacing-x4);
}
}
@media screen and (min-width: 1367px) {
.pageContainer {
grid-template-areas:
"header sidebar"
"mainSection sidebar";
grid-template-columns: 1fr var(--map-desktop-width);
}
}

View File

@@ -0,0 +1,97 @@
import { Suspense } from "react"
import { getDestinationCountryPage } from "@/lib/trpc/memoizedRequests"
import Breadcrumbs from "@/components/Breadcrumbs"
import StaticMap from "@/components/Maps/StaticMap"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import Chip from "@/components/TempDesignSystem/Chip"
import Body from "@/components/TempDesignSystem/Text/Body"
import Title from "@/components/TempDesignSystem/Text/Title"
import TrackingSDK from "@/components/TrackingSDK"
import { getIntl } from "@/i18n"
import SidebarContentWrapper from "../SidebarContentWrapper"
import DestinationPageSidePeek from "../Sidepeek"
import TopImages from "../TopImages"
import { mapExperiencesToListData } from "../utils"
import styles from "./destinationCountryPage.module.css"
import { PageContentTypeEnum } from "@/types/requests/contentType"
export default async function DestinationCountryPage() {
const [intl, pageData] = await Promise.all([
getIntl(),
getDestinationCountryPage(),
])
if (!pageData) {
return null
}
const { tracking, destinationCountryPage } = pageData
const {
images,
heading,
preamble,
experiences,
has_sidepeek,
sidepeek_button_text,
sidepeek_content,
destination_settings,
} = destinationCountryPage
const experiencesList = await mapExperiencesToListData(experiences)
return (
<>
<div className={styles.pageContainer}>
<header className={styles.header}>
<Suspense fallback={<BreadcrumbsSkeleton />}>
<Breadcrumbs variant={PageContentTypeEnum.destinationCityPage} />
</Suspense>
<TopImages images={images} />
</header>
<main className={styles.mainSection}>
{/* TODO: Add city listing by cityIdentifier */}
{">>>> MAIN CONTENT <<<<"}
</main>
<aside className={styles.sidebar}>
<SidebarContentWrapper>
<Title level="h2">{heading}</Title>
<Body color="uiTextMediumContrast">{preamble}</Body>
<ul className={styles.experienceList}>
{experiencesList.map(({ Icon, name }) => (
<li key={name}>
<Chip variant="tag">
<Icon width={20} height={20} />
{name}
</Chip>
</li>
))}
</ul>
{has_sidepeek && (
<DestinationPageSidePeek
buttonText={sidepeek_button_text}
sidePeekContent={sidepeek_content}
/>
)}
<div className={styles.mapWrapper}>
<StaticMap
country={destination_settings.country}
width={320}
height={200}
zoomLevel={3}
altText={intl.formatMessage({ id: "Map of the country" })}
/>
</div>
</SidebarContentWrapper>
</aside>
</div>
<Suspense fallback={null}>
<TrackingSDK pageData={tracking} />
</Suspense>
</>
)
}

View File

@@ -0,0 +1,25 @@
"use client"
import { useRef } from "react"
import { StickyElementNameEnum } from "@/stores/sticky-position"
import useStickyPosition from "@/hooks/useStickyPosition"
import styles from "./sidebarContentWrapper.module.css"
export default function SidebarContentWrapper({
children,
}: React.PropsWithChildren) {
const sidebarRef = useRef<HTMLDivElement>(null)
useStickyPosition({
ref: sidebarRef,
name: StickyElementNameEnum.DESTINATION_SIDEBAR,
})
return (
<div ref={sidebarRef} className={styles.sidebarContent}>
{children}
</div>
)
}

View File

@@ -0,0 +1,12 @@
.sidebarContent {
display: grid;
align-content: start;
gap: var(--Spacing-x2);
padding: var(--Spacing-x4);
}
@media screen and (min-width: 1367px) {
.sidebarContent {
position: sticky;
}
}

View File

@@ -0,0 +1,52 @@
"use client"
import { useState } from "react"
import { ChevronRightSmallIcon } from "@/components/Icons"
import JsonToHtml from "@/components/JsonToHtml"
import Button from "@/components/TempDesignSystem/Button"
import SidePeek from "@/components/TempDesignSystem/SidePeek"
import type { DestinationCityPageData } from "@/types/trpc/routers/contentstack/destinationCityPage"
import type { DestinationCountryPageData } from "@/types/trpc/routers/contentstack/destinationCountryPage"
interface DestinationPageSidepeekProps {
buttonText: string
sidePeekContent: NonNullable<
| DestinationCityPageData["sidepeek_content"]
| DestinationCountryPageData["sidepeek_content"]
>
}
export default function DestinationPageSidepeek({
buttonText,
sidePeekContent,
}: DestinationPageSidepeekProps) {
const [sidePeekIsOpen, setSidePeekIsOpen] = useState(false)
const { heading, content } = sidePeekContent
return (
<div>
<Button
onPress={() => setSidePeekIsOpen(true)}
theme="base"
variant="icon"
intent="text"
size="small"
wrapping
>
{buttonText}
<ChevronRightSmallIcon />
</Button>
<SidePeek
title={heading}
isOpen={sidePeekIsOpen}
handleClose={() => setSidePeekIsOpen(false)}
>
<JsonToHtml
nodes={content.json.children}
embeds={content.embedded_itemsConnection.edges}
/>
</SidePeek>
</div>
)
}

View File

@@ -0,0 +1,4 @@
.ctaContainer {
display: grid;
gap: var(--Spacing-x2);
}

View File

@@ -0,0 +1,34 @@
"use client"
import Image from "@/components/Image"
import styles from "./topImages.module.css"
import type { ImageVaultAsset } from "@/types/components/imageVault"
interface TopImageProps {
images: ImageVaultAsset[]
}
export default function TopImages({ images }: TopImageProps) {
const maxWidth = 1020
return (
<div className={styles.imageWrapper}>
{images.slice(0, 3).map((image, index) => (
<Image
key={image.url}
src={image.url}
alt={image.meta.alt || image.meta.caption || ""}
width={index === 0 ? maxWidth : maxWidth / 3}
height={Math.ceil(
(index === 0 ? maxWidth : maxWidth / 3) /
image.dimensions.aspectRatio
)}
focalPoint={image.focalPoint}
className={styles.image}
/>
))}
</div>
)
}

View File

@@ -0,0 +1,62 @@
.imageWrapper {
max-width: var(--max-width-page);
margin: 0 auto;
}
.image {
height: 200px;
max-height: 40dvh;
width: 100%;
border-radius: var(--Corner-radius-Medium);
}
@media screen and (max-width: 767px) {
.image:not(:first-child) {
display: none;
}
}
@media screen and (min-width: 768px) {
.imageWrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: var(--Spacing-x1);
}
.imageWrapper > .image:first-child {
grid-column: span 2;
grid-row: span 2;
height: 300px;
}
.imageWrapper > .image:only-child {
grid-column: span 3;
}
.imageWrapper > .image:nth-child(2):nth-last-child(1) {
grid-column: span 1;
grid-row: span 2;
height: 300px;
}
.imageWrapper > .image:nth-child(2):nth-last-child(2),
.imageWrapper > .image:nth-child(3) {
grid-column: span 1;
grid-row: span 1;
height: calc(150px - var(--Spacing-x-half));
max-height: calc(20dvh - var(--Spacing-x-half));
}
}
@media screen and (min-width: 1367px) {
.imageWrapper > .image:first-child,
.imageWrapper > .image:nth-child(2):nth-last-child(1) {
height: 400px;
}
.imageWrapper > .image:nth-child(2):nth-last-child(2),
.imageWrapper > .image:nth-child(3) {
height: calc(200px - var(--Spacing-x-half));
}
}

View File

@@ -0,0 +1,59 @@
import {
BikeIcon,
CityIcon,
FamilyIcon,
KayakingIcon,
MuseumIcon,
NightlifeIcon,
StarFilledIcon,
} from "@/components/Icons"
import { getIntl } from "@/i18n"
import type { FC } from "react"
import type { IconProps } from "@/types/components/icon"
export async function mapExperiencesToListData(
experiences: string[]
): Promise<{ Icon: FC<IconProps>; name: string }[]> {
const intl = await getIntl()
return experiences.map((experience) => {
switch (experience) {
case "Hiking":
return {
Icon: StarFilledIcon,
name: intl.formatMessage({ id: "Hiking" }),
}
case "Kayaking":
return {
Icon: KayakingIcon,
name: intl.formatMessage({ id: "Kayaking" }),
}
case "Bike friendly":
return {
Icon: BikeIcon,
name: intl.formatMessage({ id: "Bike friendly" }),
}
case "Museums":
return { Icon: MuseumIcon, name: intl.formatMessage({ id: "Museums" }) }
case "Family friendly":
return {
Icon: FamilyIcon,
name: intl.formatMessage({ id: "Family friendly" }),
}
case "City pulse":
return {
Icon: CityIcon,
name: intl.formatMessage({ id: "City pulse" }),
}
case "Nightlife":
return {
Icon: NightlifeIcon,
name: intl.formatMessage({ id: "Nightlife" }),
}
default:
return { Icon: StarFilledIcon, name: experience }
}
})
}

View File

@@ -31,7 +31,7 @@ export default function HotelCardDialogImage({
/>
)}
<div className={styles.tripAdvisor}>
<Chip intent="secondary" className={styles.tripAdvisor}>
<Chip className={styles.tripAdvisor}>
<TripAdvisorIcon color="burgundy" />
{ratings}
</Chip>

View File

@@ -14,14 +14,16 @@ function getCenter({
city?: string
country?: string
}): string | undefined {
switch (true) {
case !!coordinates:
return `${coordinates.lat},${coordinates.lng}`
case !!country:
return `${city}, ${country}`
default:
return city
if (coordinates) {
return `${coordinates.lat},${coordinates.lng}`
}
if (city && country) {
return `${city}, ${country}`
}
if (country) {
return country
}
return city
}
export default function StaticMap({

View File

@@ -10,7 +10,7 @@
padding-bottom: 0;
}
.hotelHeaderWidth.breadcrumbs {
.headerWidth.breadcrumbs {
max-width: min(var(--max-width-page), calc(100% - var(--max-width-spacing)));
}

View File

@@ -10,10 +10,10 @@ export const breadcrumbsVariants = cva(styles.breadcrumbs, {
[PageContentTypeEnum.accountPage]: styles.fullWidth,
[PageContentTypeEnum.contentPage]: styles.contentWidth,
[PageContentTypeEnum.collectionPage]: styles.contentWidth,
[PageContentTypeEnum.destinationOverviewPage]: styles.contentWidth,
[PageContentTypeEnum.destinationCountryPage]: styles.contentWidth,
[PageContentTypeEnum.destinationCityPage]: styles.contentWidth,
[PageContentTypeEnum.hotelPage]: styles.hotelHeaderWidth,
[PageContentTypeEnum.destinationOverviewPage]: styles.fullWidth,
[PageContentTypeEnum.destinationCountryPage]: styles.fullWidth,
[PageContentTypeEnum.destinationCityPage]: styles.fullWidth,
[PageContentTypeEnum.hotelPage]: styles.headerWidth,
[PageContentTypeEnum.loyaltyPage]: styles.fullWidth,
[PageContentTypeEnum.startPage]: styles.contentWidth,
default: styles.fullWidth,

View File

@@ -1,19 +1,31 @@
div.chip {
align-items: center;
border-radius: var(--Corner-radius-xLarge);
--chip-text-color: var(--Base-Text-High-contrast);
--chip-background-color: var(--Base-Surface-Primary-light-Normal);
display: flex;
gap: var(--Spacing-x-half);
height: 22px;
justify-content: center;
align-items: center;
padding: var(--Spacing-x-half) var(--Spacing-x1);
gap: var(--Spacing-x-half);
border-radius: var(--Corner-radius-Small);
color: var(--chip-text-color);
background-color: var(--chip-background-color);
}
.primary {
background-color: var(--Scandic-Red-90);
color: var(--Primary-Dark-On-Surface-Accent);
.chip *,
.chip svg * {
fill: var(--chip-text-color);
}
.secondary {
background-color: var(--Base-Surface-Primary-light-Normal);
color: var(--Primary-Light-On-Surface-Text);
.chip.burgundy {
--chip-text-color: var(--Primary-Dark-On-Surface-Text);
--chip-background-color: var(--Base-Text-High-contrast);
}
.chip.transparent {
--chip-text-color: var(--UI-Input-Controls-On-Fill-Normal);
--chip-background-color: rgba(64, 57, 55, 0.9);
}
.chip.tag {
--chip-background-color: var(--Base-Surface-Subtle-Hover);
}

View File

@@ -4,15 +4,9 @@ import { chipVariants } from "./variants"
import type { ChipProps } from "./chip"
export default function Chip({
children,
className,
intent,
variant,
}: ChipProps) {
export default function Chip({ children, className, variant }: ChipProps) {
const classNames = chipVariants({
className,
intent,
variant,
})
return (

View File

@@ -4,16 +4,14 @@ import styles from "./chip.module.css"
export const chipVariants = cva(styles.chip, {
variants: {
intent: {
primary: styles.primary,
secondary: styles.secondary,
},
variant: {
default: styles.default,
burgundy: styles.burgundy,
transparent: styles.transparent,
tag: styles.tag,
},
},
defaultVariants: {
intent: "primary",
variant: "default",
},
})

View File

@@ -54,6 +54,7 @@
"Bed": "Seng type",
"Bed options": "Sengemuligheder",
"Bed type": "Seng type",
"Bike friendly": "Cykelvenlig",
"Birth date": "Fødselsdato",
"Book": "Book",
"Book a table online": "Book et bord online",
@@ -91,6 +92,7 @@
"Choose room": "Vælg rum",
"Cities": "Byer",
"City": "By",
"City pulse": "Byens puls",
"City/State": "By/Stat",
"Clear all filters": "Ryd alle filtre",
"Clear searches": "Ryd søgninger",
@@ -159,6 +161,7 @@
"Failed to delete credit card, please try again later.": "Kunne ikke slette kreditkort. Prøv venligst igen senere.",
"Failed to verify membership": "Medlemskab ikke verificeret",
"Fair": "Messe",
"Family friendly": "Familievenlig",
"Filter": "Filter",
"Filter and sort": "Filtrer og sorter",
"Filter by": "Filtrer efter",
@@ -191,6 +194,7 @@
"Hi {firstName}!": "Hei {firstName}!",
"High floor": "Højt niveau",
"Highest level": "Højeste niveau",
"Hiking": "Vandring",
"Home": "Hjem",
"Hospital": "Hospital",
"Hotel": "Hotel",
@@ -219,6 +223,7 @@
"Join at no cost": "Tilmeld dig uden omkostninger",
"Join now": "Tilmeld dig nu",
"Join or log in while booking for member pricing.": "Tilmeld dig eller log ind under booking for medlemspris.",
"Kayaking": "Kajakroning",
"King bed": "Kingsize-seng",
"Language": "Sprog",
"Last name": "Efternavn",
@@ -247,6 +252,8 @@
"Main menu": "Hovedmenu",
"Manage preferences": "Administrer præferencer",
"Map": "Kort",
"Map of the city center": "Kort over byens centrum",
"Map of the country": "Kort over landet",
"Map of {hotelName}": "Map of {hotelName}",
"Marketing city": "Marketing by",
"Max {max, plural, one {{range} guest} other {{range} guests}}": "Maks {max, plural, one {{range} gæst} other {{range} gæster}}",
@@ -269,6 +276,7 @@
"Monday": "Mandag",
"Month": "Måned",
"Museum": "Museum",
"Museums": "Museer",
"My communication preferences": "Mine kommunikationspræferencer",
"My membership cards": "Mine medlemskort",
"My pages": "Mine sider",
@@ -282,6 +290,7 @@
"Nearby companies": "Nærliggende virksomheder",
"New password": "Nyt kodeord",
"Next": "Næste",
"Nightlife": "Natteliv",
"Nights needed to level up": "Nætter nødvendige for at komme i niveau",
"No": "Nej",
"No availability": "Ingen tilgængelighed",

View File

@@ -54,6 +54,7 @@
"Bed": "Bettentyp",
"Bed options": "Bettoptionen",
"Bed type": "Bettentyp",
"Bike friendly": "Fahrradfreundlich",
"Birth date": "Geburtsdatum",
"Book": "Buchen",
"Book a table online": "Tisch online buchen",
@@ -90,6 +91,7 @@
"Choose room": "Zimmer wählen",
"Cities": "Städte",
"City": "Stadt",
"City pulse": "Stadtpuls",
"City/State": "Stadt/Zustand",
"Clear all filters": "Alle Filter löschen",
"Clear searches": "Suche löschen",
@@ -158,6 +160,7 @@
"Failed to delete credit card, please try again later.": "Kreditkarte konnte nicht gelöscht werden. Bitte versuchen Sie es später noch einmal.",
"Failed to verify membership": "Medlemskab nicht verifiziert",
"Fair": "Messe",
"Family friendly": "Familienfreundlich",
"Filter": "Filter",
"Filter and sort": "Filtern und sortieren",
"Filter by": "Filtern nach",
@@ -190,6 +193,7 @@
"Hi {firstName}!": "Hallo {firstName}!",
"High floor": "Hohes Level",
"Highest level": "Höchstes Level",
"Hiking": "Wandern",
"Home": "Heim",
"Hospital": "Krankenhaus",
"Hotel": "Hotel",
@@ -218,6 +222,7 @@
"Join at no cost": "Kostenlos beitreten",
"Join now": "Mitglied werden",
"Join or log in while booking for member pricing.": "Treten Sie Scandic Friends bei oder loggen Sie sich ein, um den Mitgliederpreis zu erhalten.",
"Kayaking": "Kajakfahren",
"King bed": "Kingsize-Bett",
"Language": "Sprache",
"Last name": "Nachname",
@@ -246,6 +251,8 @@
"Main menu": "Hauptmenü",
"Manage preferences": "Verwalten von Voreinstellungen",
"Map": "Karte",
"Map of the city center": "Karte des Stadtzentrums",
"Map of the country": "Karte des Landes",
"Map of {hotelName}": "Map of {hotelName}",
"Marketing city": "Marketingstadt",
"Max {max, plural, one {{range} guest} other {{range} guests}}": "Max {max, plural, one {{range} gast} other {{range} gäste}}",
@@ -267,6 +274,7 @@
"Monday": "Montag",
"Month": "Monat",
"Museum": "Museum",
"Museums": "Museen",
"My communication preferences": "Meine Kommunikationseinstellungen",
"My membership cards": "Meine Mitgliedskarten",
"My pages": "Meine Seiten",
@@ -280,6 +288,7 @@
"Nearby companies": "Nahe gelegene Unternehmen",
"New password": "Neues Kennwort",
"Next": "Nächste",
"Nightlife": "Nachtleben",
"Nights needed to level up": "Nächte, die zum Levelaufstieg benötigt werden",
"No": "Nein",
"No availability": "Keine Verfügbarkeit",

View File

@@ -54,6 +54,7 @@
"Bed": "Bed",
"Bed options": "Bed options",
"Bed type": "Bed type",
"Bike friendly": "Bike friendly",
"Birth date": "Birth date",
"Book": "Book",
"Book a table online": "Book a table online",
@@ -98,6 +99,7 @@
"Choose room": "Choose room",
"Cities": "Cities",
"City": "City",
"City pulse": "City pulse",
"City/State": "City/State",
"Clear all filters": "Clear all filters",
"Clear searches": "Clear searches",
@@ -170,6 +172,7 @@
"Failed to delete credit card, please try again later.": "Failed to delete credit card, please try again later.",
"Failed to verify membership": "Failed to verify membership",
"Fair": "Fair",
"Family friendly": "Family friendly",
"Filter": "Filter",
"Filter and sort": "Filter and sort",
"Filter by": "Filter by",
@@ -207,6 +210,7 @@
"Hi {firstName}!": "Hi {firstName}!",
"High floor": "High floor",
"Highest level": "Highest level",
"Hiking": "Hiking",
"Home": "Home",
"Hospital": "Hospital",
"Hotel": "Hotel",
@@ -235,6 +239,7 @@
"Join at no cost": "Join at no cost",
"Join now": "Join now",
"Join or log in while booking for member pricing.": "Join or log in while booking for member pricing.",
"Kayaking": "Kayaking",
"King bed": "King bed",
"Language": "Language",
"Last name": "Last name",
@@ -265,6 +270,8 @@
"Manage booking": "Manage booking",
"Manage preferences": "Manage preferences",
"Map": "Map",
"Map of the city center": "Map of the city center",
"Map of the country": "Map of the country",
"Map of {hotelName}": "Map of {hotelName}",
"Marketing city": "Marketing city",
"Max {max, plural, one {{range} guest} other {{range} guests}}": "Max {max, plural, one {{range} guest} other {{range} guests}}",
@@ -291,6 +298,7 @@
"Monday": "Monday",
"Month": "Month",
"Museum": "Museum",
"Museums": "Museums",
"My communication preferences": "My communication preferences",
"My membership cards": "My membership cards",
"My pages": "My pages",
@@ -304,6 +312,7 @@
"Nearby companies": "Nearby companies",
"New password": "New password",
"Next": "Next",
"Nightlife": "Nightlife",
"Nights needed to level up": "Nights needed to level up",
"No": "No",
"No availability": "No availability",

View File

@@ -54,6 +54,7 @@
"Bed": "Vuodetyyppi",
"Bed options": "Vuodevaihtoehdot",
"Bed type": "Vuodetyyppi",
"Bike friendly": "Pyöräystävällinen",
"Birth date": "Syntymäaika",
"Book": "Varaa",
"Book a table online": "Varaa pöytä verkossa",
@@ -91,6 +92,7 @@
"Choose room": "Valitse huone",
"Cities": "Kaupungit",
"City": "Kaupunki",
"City pulse": "Kaupungin syke",
"City/State": "Kaupunki/Osavaltio",
"Clear all filters": "Tyhjennä kaikki suodattimet",
"Clear searches": "Tyhjennä haut",
@@ -159,6 +161,7 @@
"Failed to delete credit card, please try again later.": "Luottokortin poistaminen epäonnistui, yritä myöhemmin uudelleen.",
"Failed to verify membership": "Jäsenyys ei verifioitu",
"Fair": "Messukeskus",
"Family friendly": "Perheystävällinen",
"Filter": "Suodatin",
"Filter and sort": "Suodata ja lajittele",
"Filter by": "Suodatusperuste",
@@ -191,6 +194,7 @@
"Hi {firstName}!": "Hi {firstName}!",
"High floor": "Korkea taso",
"Highest level": "Korkein taso",
"Hiking": "Hiking",
"Home": "Kotiin",
"Hospital": "Sairaala",
"Hotel": "Hotelli",
@@ -219,6 +223,7 @@
"Join at no cost": "Liity maksutta",
"Join now": "Liity jäseneksi",
"Join or log in while booking for member pricing.": "Liity tai kirjaudu sisään, kun varaat jäsenhinnan.",
"Kayaking": "Melonta",
"King bed": "King-vuode",
"Language": "Kieli",
"Last name": "Sukunimi",
@@ -247,6 +252,8 @@
"Main menu": "Päävalikko",
"Manage preferences": "Asetusten hallinta",
"Map": "Kartta",
"Map of the city center": "Kartta kaupungin keskustasta",
"Map of the country": "Kartta maasta",
"Map of {hotelName}": "Map of {hotelName}",
"Marketing city": "Markkinointikaupunki",
"Max {max, plural, one {{range} guest} other {{range} guests}}": "Max {max, plural, one {{range} vieras} other {{range} vieraita}}",
@@ -269,6 +276,7 @@
"Monday": "Maanantai",
"Month": "Kuukausi",
"Museum": "Museo",
"Museums": "Museot",
"My communication preferences": "Viestintämieltymykseni",
"My membership cards": "Jäsenkorttini",
"My pages": "Omat sivut",
@@ -282,6 +290,7 @@
"Nearby companies": "Läheiset yritykset",
"New password": "Uusi salasana",
"Next": "Seuraava",
"Nightlife": "Yöelämä",
"Nights needed to level up": "Yöt, joita tarvitaan tasolle",
"No": "Ei",
"No availability": "Ei saatavuutta",

View File

@@ -54,6 +54,7 @@
"Bed": "Seng type",
"Bed options": "Sengemuligheter",
"Bed type": "Seng type",
"Bike friendly": "Sykkelvennlig",
"Birth date": "Fødselsdato",
"Book": "Bestill",
"Book a table online": "Bestill bord online",
@@ -91,6 +92,7 @@
"Choose room": "Velg rom",
"Cities": "Byer",
"City": "By",
"City pulse": "Byens puls",
"City/State": "By/Stat",
"Clear all filters": "Fjern alle filtre",
"Clear searches": "Tømme søk",
@@ -158,6 +160,7 @@
"Failed to delete credit card, please try again later.": "Kunne ikke slette kredittkortet, prøv igjen senere.",
"Failed to verify membership": "Medlemskap ikke verifisert",
"Fair": "Messe",
"Family friendly": "Familievennlig",
"Filter": "Filter",
"Filter and sort": "Filtrer og sorter",
"Filter by": "Filtrer etter",
@@ -190,6 +193,7 @@
"Hi {firstName}!": "Hei {firstName}!",
"High floor": "Høy nivå",
"Highest level": "Høyeste nivå",
"Hiking": "Fotturer",
"Home": "Hjem",
"Hospital": "Sykehus",
"Hotel": "Hotel",
@@ -218,6 +222,7 @@
"Join at no cost": "Bli med uten kostnad",
"Join now": "Bli medlem nå",
"Join or log in while booking for member pricing.": "Bli med eller logg inn under bestilling for medlemspris.",
"Kayaking": "Kajakkpadling",
"King bed": "King-size-seng",
"Language": "Språk",
"Last name": "Etternavn",
@@ -246,7 +251,9 @@
"Main menu": "Hovedmeny",
"Manage preferences": "Administrer preferanser",
"Map": "Kart",
"Map of {hotelName}": "Map of {hotelName}",
"Map of the city center": "Kart over sentrum",
"Map of the country": "Kart over landet",
"Map of {hotelName}": "Kart over {hotelName}",
"Marketing city": "Markedsføringsby",
"Max {max, plural, one {{range} guest} other {{range} guests}}": "Maks {max, plural, one {{range} gjest} other {{range} gjester}}",
"Meetings & Conferences": "Møter & Konferanser",
@@ -268,6 +275,7 @@
"Monday": "Mandag",
"Month": "Måned",
"Museum": "Museum",
"Museums": "Museums",
"My communication preferences": "Mine kommunikasjonspreferanser",
"My membership cards": "Mine medlemskort",
"My pages": "Mine sider",
@@ -281,6 +289,7 @@
"Nearby companies": "Nærliggende selskaper",
"New password": "Nytt passord",
"Next": "Neste",
"Nightlife": "Natteliv",
"Nights needed to level up": "Netter som trengs for å komme opp i nivå",
"No": "Nei",
"No availability": "Ingen tilgjengelighet",

View File

@@ -54,6 +54,7 @@
"Bed": "Sängtyp",
"Bed options": "Sängalternativ",
"Bed type": "Sängtyp",
"Bike friendly": "Cykelvänligt",
"Birth date": "Födelsedatum",
"Book": "Boka",
"Book a table online": "Boka ett bord online",
@@ -91,6 +92,7 @@
"Choose room": "Välj rum",
"Cities": "Städer",
"City": "Ort",
"City pulse": "Stadspuls",
"City/State": "Ort",
"Clear all filters": "Rensa alla filter",
"Clear searches": "Rensa tidigare sökningar",
@@ -158,6 +160,7 @@
"Failed to delete credit card, please try again later.": "Det gick inte att ta bort kreditkortet, försök igen senare.",
"Failed to verify membership": "Medlemskap inte verifierat",
"Fair": "Mässa",
"Family friendly": "Familjevänligt",
"Filter": "Filter",
"Filter and sort": "Filtrera och sortera",
"Filter by": "Filtrera på",
@@ -190,6 +193,7 @@
"Hi {firstName}!": "Hej {firstName}!",
"High floor": "Högt upp",
"Highest level": "Högsta nivå",
"Hiking": "Vandring",
"Home": "Hem",
"Hospital": "Sjukhus",
"Hotel": "Hotell",
@@ -218,6 +222,7 @@
"Join at no cost": "Gå med utan kostnad",
"Join now": "Gå med nu",
"Join or log in while booking for member pricing.": "Bli medlem eller logga in när du bokar för medlemspriser.",
"Kayaking": "Kajakpaddling",
"King bed": "King size-säng",
"Language": "Språk",
"Last name": "Efternamn",
@@ -246,7 +251,9 @@
"Main menu": "Huvudmeny",
"Manage preferences": "Hantera inställningar",
"Map": "Karta",
"Map of {hotelName}": "Map of {hotelName}",
"Map of the city center": "Karta över stadskärnan",
"Map of the country": "Karta över landet",
"Map of {hotelName}": "Karta över {hotelName}",
"Marketing city": "Marknadsföringsstad",
"Max {max, plural, one {{range} guest} other {{range} guests}}": "Max {max, plural, one {{range} gäst} other {{range} gäster}}",
"Meetings & Conferences": "Möten & Konferenser",
@@ -268,6 +275,7 @@
"Monday": "Måndag",
"Month": "Månad",
"Museum": "Museum",
"Museums": "Museer",
"My communication preferences": "Mina kommunikationspreferenser",
"My membership cards": "Mina medlemskort",
"My pages": "Mina sidor",
@@ -281,6 +289,7 @@
"Nearby companies": "Närliggande företag",
"New password": "Nytt lösenord",
"Next": "Nästa",
"Nightlife": "Nattliv",
"Nights needed to level up": "Nätter som behövs för att gå upp i nivå",
"No": "Nej",
"No availability": "Ingen tillgänglighet",

View File

@@ -0,0 +1,7 @@
#import "../System.graphql"
fragment DestinationCityPageRef on DestinationCityPage {
system {
...System
}
}

View File

@@ -0,0 +1,7 @@
#import "../System.graphql"
fragment DestinationCountryPageRef on DestinationCountryPage {
system {
...System
}
}

View File

@@ -1,8 +1,69 @@
#import "../../Fragments/System.graphql"
#import "../../Fragments/PageLink/ContentPageLink.graphql"
#import "../../Fragments/PageLink/LoyaltyPageLink.graphql"
#import "../../Fragments/PageLink/AccountPageLink.graphql"
#import "../../Fragments/PageLink/CollectionPageLink.graphql"
#import "../../Fragments/PageLink/HotelPageLink.graphql"
#import "../../Fragments/AccountPage/Ref.graphql"
#import "../../Fragments/ContentPage/Ref.graphql"
#import "../../Fragments/LoyaltyPage/Ref.graphql"
#import "../../Fragments/HotelPage/Ref.graphql"
#import "../../Fragments/CollectionPage/Ref.graphql"
#import "../../Fragments/DestinationCountryPage/Ref.graphql"
query GetDestinationCityPage($locale: String!, $uid: String!) {
destination_city_page(uid: $uid, locale: $locale) {
title
destination_settings {
countryConnection {
edges {
node {
... on DestinationCountryPage {
title
url
destination_settings {
country
}
}
}
}
}
city_denmark
city_finland
city_germany
city_norway
city_poland
city_sweden
}
heading
preamble
experiences {
destination_experiences
}
images {
image
}
has_sidepeek
sidepeek_button_text
sidepeek_content {
heading
content {
embedded_itemsConnection {
edges {
node {
__typename
...AccountPageLink
...ContentPageLink
...CollectionPageLink
...HotelPageLink
...LoyaltyPageLink
}
}
}
json
}
}
system {
...System
created_at
@@ -16,6 +77,32 @@ query GetDestinationCityPage($locale: String!, $uid: String!) {
query GetDestinationCityPageRefs($locale: String!, $uid: String!) {
destination_city_page(locale: $locale, uid: $uid) {
destination_settings {
countryConnection {
edges {
node {
__typename
...DestinationCountryPageRef
}
}
}
}
sidepeek_content {
content {
embedded_itemsConnection {
edges {
node {
__typename
...AccountPageRef
...ContentPageRef
...LoyaltyPageRef
...HotelPageRef
...CollectionPageRef
}
}
}
}
}
system {
...System
}

View File

@@ -1,8 +1,50 @@
#import "../../Fragments/System.graphql"
#import "../../Fragments/PageLink/ContentPageLink.graphql"
#import "../../Fragments/PageLink/LoyaltyPageLink.graphql"
#import "../../Fragments/PageLink/AccountPageLink.graphql"
#import "../../Fragments/PageLink/CollectionPageLink.graphql"
#import "../../Fragments/PageLink/HotelPageLink.graphql"
#import "../../Fragments/AccountPage/Ref.graphql"
#import "../../Fragments/ContentPage/Ref.graphql"
#import "../../Fragments/LoyaltyPage/Ref.graphql"
#import "../../Fragments/HotelPage/Ref.graphql"
#import "../../Fragments/CollectionPage/Ref.graphql"
query GetDestinationCountryPage($locale: String!, $uid: String!) {
destination_country_page(uid: $uid, locale: $locale) {
title
destination_settings {
country
}
heading
preamble
experiences {
destination_experiences
}
images {
image
}
has_sidepeek
sidepeek_button_text
sidepeek_content {
heading
content {
embedded_itemsConnection {
edges {
node {
__typename
...AccountPageLink
...ContentPageLink
...CollectionPageLink
...HotelPageLink
...LoyaltyPageLink
}
}
}
json
}
}
system {
...System
created_at
@@ -16,6 +58,22 @@ query GetDestinationCountryPage($locale: String!, $uid: String!) {
query GetDestinationCountryPageRefs($locale: String!, $uid: String!) {
destination_country_page(locale: $locale, uid: $uid) {
sidepeek_content {
content {
embedded_itemsConnection {
edges {
node {
__typename
...AccountPageRef
...ContentPageRef
...LoyaltyPageRef
...HotelPageRef
...CollectionPageRef
}
}
}
}
}
system {
...System
}

View File

@@ -1,25 +1,180 @@
import { z } from "zod"
import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { systemSchema } from "../schemas/system"
export const destinationCityPageSchema = z.object({
destination_city_page: z.object({
title: z.string(),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
trackingProps: z.object({
url: z.string(),
}),
})
import type { ImageVaultAsset } from "@/types/components/imageVault"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import { Country } from "@/types/enums/country"
export const destinationCityPageSchema = z
.object({
destination_city_page: z.object({
title: z.string(),
destination_settings: z
.object({
countryConnection: z
.object({
edges: z.array(
z.object({
node: z.object({
destination_settings: z.object({
country: z.nativeEnum(Country),
}),
}),
})
),
})
.transform(
(countryConnection) =>
countryConnection.edges[0].node.destination_settings.country
),
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(
({
countryConnection: country,
city_denmark,
city_finland,
city_germany,
city_norway,
city_poland,
city_sweden,
}) => {
switch (country) {
case Country.Denmark:
return { country, city: city_denmark }
case Country.Finland:
return { country, city: city_finland }
case Country.Germany:
return { country, city: city_germany }
case Country.Poland:
return { country, city: city_poland }
case Country.Norway:
return { country, city: city_norway }
case Country.Sweden:
return { country, city: city_sweden }
default:
throw new Error(`Invalid country: ${country}`)
}
}
),
heading: z.string(),
preamble: z.string(),
experiences: z
.object({
destination_experiences: z.array(z.string()),
})
.transform(({ destination_experiences }) => destination_experiences),
images: z
.array(z.object({ image: tempImageVaultAssetSchema }))
.transform((images) =>
images
.map((image) => image.image)
.filter((image): image is ImageVaultAsset => !!image)
),
has_sidepeek: z.boolean().default(false),
sidepeek_button_text: z.string().default(""),
sidepeek_content: z.object({
heading: z.string(),
content: z.object({
json: z.any(),
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: z
.discriminatedUnion("__typename", [
pageLinks.accountPageSchema,
pageLinks.contentPageSchema,
pageLinks.hotelPageSchema,
pageLinks.loyaltyPageSchema,
pageLinks.collectionPageSchema,
])
.transform((data) => {
const link = pageLinks.transform(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
}),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
trackingProps: z.object({
url: z.string(),
}),
})
.transform((data) => {
const destinationCityPage = data.destination_city_page
const system = destinationCityPage.system
const trackingUrl = data.trackingProps.url
const tracking: TrackingSDKPageData = {
pageId: system.uid,
domainLanguage: system.locale,
publishDate: system.updated_at,
createDate: system.created_at,
channel: TrackingChannelEnum["destination-page"],
pageType: "staticcontentpage",
pageName: trackingUrl,
siteSections: trackingUrl,
siteVersion: "new-web",
}
return {
destinationCityPage,
tracking,
}
})
/** REFS */
export const destinationCityPageRefsSchema = z.object({
destination_city_page: z.object({
destination_settings: z.object({
countryConnection: z.object({
edges: z.array(
z.object({
node: pageLinks.destinationCountryPageRefSchema,
})
),
}),
}),
sidepeek_content: z.object({
content: z.object({
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [
pageLinks.accountPageRefSchema,
pageLinks.contentPageRefSchema,
pageLinks.hotelPageRefSchema,
pageLinks.loyaltyPageRefSchema,
pageLinks.collectionPageRefSchema,
]),
})
),
}),
}),
}),
system: systemSchema,
}),
})

View File

@@ -20,11 +20,8 @@ import {
getDestinationCityPageRefsSuccessCounter,
getDestinationCityPageSuccessCounter,
} from "./telemetry"
import { generatePageTags } from "./utils"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type {
GetDestinationCityPageData,
GetDestinationCityPageRefsSchema,
@@ -91,6 +88,8 @@ export const destinationCityPageQueryRouter = router({
JSON.stringify({ query: { lang, uid } })
)
const tags = generatePageTags(validatedRefsData.data, lang)
getDestinationCityPageCounter.add(1, { lang, uid: `${uid}` })
console.info(
"contentstack.destinationCityPage start",
@@ -107,7 +106,7 @@ export const destinationCityPageQueryRouter = router({
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid)],
tags,
},
}
)
@@ -129,22 +128,22 @@ export const destinationCityPageQueryRouter = router({
throw notFoundError
}
const destinationCityPage = destinationCityPageSchema.safeParse(
const validatedDestinationCityPage = destinationCityPageSchema.safeParse(
response.data
)
if (!destinationCityPage.success) {
if (!validatedDestinationCityPage.success) {
getDestinationCityPageFailCounter.add(1, {
lang,
uid: `${uid}`,
error_type: "validation_error",
error: JSON.stringify(destinationCityPage.error),
error: JSON.stringify(validatedDestinationCityPage.error),
})
console.error(
"contentstack.destinationCityPage validation error",
JSON.stringify({
query: { lang, uid },
error: destinationCityPage.error,
error: validatedDestinationCityPage.error,
})
)
return null
@@ -158,22 +157,6 @@ export const destinationCityPageQueryRouter = router({
})
)
const system = destinationCityPage.data.destination_city_page.system
const tracking: TrackingSDKPageData = {
pageId: system.uid,
domainLanguage: lang,
publishDate: system.updated_at,
createDate: system.created_at,
channel: TrackingChannelEnum["destination-page"],
pageType: "staticcontentpage",
pageName: destinationCityPage.data.trackingProps.url,
siteSections: destinationCityPage.data.trackingProps.url,
siteVersion: "new-web",
}
return {
destinationCityPage: destinationCityPage.data.destination_city_page,
tracking,
}
return validatedDestinationCityPage.data
}),
})

View File

@@ -0,0 +1,37 @@
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
import type { System } from "@/types/requests/system"
import type { GetDestinationCityPageRefsSchema } from "@/types/trpc/routers/contentstack/destinationCityPage"
import type { Lang } from "@/constants/languages"
export function generatePageTags(
validatedData: GetDestinationCityPageRefsSchema,
lang: Lang
): string[] {
const connections = getConnections(validatedData)
return [
generateTagsFromSystem(lang, connections),
generateTag(lang, validatedData.destination_city_page.system.uid),
].flat()
}
export function getConnections({
destination_city_page,
}: GetDestinationCityPageRefsSchema) {
const connections: System["system"][] = [destination_city_page.system]
connections.push(
destination_city_page.destination_settings.countryConnection.edges[0].node
.system
)
if (destination_city_page.sidepeek_content) {
destination_city_page.sidepeek_content.content.embedded_itemsConnection.edges.forEach(
({ node }) => {
connections.push(node.system)
}
)
}
return connections
}

View File

@@ -1,25 +1,120 @@
import { z } from "zod"
import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import { systemSchema } from "../schemas/system"
export const destinationCountryPageSchema = z.object({
destination_country_page: z.object({
title: z.string(),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
trackingProps: z.object({
url: z.string(),
}),
})
import type { ImageVaultAsset } from "@/types/components/imageVault"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import { Country } from "@/types/enums/country"
export const destinationCountryPageSchema = z
.object({
destination_country_page: z.object({
title: z.string(),
destination_settings: z.object({
country: z.nativeEnum(Country),
}),
heading: z.string(),
preamble: z.string(),
experiences: z
.object({
destination_experiences: z.array(z.string()),
})
.transform(({ destination_experiences }) => destination_experiences),
images: z
.array(z.object({ image: tempImageVaultAssetSchema }))
.transform((images) =>
images
.map((image) => image.image)
.filter((image): image is ImageVaultAsset => !!image)
),
has_sidepeek: z.boolean().default(false),
sidepeek_button_text: z.string().default(""),
sidepeek_content: z.object({
heading: z.string(),
content: z.object({
json: z.any(),
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: z
.discriminatedUnion("__typename", [
pageLinks.accountPageSchema,
pageLinks.contentPageSchema,
pageLinks.hotelPageSchema,
pageLinks.loyaltyPageSchema,
pageLinks.collectionPageSchema,
])
.transform((data) => {
const link = pageLinks.transform(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
}),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
trackingProps: z.object({
url: z.string(),
}),
})
.transform((data) => {
const countryPageData = data.destination_country_page
const system = countryPageData.system
const trackingUrl = data.trackingProps.url
const tracking: TrackingSDKPageData = {
pageId: system.uid,
domainLanguage: system.locale,
publishDate: system.updated_at,
createDate: system.created_at,
channel: TrackingChannelEnum["destination-page"],
pageType: "staticcontentpage",
pageName: trackingUrl,
siteSections: trackingUrl,
siteVersion: "new-web",
}
return {
destinationCountryPage: countryPageData,
tracking,
}
})
/** REFS */
export const destinationCountryPageRefsSchema = z.object({
destination_country_page: z.object({
sidepeek_content: z.object({
content: z.object({
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [
pageLinks.accountPageRefSchema,
pageLinks.contentPageRefSchema,
pageLinks.hotelPageRefSchema,
pageLinks.loyaltyPageRefSchema,
pageLinks.collectionPageRefSchema,
]),
})
),
}),
}),
}),
system: systemSchema,
}),
})

View File

@@ -20,11 +20,8 @@ import {
getDestinationCountryPageRefsSuccessCounter,
getDestinationCountryPageSuccessCounter,
} from "./telemetry"
import { generatePageTags } from "./utils"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type {
GetDestinationCountryPageData,
GetDestinationCountryPageRefsSchema,
@@ -91,6 +88,8 @@ export const destinationCountryPageQueryRouter = router({
JSON.stringify({ query: { lang, uid } })
)
const tags = generatePageTags(validatedRefsData.data, lang)
getDestinationCountryPageCounter.add(1, { lang, uid: `${uid}` })
console.info(
"contentstack.destinationCountryPage start",
@@ -107,7 +106,7 @@ export const destinationCountryPageQueryRouter = router({
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid)],
tags,
},
}
)
@@ -158,23 +157,6 @@ export const destinationCountryPageQueryRouter = router({
})
)
const system = destinationCountryPage.data.destination_country_page.system
const tracking: TrackingSDKPageData = {
pageId: system.uid,
domainLanguage: lang,
publishDate: system.updated_at,
createDate: system.created_at,
channel: TrackingChannelEnum["destination-page"],
pageType: "staticcontentpage",
pageName: destinationCountryPage.data.trackingProps.url,
siteSections: destinationCountryPage.data.trackingProps.url,
siteVersion: "new-web",
}
return {
destinationCountryPage:
destinationCountryPage.data.destination_country_page,
tracking,
}
return destinationCountryPage.data
}),
})

View File

@@ -0,0 +1,32 @@
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
import type { System } from "@/types/requests/system"
import type { GetDestinationCountryPageRefsSchema } from "@/types/trpc/routers/contentstack/destinationCountryPage"
import type { Lang } from "@/constants/languages"
export function generatePageTags(
validatedData: GetDestinationCountryPageRefsSchema,
lang: Lang
): string[] {
const connections = getConnections(validatedData)
return [
generateTagsFromSystem(lang, connections),
generateTag(lang, validatedData.destination_country_page.system.uid),
].flat()
}
export function getConnections({
destination_country_page,
}: GetDestinationCountryPageRefsSchema) {
const connections: System["system"][] = [destination_country_page.system]
if (destination_country_page.sidepeek_content) {
destination_country_page.sidepeek_content.content.embedded_itemsConnection.edges.forEach(
({ node }) => {
connections.push(node.system)
}
)
}
return connections
}

View File

@@ -52,6 +52,33 @@ export const contentPageRefSchema = z.object({
system: systemSchema,
})
export const destinationCityPageSchema = z
.object({
__typename: z.literal(ContentEnum.blocks.DestinationCityPage),
})
.merge(pageLinkSchema)
export const destinationCityPageRefSchema = z.object({
__typename: z.literal(ContentEnum.blocks.DestinationCityPage),
system: systemSchema,
})
export const destinationCountryPageSchema = z
.object({
__typename: z.literal(ContentEnum.blocks.DestinationCountryPage),
})
.merge(pageLinkSchema)
export const destinationCountryPageRefSchema = z.object({
__typename: z.literal(ContentEnum.blocks.DestinationCountryPage),
system: systemSchema,
})
export const destinationOverviewPageRefSchema = z.object({
__typename: z.literal(ContentEnum.blocks.DestinationOverviewPage),
system: systemSchema,
})
export const hotelPageSchema = z
.object({
__typename: z.literal(ContentEnum.blocks.HotelPage),

View File

@@ -5,6 +5,7 @@ export enum StickyElementNameEnum {
BOOKING_WIDGET = "BOOKING_WIDGET",
HOTEL_TAB_NAVIGATION = "HOTEL_TAB_NAVIGATION",
HOTEL_STATIC_MAP = "HOTEL_STATIC_MAP",
DESTINATION_SIDEBAR = "DESTINATION_SIDEBAR",
}
export interface StickyElement {
@@ -34,6 +35,8 @@ const priorityMap: Record<StickyElementNameEnum, number> = {
[StickyElementNameEnum.HOTEL_TAB_NAVIGATION]: 3,
[StickyElementNameEnum.HOTEL_STATIC_MAP]: 3,
[StickyElementNameEnum.DESTINATION_SIDEBAR]: 3,
}
const useStickyPositionStore = create<StickyStore>((set, get) => ({

View File

@@ -3,6 +3,9 @@ export namespace ContentEnum {
AccountPage = "AccountPage",
CollectionPage = "CollectionPage",
ContentPage = "ContentPage",
DestinationCityPage = "DestinationCityPage",
DestinationCountryPage = "DestinationCountryPage",
DestinationOverviewPage = "DestinationOverviewPage",
HotelPage = "HotelPage",
ImageContainer = "ImageContainer",
LoyaltyPage = "LoyaltyPage",

View File

@@ -7,8 +7,9 @@ import type {
export interface GetDestinationCityPageData
extends z.input<typeof destinationCityPageSchema> {}
export interface DestinationCityPage
interface DestinationCityPage
extends z.output<typeof destinationCityPageSchema> {}
export type DestinationCityPageData = DestinationCityPage["destinationCityPage"]
export interface GetDestinationCityPageRefsSchema
extends z.input<typeof destinationCityPageRefsSchema> {}

View File

@@ -7,8 +7,10 @@ import type {
export interface GetDestinationCountryPageData
extends z.input<typeof destinationCountryPageSchema> {}
export interface DestinationCountryPage
interface DestinationCountryPage
extends z.output<typeof destinationCountryPageSchema> {}
export type DestinationCountryPageData =
DestinationCountryPage["destinationCountryPage"]
export interface GetDestinationCountryPageRefsSchema
extends z.input<typeof destinationCountryPageRefsSchema> {}