Merged in feat/SW-1442-destination-overview-page (pull request #1188)

feat(SW-1442): added destination overview page

* feat(SW-1442): added destination overview page


Approved-by: Fredrik Thorsson
Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-01-20 12:21:04 +00:00
parent 814b010569
commit 7ac200bd7c
27 changed files with 526 additions and 16 deletions

View File

@@ -5,6 +5,7 @@ import { isSignupPage } from "@/constants/routes/signup"
import { env } from "@/env/server"
import { getHotelPage } from "@/lib/trpc/memoizedRequests"
import DestinationOverviewPage from "@/components/ContentType/DestinationOverviewPage"
import HotelPage from "@/components/ContentType/HotelPage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
@@ -55,6 +56,8 @@ export default async function ContentTypePage({
}
case PageContentTypeEnum.loyaltyPage:
return <LoyaltyPage />
case PageContentTypeEnum.destinationOverviewPage:
return <DestinationOverviewPage />
case PageContentTypeEnum.hotelPage:
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()

View File

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

View File

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

View File

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

View File

@@ -29,6 +29,13 @@ query GetContentPageSettings($uid: String!, $locale: String!) {
}
}
}
query GetDestinationOverviewPageSettings($uid: String!, $locale: String!) {
destination_overview_page(uid: $uid, locale: $locale) {
page_settings {
hide_booking_widget
}
}
}
query GetHotelPageSettings($uid: String!, $locale: String!) {
hotel_page(uid: $uid, locale: $locale) {

View File

@@ -0,0 +1,31 @@
#import "../../Fragments/Breadcrumbs/Breadcrumbs.graphql"
#import "../../Fragments/System.graphql"
query GetDestinationOverviewPageBreadcrumbs($locale: String!, $uid: String!) {
destination_overview_page(locale: $locale, uid: $uid) {
web {
breadcrumbs {
...Breadcrumbs
}
}
system {
...System
}
}
}
query GetDestinationOverviewPageBreadcrumbsRefs(
$locale: String!
$uid: String!
) {
destination_overview_page(locale: $locale, uid: $uid) {
web {
breadcrumbs {
...BreadcrumbsRefs
}
}
system {
...System
}
}
}

View File

@@ -0,0 +1,48 @@
#import "../../Fragments/System.graphql"
query GetDestinationOverviewPage($locale: String!, $uid: String!) {
destination_overview_page(uid: $uid, locale: $locale) {
title
url
system {
...System
created_at
updated_at
}
}
trackingProps: destination_overview_page(locale: "en", uid: $uid) {
url
}
}
query GetDestinationOverviewPageRefs($locale: String!, $uid: String!) {
destination_overview_page(locale: $locale, uid: $uid) {
system {
...System
}
}
}
query GetDaDeEnUrlsDestinationOverviewPage($uid: String!) {
de: destination_overview_page(locale: "de", uid: $uid) {
url
}
en: destination_overview_page(locale: "en", uid: $uid) {
url
}
da: destination_overview_page(locale: "da", uid: $uid) {
url
}
}
query GetFiNoSvUrlsDestinationOverviewPage($uid: String!) {
fi: destination_overview_page(locale: "fi", uid: $uid) {
url
}
no: destination_overview_page(locale: "no", uid: $uid) {
url
}
sv: destination_overview_page(locale: "sv", uid: $uid) {
url
}
}

View File

@@ -0,0 +1,18 @@
#import "../../Fragments/Metadata.graphql"
#import "../../Fragments/System.graphql"
query GetDestinationOverviewPageMetadata($locale: String!, $uid: String!) {
destination_overview_page(locale: $locale, uid: $uid) {
web {
breadcrumbs {
title
}
seo_metadata {
...Metadata
}
}
system {
...System
}
}
}

View File

@@ -1,6 +1,6 @@
#import "../Fragments/System.graphql"
query ResolveEntryByUrl($locale: String!, $url: String!) {
query EntryByUrlBatch1($locale: String!, $url: String!) {
all_account_page(where: { url: $url }, locale: $locale) {
items {
system {
@@ -50,3 +50,14 @@ query ResolveEntryByUrl($locale: String!, $url: String!) {
total
}
}
query EntryByUrlBatch2($locale: String!, $url: String!) {
all_destination_overview_page(where: { url: $url }, locale: $locale) {
items {
system {
...System
}
}
total
}
}

View File

@@ -0,0 +1,41 @@
import deepmerge from "deepmerge"
import { arrayMerge } from "@/utils/merge"
import { edgeRequest } from "./edgeRequest"
import type { BatchRequestDocument } from "graphql-request"
import type { Data } from "@/types/request"
export async function batchEdgeRequest<T>(
queries: BatchRequestDocument[]
): Promise<Data<T>> {
try {
const response = await Promise.allSettled(
queries.map((query) => edgeRequest<T>(query.document, query.variables))
)
let data = {} as T
const reasons: PromiseRejectedResult["reason"][] = []
response.forEach((res) => {
if (res.status === "fulfilled") {
data = deepmerge(data, res.value.data, { arrayMerge })
} else {
reasons.push(res.reason)
}
})
if (reasons.length) {
reasons.forEach((reason) => {
console.error(`Batch request failed`, reason)
})
}
return { data }
} catch (error) {
console.error("Error in batched graphql request")
console.error(error)
throw new Error("Something went wrong")
}
}

View File

@@ -170,3 +170,9 @@ export const getMeetingRooms = cache(
return serverClient().hotel.meetingRooms(input)
}
)
export const getDestinationOverviewPage = cache(
async function getMemoizedDestinationOverviewPage() {
return serverClient().contentstack.destinationOverviewPage.get()
}
)

View File

@@ -10,11 +10,12 @@ const bookingWidgetToggleSchema = z
export const validateBookingWidgetToggleSchema = z.object({
account_page: bookingWidgetToggleSchema,
loyalty_page: bookingWidgetToggleSchema,
collection_page: bookingWidgetToggleSchema,
content_page: bookingWidgetToggleSchema,
hotel_page: bookingWidgetToggleSchema,
current_blocks_page: bookingWidgetToggleSchema,
destination_overview_page: bookingWidgetToggleSchema,
hotel_page: bookingWidgetToggleSchema,
loyalty_page: bookingWidgetToggleSchema,
})
export type ValidateBookingWidgetToggleType = z.infer<

View File

@@ -3,6 +3,7 @@ import {
GetCollectionPageSettings,
GetContentPageSettings,
GetCurrentBlocksPageSettings,
GetDestinationOverviewPageSettings,
GetHotelPageSettings,
GetLoyaltyPageSettings,
} from "@/lib/graphql/Query/BookingWidgetToggle.graphql"
@@ -50,6 +51,9 @@ export const bookingwidgetQueryRouter = router({
case PageContentTypeEnum.contentPage:
GetPageSettings = GetContentPageSettings
break
case PageContentTypeEnum.destinationOverviewPage:
GetPageSettings = GetDestinationOverviewPageSettings
break
case PageContentTypeEnum.hotelPage:
GetPageSettings = GetHotelPageSettings
break

View File

@@ -13,6 +13,10 @@ import {
GetContentPageBreadcrumbs,
GetContentPageBreadcrumbsRefs,
} from "@/lib/graphql/Query/Breadcrumbs/ContentPage.graphql"
import {
GetDestinationOverviewPageBreadcrumbs,
GetDestinationOverviewPageBreadcrumbsRefs,
} from "@/lib/graphql/Query/Breadcrumbs/DestinationOverviewPage.graphql"
import {
GetHotelPageBreadcrumbs,
GetHotelPageBreadcrumbsRefs,
@@ -203,6 +207,17 @@ export const breadcrumbsQueryRouter = router({
},
variables
)
case PageContentTypeEnum.destinationOverviewPage:
return await getBreadcrumbs<{
destination_overview_page: RawBreadcrumbsSchema
}>(
{
dataKey: "destination_overview_page",
refQuery: GetDestinationOverviewPageBreadcrumbsRefs,
query: GetDestinationOverviewPageBreadcrumbs,
},
variables
)
case PageContentTypeEnum.hotelPage:
return await getBreadcrumbs<{
hotel_page: RawBreadcrumbsSchema

View File

@@ -0,0 +1,7 @@
import { mergeRouters } from "@/server/trpc"
import { destinationOverviewPageQueryRouter } from "./query"
export const destinationOverviewPageRouter = mergeRouters(
destinationOverviewPageQueryRouter
)

View File

@@ -0,0 +1,25 @@
import { z } from "zod"
import { systemSchema } from "../schemas/system"
export const destinationOverviewPageSchema = z.object({
destination_overview_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(),
}),
})
/** REFS */
export const destinationOverviewPageRefsSchema = z.object({
destination_overview_page: z.object({
system: systemSchema,
}),
})

View File

@@ -0,0 +1,190 @@
import {
GetDestinationOverviewPage,
GetDestinationOverviewPageRefs,
} from "@/lib/graphql/Query/DestinationOverviewPage/DestinationOverviewPage.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import { generateTag } from "@/utils/generateTag"
import {
destinationOverviewPageRefsSchema,
destinationOverviewPageSchema,
} from "./output"
import {
getDestinationOverviewPageCounter,
getDestinationOverviewPageFailCounter,
getDestinationOverviewPageRefsCounter,
getDestinationOverviewPageRefsFailCounter,
getDestinationOverviewPageRefsSuccessCounter,
getDestinationOverviewPageSuccessCounter,
} from "./telemetry"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type {
GetDestinationOverviewPageData,
GetDestinationOverviewPageRefsSchema,
} from "@/types/trpc/routers/contentstack/destinationOverviewPage"
export const destinationOverviewPageQueryRouter = router({
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
const { lang, uid } = ctx
getDestinationOverviewPageRefsCounter.add(1, { lang, uid: `${uid}` })
console.info(
"contentstack.destinationOverviewPage.refs start",
JSON.stringify({
query: { lang, uid },
})
)
const refsResponse = await request<GetDestinationOverviewPageRefsSchema>(
GetDestinationOverviewPageRefs,
{
locale: lang,
uid,
},
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid)],
},
}
)
if (!refsResponse.data) {
const notFoundError = notFound(refsResponse)
getDestinationOverviewPageRefsFailCounter.add(1, {
lang,
uid,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error(
"contentstack.destinationOverviewPage.refs not found error",
JSON.stringify({
query: { lang, uid },
error: { code: notFoundError.code },
})
)
throw notFoundError
}
const validatedRefsData = destinationOverviewPageRefsSchema.safeParse(
refsResponse.data
)
if (!validatedRefsData.success) {
getDestinationOverviewPageRefsFailCounter.add(1, {
lang,
uid,
error_type: "validation_error",
error: JSON.stringify(validatedRefsData.error),
})
console.error(
"contentstack.destinationOverviewPage.refs validation error",
JSON.stringify({
query: { lang, uid },
error: validatedRefsData.error,
})
)
return null
}
getDestinationOverviewPageRefsSuccessCounter.add(1, { lang, uid: `${uid}` })
console.info(
"contentstack.destinationOverviewPage.refs success",
JSON.stringify({
query: { lang, uid },
})
)
getDestinationOverviewPageCounter.add(1, { lang, uid: `${uid}` })
console.info(
"contentstack.destinationOverviewPage start",
JSON.stringify({
query: { lang, uid },
})
)
const response = await request<GetDestinationOverviewPageData>(
GetDestinationOverviewPage,
{
locale: lang,
uid,
},
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid)],
},
}
)
if (!response.data) {
const notFoundError = notFound(response)
getDestinationOverviewPageFailCounter.add(1, {
lang,
uid: `${uid}`,
error_type: "not_found",
error: JSON.stringify({ code: notFoundError.code }),
})
console.error(
"contentstack.destinationOverviewPage not found error",
JSON.stringify({
query: { lang, uid },
error: { code: notFoundError.code },
})
)
throw notFoundError
}
const destinationOverviewPage = destinationOverviewPageSchema.safeParse(
response.data
)
if (!destinationOverviewPage.success) {
getDestinationOverviewPageFailCounter.add(1, {
lang,
uid: `${uid}`,
error_type: "validation_error",
error: JSON.stringify(destinationOverviewPage.error),
})
console.error(
"contentstack.destinationOverviewPage validation error",
JSON.stringify({
query: { lang, uid },
error: destinationOverviewPage.error,
})
)
return null
}
getDestinationOverviewPageSuccessCounter.add(1, { lang, uid: `${uid}` })
console.info(
"contentstack.destinationOverviewPage success",
JSON.stringify({
query: { lang, uid },
})
)
const system = destinationOverviewPage.data.destination_overview_page.system
const tracking: TrackingSDKPageData = {
pageId: system.uid,
domainLanguage: lang,
publishDate: system.updated_at,
createDate: system.created_at,
channel: TrackingChannelEnum["destination-overview-page"],
pageType: "staticcontentpage",
pageName: destinationOverviewPage.data.trackingProps.url,
siteSections: destinationOverviewPage.data.trackingProps.url,
siteVersion: "new-web",
}
return {
destinationOverviewPage:
destinationOverviewPage.data.destination_overview_page,
tracking,
}
}),
})

View File

@@ -0,0 +1,23 @@
import { metrics } from "@opentelemetry/api"
const meter = metrics.getMeter("trpc.contentstack.destinationOverviewPage")
export const getDestinationOverviewPageRefsCounter = meter.createCounter(
"trpc.contentstack.destinationOverviewPage.get"
)
export const getDestinationOverviewPageRefsFailCounter = meter.createCounter(
"trpc.contentstack.destinationOverviewPage.get-fail"
)
export const getDestinationOverviewPageRefsSuccessCounter = meter.createCounter(
"trpc.contentstack.destinationOverviewPage.get-success"
)
export const getDestinationOverviewPageCounter = meter.createCounter(
"trpc.contentstack.destinationOverviewPage.get"
)
export const getDestinationOverviewPageSuccessCounter = meter.createCounter(
"trpc.contentstack.destinationOverviewPage.get-success"
)
export const getDestinationOverviewPageFailCounter = meter.createCounter(
"trpc.contentstack.destinationOverviewPage.get-fail"
)

View File

@@ -6,6 +6,7 @@ import { bookingwidgetRouter } from "./bookingwidget"
import { breadcrumbsRouter } from "./breadcrumbs"
import { collectionPageRouter } from "./collectionPage"
import { contentPageRouter } from "./contentPage"
import { destinationOverviewPageRouter } from "./destinationOverviewPage"
import { hotelPageRouter } from "./hotelPage"
import { languageSwitcherRouter } from "./languageSwitcher"
import { loyaltyLevelRouter } from "./loyaltyLevel"
@@ -24,6 +25,7 @@ export const contentstackRouter = router({
loyaltyPage: loyaltyPageRouter,
collectionPage: collectionPageRouter,
contentPage: contentPageRouter,
destinationOverviewPage: destinationOverviewPageRouter,
myPages: myPagesRouter,
metadata: metadataRouter,
rewards: rewardRouter,

View File

@@ -19,6 +19,10 @@ import {
GetDaDeEnUrlsCurrentBlocksPage,
GetFiNoSvUrlsCurrentBlocksPage,
} from "@/lib/graphql/Query/Current/LanguageSwitcher.graphql"
import {
GetDaDeEnUrlsDestinationOverviewPage,
GetFiNoSvUrlsDestinationOverviewPage,
} from "@/lib/graphql/Query/DestinationOverviewPage/DestinationOverviewPage.graphql"
import {
GetDaDeEnUrlsHotelPage,
GetFiNoSvUrlsHotelPage,
@@ -96,6 +100,10 @@ async function getLanguageSwitcher(options: LanguageSwitcherVariables) {
daDeEnDocument = GetDaDeEnUrlsCollectionPage
fiNoSvDocument = GetFiNoSvUrlsCollectionPage
break
case PageContentTypeEnum.destinationOverviewPage:
daDeEnDocument = GetDaDeEnUrlsDestinationOverviewPage
fiNoSvDocument = GetFiNoSvUrlsDestinationOverviewPage
break
default:
console.error(`type: [${options.contentType}]`)
console.error(`Trying to get a content type that is not supported`)

View File

@@ -4,6 +4,7 @@ import { cache } from "react"
import { GetAccountPageMetadata } from "@/lib/graphql/Query/AccountPage/Metadata.graphql"
import { GetCollectionPageMetadata } from "@/lib/graphql/Query/CollectionPage/Metadata.graphql"
import { GetContentPageMetadata } from "@/lib/graphql/Query/ContentPage/Metadata.graphql"
import { GetDestinationOverviewPageMetadata } from "@/lib/graphql/Query/DestinationOverviewPage/Metadata.graphql"
import { GetHotelPageMetadata } from "@/lib/graphql/Query/HotelPage/Metadata.graphql"
import { GetLoyaltyPageMetadata } from "@/lib/graphql/Query/LoyaltyPage/Metadata.graphql"
import { request } from "@/lib/graphql/request"
@@ -136,6 +137,13 @@ export const metadataQueryRouter = router({
content_page: RawMetadataSchema
}>(GetContentPageMetadata, variables)
return getTransformedMetadata(contentPageResponse.content_page)
case PageContentTypeEnum.destinationOverviewPage:
const destinationOverviewPageResponse = await fetchMetadata<{
destination_overview_page: RawMetadataSchema
}>(GetDestinationOverviewPageMetadata, variables)
return getTransformedMetadata(
destinationOverviewPageResponse.destination_overview_page
)
case PageContentTypeEnum.loyaltyPage:
const loyaltyPageResponse = await fetchMetadata<{
loyalty_page: RawMetadataSchema

View File

@@ -6,6 +6,7 @@ export enum TrackingChannelEnum {
"static-content-page" = "static-content-page",
"hotelreservation" = "hotelreservation",
"collection-page" = "collection-page",
"destination-overview-page" = "destination-overview-page",
"hotels" = "hotels",
}

View File

@@ -23,6 +23,7 @@ export type ContentTypeParams = {
| PageContentTypeEnum.contentPage
| PageContentTypeEnum.hotelPage
| PageContentTypeEnum.collectionPage
| PageContentTypeEnum.destinationOverviewPage
}
export type ContentTypeWebviewParams = {

View File

@@ -1,8 +1,9 @@
export enum PageContentTypeEnum {
accountPage = "account_page",
loyaltyPage = "loyalty_page",
hotelPage = "hotel_page",
collectionPage = "collection_page",
contentPage = "content_page",
currentBlocksPage = "current_blocks_page",
destinationOverviewPage = "destination_overview_page",
hotelPage = "hotel_page",
loyaltyPage = "loyalty_page",
}

View File

@@ -19,4 +19,5 @@ export const validateEntryResolveSchema = z.object({
all_loyalty_page: entryResolveSchema,
all_current_blocks_page: entryResolveSchema,
all_hotel_page: entryResolveSchema,
all_destination_overview_page: entryResolveSchema,
})

View File

@@ -0,0 +1,17 @@
import type { z } from "zod"
import type {
destinationOverviewPageRefsSchema,
destinationOverviewPageSchema,
} from "@/server/routers/contentstack/destinationOverviewPage/output"
export interface GetDestinationOverviewPageData
extends z.input<typeof destinationOverviewPageSchema> {}
export interface DestinationPage
extends z.output<typeof destinationOverviewPageSchema> {}
export interface GetDestinationOverviewPageRefsSchema
extends z.input<typeof destinationOverviewPageRefsSchema> {}
export interface DestinationOverviewPageRefs
extends z.output<typeof destinationOverviewPageRefsSchema> {}

View File

@@ -1,23 +1,25 @@
import { Lang } from "@/constants/languages"
import { edgeRequest } from "@/lib/graphql/edgeRequest"
import { ResolveEntryByUrl } from "@/lib/graphql/Query/ResolveEntry.graphql"
import { batchEdgeRequest } from "@/lib/graphql/batchEdgeRequest"
import {
EntryByUrlBatch1,
EntryByUrlBatch2,
} from "@/lib/graphql/Query/ResolveEntry.graphql"
import { internalServerError } from "@/server/errors/next"
import { validateEntryResolveSchema } from "@/types/requests/entry"
export async function resolve(url: string, lang = Lang.en) {
const response = await edgeRequest(
ResolveEntryByUrl,
const variables = { locale: lang, url }
const response = await batchEdgeRequest([
{
locale: lang,
url,
document: EntryByUrlBatch1,
variables,
},
{
next: {
revalidate: 3600,
},
}
)
document: EntryByUrlBatch2,
variables,
},
])
const validatedData = validateEntryResolveSchema.safeParse(response.data)