Merged in feat/SW-286-collectionPage (pull request #765)

Feat(SW-286): CollectionPage

Approved-by: Erik Tiekstra
Approved-by: Fredrik Thorsson
This commit is contained in:
Matilda Landström
2024-11-04 12:10:51 +00:00
parent dabdd597e2
commit d5efaa686c
45 changed files with 918 additions and 35 deletions

View File

@@ -2,9 +2,10 @@ import { notFound } from "next/navigation"
import { env } from "@/env/server"
import ContentPage from "@/components/ContentType/ContentPage"
import HotelPage from "@/components/ContentType/HotelPage"
import LoyaltyPage from "@/components/ContentType/LoyaltyPage"
import CollectionPage from "@/components/ContentType/StaticPages/CollectionPage"
import ContentPage from "@/components/ContentType/StaticPages/ContentPage"
import { setLang } from "@/i18n/serverContext"
import {
@@ -22,6 +23,11 @@ export default function ContentTypePage({
setLang(params.lang)
switch (params.contentType) {
case "collection-page":
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()
}
return <CollectionPage />
case "content-page":
if (env.HIDE_FOR_NEXT_RELEASE) {
return notFound()

View File

@@ -42,13 +42,17 @@ export default function CardsGrid({
case CardsGridEnum.cards.Card:
return (
<Card
theme={cards_grid.theme ?? "one"}
theme={
cards_grid.theme ?? card.backgroundImage ? "image" : "one"
}
key={card.system.uid}
scriptedTopTitle={card.scripted_top_title}
heading={card.heading}
bodyText={card.body_text}
secondaryButton={card.secondaryButton}
primaryButton={card.primaryButton}
backgroundImage={card.backgroundImage}
imageGradient
/>
)
case CardsGridEnum.cards.TeaserCard:

View File

@@ -0,0 +1,22 @@
import { serverClient } from "@/lib/trpc/server"
import StaticPage from ".."
export default async function CollectionPage() {
const collectionPageRes =
await serverClient().contentstack.collectionPage.get()
if (!collectionPageRes) {
return null
}
const { tracking, collectionPage } = collectionPageRes
return (
<StaticPage
content={collectionPage}
tracking={tracking}
pageType="collection"
/>
)
}

View File

@@ -0,0 +1,17 @@
import { serverClient } from "@/lib/trpc/server"
import StaticPage from ".."
export default async function ContentPage() {
const contentPageRes = await serverClient().contentstack.contentPage.get()
if (!contentPageRes) {
return null
}
const { tracking, contentPage } = contentPageRes
return (
<StaticPage content={contentPage} tracking={tracking} pageType="content" />
)
}

View File

@@ -1,5 +1,3 @@
import { serverClient } from "@/lib/trpc/server"
import Blocks from "@/components/Blocks"
import Hero from "@/components/Hero"
import Sidebar from "@/components/Sidebar"
@@ -8,21 +6,22 @@ import Preamble from "@/components/TempDesignSystem/Text/Preamble"
import Title from "@/components/TempDesignSystem/Text/Title"
import TrackingSDK from "@/components/TrackingSDK"
import styles from "./contentPage.module.css"
import { staticPageVariants } from "./variants"
export default async function ContentPage() {
const contentPageRes = await serverClient().contentstack.contentPage.get()
import styles from "./staticPage.module.css"
if (!contentPageRes) {
return null
}
import type { StaticPageProps } from "./staticPage"
const { tracking, contentPage } = contentPageRes
const { blocks, hero_image, header, sidebar } = contentPage
export default function StaticPage({
content,
tracking,
pageType,
}: StaticPageProps) {
const { blocks, hero_image, header } = content
return (
<>
<section className={styles.contentPage}>
<section className={staticPageVariants({ pageType })}>
<header className={styles.header}>
<div className={styles.headerContent}>
{header ? (
@@ -54,7 +53,9 @@ export default async function ContentPage() {
{blocks ? <Blocks blocks={blocks} /> : null}
</main>
{sidebar?.length ? <Sidebar blocks={sidebar} /> : null}
{"sidebar" in content && content.sidebar?.length ? (
<Sidebar blocks={content.sidebar} />
) : null}
</div>
</section>

View File

@@ -1,4 +1,4 @@
.contentPage {
.page {
padding-bottom: var(--Spacing-x9);
}
@@ -32,20 +32,27 @@
}
.contentContainer {
padding: var(--Spacing-x4) var(--Spacing-x2) 0;
}
.content .contentContainer {
display: grid;
grid-template-areas:
"main"
"sidebar";
gap: var(--Spacing-x4);
align-items: start;
padding: var(--Spacing-x4) var(--Spacing-x2) 0;
}
.mainContent {
grid-area: main;
display: grid;
gap: var(--Spacing-x4);
width: 100%;
gap: var(--Spacing-x6);
}
.content .mainContent {
grid-area: main;
}
@media (min-width: 768px) {
@@ -58,12 +65,20 @@
.heroContainer {
padding: var(--Spacing-x4) 0;
}
.contentContainer {
max-width: var(--max-width-content);
padding: var(--Spacing-x4) 0 0;
margin: 0 auto;
}
.content .contentContainer {
grid-template-areas: "main sidebar";
grid-template-columns: var(--max-width-text-block) 1fr;
gap: var(--Spacing-x9);
max-width: var(--max-width-content);
margin: 0 auto;
padding: var(--Spacing-x4) 0 0;
}
.mainContent {
gap: var(--Spacing-x9);
}
}

View File

@@ -0,0 +1,15 @@
import { staticPageVariants } from "./variants"
import type { VariantProps } from "class-variance-authority"
import type { TrackingSDKPageData } from "@/types/components/tracking"
import type { CollectionPage } from "@/types/trpc/routers/contentstack/collectionPage"
import type { ContentPage } from "@/types/trpc/routers/contentstack/contentPage"
export interface StaticPageProps
extends Omit<React.HTMLAttributes<HTMLDivElement>, "content">,
VariantProps<typeof staticPageVariants> {
pageType?: "collection" | "content"
content: CollectionPage["collection_page"] | ContentPage["content_page"]
tracking: TrackingSDKPageData
}

View File

@@ -0,0 +1,15 @@
import { cva } from "class-variance-authority"
import styles from "./staticPage.module.css"
export const staticPageVariants = cva(styles.page, {
variants: {
pageType: {
collection: styles.collection,
content: styles.content,
},
},
defaultVariants: {
pageType: "content",
},
})

View File

@@ -41,6 +41,7 @@
.content {
margin: var(--Spacing-x0) var(--Spacing-x4);
position: absolute;
display: grid;
gap: var(--Spacing-x2);
}

View File

@@ -5,6 +5,7 @@
#import "./Refs/Card.graphql"
#import "./Refs/LoyaltyCard.graphql"
#import "./Refs/TeaserCard.graphql"
fragment CardsGrid_ContentPage on ContentPageBlocksCardsGrid {
cards_grid {
layout
@@ -39,6 +40,38 @@ fragment CardsGrid_ContentPageRefs on ContentPageBlocksCardsGrid {
}
}
fragment CardsGrid_CollectionPage on CollectionPageBlocksCardsGrid {
cards_grid {
layout
preamble
theme
title
cardConnection(limit: 10) {
edges {
node {
__typename
...CardBlock
...TeaserCardBlock
}
}
}
}
}
fragment CardsGrid_CollectionPageRefs on CollectionPageBlocksCardsGrid {
cards_grid {
cardConnection(limit: 10) {
edges {
node {
__typename
...CardBlockRef
...TeaserCardBlockRef
}
}
}
}
}
fragment CardsGrid_LoyaltyPage on LoyaltyPageBlocksCardsGrid {
cards_grid {
layout

View File

@@ -1,8 +1,10 @@
#import "../AccountPage/Ref.graphql"
#import "../CollectionPage/Ref.graphql"
#import "../ContentPage/Ref.graphql"
#import "../LoyaltyPage/Ref.graphql"
#import "../PageLink/AccountPageLink.graphql"
#import "../PageLink/CollectionPageLink.graphql"
#import "../PageLink/ContentPageLink.graphql"
#import "../PageLink/LoyaltyPageLink.graphql"
@@ -32,6 +34,12 @@ fragment Shortcuts_AccountPage on AccountPageContentShortcuts {
}
}
fragment Shortcuts_CollectionPage on CollectionPageBlocksShortcuts {
shortcuts {
...Shortcuts
}
}
fragment Shortcuts_ContentPage on ContentPageBlocksShortcuts {
shortcuts {
...Shortcuts
@@ -65,6 +73,12 @@ fragment Shortcuts_AccountPageRefs on AccountPageContentShortcuts {
}
}
fragment Shortcuts_CollectionPageRefs on CollectionPageBlocksShortcuts {
shortcuts {
...ShortcutsRefs
}
}
fragment Shortcuts_ContentPageRefs on ContentPageBlocksShortcuts {
shortcuts {
...ShortcutsRefs

View File

@@ -68,3 +68,64 @@ fragment UspGrid_ContentPageRefs on ContentPageBlocksUspGrid {
}
}
}
fragment UspGrid_CollectionPage on CollectionPageBlocksUspGrid {
__typename
usp_grid {
cardsConnection {
edges {
node {
... on UspGrid {
usp_card {
__typename
icon
text {
embedded_itemsConnection {
totalCount
edges {
node {
__typename
...AccountPageLink
...ContentPageLink
...HotelPageLink
...LoyaltyPageLink
}
}
}
json
}
}
}
}
}
}
}
}
fragment UspGrid_CollectionPageRefs on CollectionPageBlocksUspGrid {
usp_grid {
cardsConnection {
edges {
node {
... on UspGrid {
usp_card {
text {
embedded_itemsConnection {
edges {
node {
__typename
...AccountPageRef
...ContentPageRef
...HotelPageRef
...LoyaltyPageRef
}
}
}
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,21 @@
#import "../PageLink/CollectionPageLink.graphql"
#import "../PageLink/ContentPageLink.graphql"
#import "../PageLink/HotelPageLink.graphql"
#import "../PageLink/LoyaltyPageLink.graphql"
fragment NavigationLinks on CollectionPageHeader {
navigation_links {
title
linkConnection {
edges {
node {
__typename
...HotelPageLink
...CollectionPageLink
...ContentPageLink
...LoyaltyPageLink
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,12 @@
#import "../System.graphql"
fragment CollectionPageLink on CollectionPage {
title
url
system {
...System
}
web {
original_url
}
}

View File

@@ -14,6 +14,14 @@ query GetLoyaltyPageSettings($uid: String!, $locale: String!) {
}
}
query GetCollectionPageSettings($uid: String!, $locale: String!) {
collection_page(uid: $uid, locale: $locale) {
page_settings {
hide_booking_widget
}
}
}
query GetContentPageSettings($uid: String!, $locale: String!) {
content_page(uid: $uid, locale: $locale) {
page_settings {

View File

@@ -0,0 +1,28 @@
#import "../../Fragments/Breadcrumbs/Breadcrumbs.graphql"
#import "../../Fragments/System.graphql"
query GetCollectionPageBreadcrumbs($locale: String!, $uid: String!) {
collection_page(locale: $locale, uid: $uid) {
web {
breadcrumbs {
...Breadcrumbs
}
}
system {
...System
}
}
}
query GetCollectionPageBreadcrumbsRefs($locale: String!, $uid: String!) {
collection_page(locale: $locale, uid: $uid) {
web {
breadcrumbs {
...BreadcrumbsRefs
}
}
system {
...System
}
}
}

View File

@@ -0,0 +1,83 @@
#import "../../Fragments/System.graphql"
#import "../../Fragments/Blocks/CardsGrid.graphql"
#import "../../Fragments/Blocks/Shortcuts.graphql"
#import "../../Fragments/Blocks/UspGrid.graphql"
#import "../../Fragments/CollectionPage/NavigationLinks.graphql"
query GetCollectionPage($locale: String!, $uid: String!) {
collection_page(uid: $uid, locale: $locale) {
hero_image
title
header {
heading
preamble
...NavigationLinks
}
blocks {
__typename
...CardsGrid_CollectionPage
...Shortcuts_CollectionPage
...UspGrid_CollectionPage
}
system {
...System
created_at
updated_at
}
}
}
query GetCollectionPageRefs($locale: String!, $uid: String!) {
collection_page(locale: $locale, uid: $uid) {
header {
navigation_links {
linkConnection {
edges {
node {
__typename
...CollectionPageRef
...ContentPageRef
...HotelPageRef
...LoyaltyPageRef
}
}
}
}
}
blocks {
__typename
...CardsGrid_CollectionPageRefs
...Shortcuts_CollectionPageRefs
...UspGrid_CollectionPageRefs
}
system {
...System
}
}
}
query GetDaDeEnUrlsCollectionPage($uid: String!) {
de: collection_page(locale: "de", uid: $uid) {
url
}
en: collection_page(locale: "en", uid: $uid) {
url
}
da: collection_page(locale: "da", uid: $uid) {
url
}
}
query GetFiNoSvUrlsCollectionPage($uid: String!) {
fi: collection_page(locale: "fi", uid: $uid) {
url
}
no: collection_page(locale: "no", uid: $uid) {
url
}
sv: collection_page(locale: "sv", uid: $uid) {
url
}
}

View File

@@ -9,6 +9,14 @@ query ResolveEntryByUrl($locale: String!, $url: String!) {
}
total
}
all_collection_page(where: { url: $url }, locale: $locale) {
items {
system {
...System
}
}
total
}
all_content_page(where: { url: $url }, locale: $locale) {
items {
system {

View File

@@ -11,6 +11,7 @@ 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,

View File

@@ -2,6 +2,7 @@ import { ValueOf } from "next/dist/shared/lib/constants"
import {
GetAccountPageSettings,
GetCollectionPageSettings,
GetContentPageSettings,
GetCurrentBlocksPageSettings,
GetHotelPageSettings,
@@ -43,6 +44,9 @@ export const bookingwidgetQueryRouter = router({
case ContentTypeEnum.loyaltyPage:
GetPageSettings = GetLoyaltyPageSettings
break
case ContentTypeEnum.collectionPage:
GetPageSettings = GetCollectionPageSettings
break
case ContentTypeEnum.contentPage:
GetPageSettings = GetContentPageSettings
break

View File

@@ -50,6 +50,16 @@ export type GetLoyaltyPageBreadcrumbsRefsData = z.infer<
typeof validateLoyaltyPageBreadcrumbsRefsContentstackSchema
>
export const validateCollectionPageBreadcrumbsRefsContentstackSchema = z.object(
{
collection_page: breadcrumbsRefs,
}
)
export type GetCollectionPageBreadcrumbsRefsData = z.infer<
typeof validateCollectionPageBreadcrumbsRefsContentstackSchema
>
export const validateContentPageBreadcrumbsRefsContentstackSchema = z.object({
content_page: breadcrumbsRefs,
})
@@ -107,3 +117,11 @@ export const validateContentPageBreadcrumbsContentstackSchema = z.object({
export type GetContentPageBreadcrumbsData = z.infer<
typeof validateContentPageBreadcrumbsContentstackSchema
>
export const validateCollectionPageBreadcrumbsContentstackSchema = z.object({
collection_page: page,
})
export type GetCollectionPageBreadcrumbsData = z.infer<
typeof validateCollectionPageBreadcrumbsContentstackSchema
>

View File

@@ -2,6 +2,10 @@ import {
GetMyPagesBreadcrumbs,
GetMyPagesBreadcrumbsRefs,
} from "@/lib/graphql/Query/Breadcrumbs/AccountPage.graphql"
import {
GetCollectionPageBreadcrumbs,
GetCollectionPageBreadcrumbsRefs,
} from "@/lib/graphql/Query/Breadcrumbs/CollectionPage.graphql"
import {
GetContentPageBreadcrumbs,
GetContentPageBreadcrumbsRefs,
@@ -13,12 +17,16 @@ import {
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import {
GetCollectionPageBreadcrumbsData,
GetCollectionPageBreadcrumbsRefsData,
type GetContentPageBreadcrumbsData,
type GetContentPageBreadcrumbsRefsData,
type GetLoyaltyPageBreadcrumbsData,
type GetLoyaltyPageBreadcrumbsRefsData,
type GetMyPagesBreadcrumbsData,
type GetMyPagesBreadcrumbsRefsData,
validateCollectionPageBreadcrumbsContentstackSchema,
validateCollectionPageBreadcrumbsRefsContentstackSchema,
validateContentPageBreadcrumbsContentstackSchema,
validateContentPageBreadcrumbsRefsContentstackSchema,
validateLoyaltyPageBreadcrumbsContentstackSchema,
@@ -84,6 +92,48 @@ async function getLoyaltyPageBreadcrumbs(variables: Variables) {
)
}
async function getCollectionPageBreadcrumbs(variables: Variables) {
const refsResponse =
await getRefsResponse<GetCollectionPageBreadcrumbsRefsData>(
GetCollectionPageBreadcrumbsRefs,
variables
)
const validatedRefsData =
validateCollectionPageBreadcrumbsRefsContentstackSchema.safeParse(
refsResponse.data
)
if (!validatedRefsData.success) {
console.error(
`Failed to validate CollectionPpage Breadcrumbs Refs - (uid: ${variables.uid})`
)
console.error(validatedRefsData.error)
return null
}
const tags = getTags(validatedRefsData.data.collection_page, variables)
const response = await getResponse<GetCollectionPageBreadcrumbsData>(
GetCollectionPageBreadcrumbs,
variables,
tags
)
if (!response.data.collection_page.web?.breadcrumbs?.title) {
return null
}
const validatedBreadcrumbsData =
validateCollectionPageBreadcrumbsContentstackSchema.safeParse(response.data)
if (!validatedBreadcrumbsData.success) {
console.error(
`Failed to validate Collectionpage Breadcrumbs Data - (uid: ${variables.uid})`
)
console.error(validatedBreadcrumbsData.error)
return null
}
return getBreadcrumbs(
validatedBreadcrumbsData.data.collection_page,
variables.locale
)
}
async function getContentPageBreadcrumbs(variables: Variables) {
const refsResponse = await getRefsResponse<GetContentPageBreadcrumbsRefsData>(
GetContentPageBreadcrumbsRefs,
@@ -189,6 +239,8 @@ export const breadcrumbsQueryRouter = router({
switch (ctx.contentType) {
case PageTypeEnum.accountPage:
return await getMyPagesBreadcrumbs(variables)
case PageTypeEnum.collectionPage:
return await getCollectionPageBreadcrumbs(variables)
case PageTypeEnum.contentPage:
return await getContentPageBreadcrumbs(variables)
case PageTypeEnum.loyaltyPage:

View File

@@ -0,0 +1,5 @@
import { mergeRouters } from "@/server/trpc"
import { collectionPageQueryRouter } from "./query"
export const collectionPageRouter = mergeRouters(collectionPageQueryRouter)

View File

@@ -0,0 +1,121 @@
import { z } from "zod"
import { discriminatedUnionArray } from "@/lib/discriminatedUnion"
import {
cardGridRefsSchema,
cardsGridSchema,
} from "../schemas/blocks/cardsGrid"
import {
shortcutsRefsSchema,
shortcutsSchema,
} from "../schemas/blocks/shortcuts"
import { uspGridRefsSchema, uspGridSchema } from "../schemas/blocks/uspGrid"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import {
linkAndTitleSchema,
linkConnectionRefs,
} from "../schemas/linkConnection"
import { systemSchema } from "../schemas/system"
import { CollectionPageEnum } from "@/types/enums/collectionPage"
// Block schemas
export const collectionPageCards = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.CardsGrid),
})
.merge(cardsGridSchema)
export const collectionPageShortcuts = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.Shortcuts),
})
.merge(shortcutsSchema)
export const collectionPageUspGrid = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.UspGrid),
})
.merge(uspGridSchema)
export const blocksSchema = z.discriminatedUnion("__typename", [
collectionPageCards,
collectionPageShortcuts,
collectionPageUspGrid,
])
const navigationLinksSchema = z
.array(linkAndTitleSchema)
.nullable()
.transform((data) => {
if (!data) {
return null
}
return data
.filter((item) => !!item.link)
.map((item) => ({
url: item.link!.url,
title: item.title || item.link!.title,
}))
})
// Content Page Schema and types
export const collectionPageSchema = z.object({
collection_page: z.object({
hero_image: tempImageVaultAssetSchema,
blocks: discriminatedUnionArray(blocksSchema.options).nullable(),
title: z.string(),
header: z.object({
heading: z.string(),
preamble: z.string(),
navigation_links: navigationLinksSchema,
}),
system: systemSchema.merge(
z.object({
created_at: z.string(),
updated_at: z.string(),
})
),
}),
})
/** REFS */
const collectionPageCardsRefs = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.CardsGrid),
})
.merge(cardGridRefsSchema)
const collectionPageShortcutsRefs = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.Shortcuts),
})
.merge(shortcutsRefsSchema)
const collectionPageUspGridRefs = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.UspGrid),
})
.merge(uspGridRefsSchema)
const collectionPageBlockRefsItem = z.discriminatedUnion("__typename", [
collectionPageShortcutsRefs,
collectionPageCardsRefs,
collectionPageUspGridRefs,
])
const collectionPageHeaderRefs = z.object({
navigation_links: z.array(linkConnectionRefs),
})
export const collectionPageRefsSchema = z.object({
collection_page: z.object({
header: collectionPageHeaderRefs,
blocks: discriminatedUnionArray(
collectionPageBlockRefsItem.options
).nullable(),
system: systemSchema,
}),
})

View File

@@ -0,0 +1,78 @@
import { Lang } from "@/constants/languages"
import { GetCollectionPage } from "@/lib/graphql/Query/CollectionPage/CollectionPage.graphql"
import { request } from "@/lib/graphql/request"
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
import { collectionPageSchema } from "./output"
import {
fetchCollectionPageRefs,
generatePageTags,
getCollectionPageCounter,
validateCollectionPageRefs,
} from "./utils"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import { GetCollectionPageSchema } from "@/types/trpc/routers/contentstack/collectionPage"
export const collectionPageQueryRouter = router({
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
const { lang, uid } = ctx
const collectionPageRefsData = await fetchCollectionPageRefs(lang, uid)
const collectionPageRefs = validateCollectionPageRefs(
collectionPageRefsData,
lang,
uid
)
if (!collectionPageRefs) {
return null
}
const tags = generatePageTags(collectionPageRefs, lang)
getCollectionPageCounter.add(1, { lang, uid })
console.info(
"contentstack.collectionPage start",
JSON.stringify({
query: { lang, uid },
})
)
const response = await request<GetCollectionPageSchema>(
GetCollectionPage,
{ locale: lang, uid },
{
cache: "force-cache",
next: {
tags,
},
}
)
const collectionPage = collectionPageSchema.safeParse(response.data)
if (!collectionPage.success) {
console.error(
`Failed to validate CollectionPage Data - (lang: ${lang}, uid: ${uid})`
)
console.error(collectionPage.error?.format())
return null
}
const tracking: TrackingSDKPageData = {
pageId: collectionPage.data.collection_page.system.uid,
lang: collectionPage.data.collection_page.system.locale as Lang,
publishedDate: collectionPage.data.collection_page.system.updated_at,
createdDate: collectionPage.data.collection_page.system.created_at,
channel: TrackingChannelEnum["collection-page"],
pageType: "collectionpage",
}
return {
collectionPage: collectionPage.data.collection_page,
tracking,
}
}),
})

View File

@@ -0,0 +1,152 @@
import { metrics } from "@opentelemetry/api"
import { Lang } from "@/constants/languages"
import { GetCollectionPageRefs } from "@/lib/graphql/Query/CollectionPage/CollectionPage.graphql"
import { request } from "@/lib/graphql/request"
import { notFound } from "@/server/errors/trpc"
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
import { collectionPageRefsSchema } from "./output"
import { CollectionPageEnum } from "@/types/enums/collectionPage"
import { System } from "@/types/requests/system"
import {
CollectionPageRefs,
GetCollectionPageRefsSchema,
} from "@/types/trpc/routers/contentstack/collectionPage"
const meter = metrics.getMeter("trpc.collectionPage")
// OpenTelemetry metrics: CollectionPage
export const getCollectionPageCounter = meter.createCounter(
"trpc.contentstack.collectionPage.get"
)
const getCollectionPageRefsCounter = meter.createCounter(
"trpc.contentstack.collectionPage.get"
)
const getCollectionPageRefsFailCounter = meter.createCounter(
"trpc.contentstack.collectionPage.get-fail"
)
const getCollectionPageRefsSuccessCounter = meter.createCounter(
"trpc.contentstack.collectionPage.get-success"
)
export async function fetchCollectionPageRefs(lang: Lang, uid: string) {
getCollectionPageRefsCounter.add(1, { lang, uid })
console.info(
"contentstack.collectionPage.refs start",
JSON.stringify({
query: { lang, uid },
})
)
const refsResponse = await request<GetCollectionPageRefsSchema>(
GetCollectionPageRefs,
{ locale: lang, uid },
{
cache: "force-cache",
next: {
tags: [generateTag(lang, uid)],
},
}
)
if (!refsResponse.data) {
const notFoundError = notFound(refsResponse)
getCollectionPageRefsFailCounter.add(1, {
lang,
uid,
error_type: "http_error",
error: JSON.stringify({
code: notFoundError.code,
}),
})
console.error(
"contentstack.collectionPage.refs not found error",
JSON.stringify({
query: {
lang,
uid,
},
error: { code: notFoundError.code },
})
)
throw notFoundError
}
return refsResponse.data
}
export function validateCollectionPageRefs(
data: GetCollectionPageRefsSchema,
lang: Lang,
uid: string
) {
const validatedData = collectionPageRefsSchema.safeParse(data)
if (!validatedData.success) {
getCollectionPageRefsFailCounter.add(1, {
lang,
uid,
error_type: "validation_error",
error: JSON.stringify(validatedData.error),
})
console.error(
"contentstack.collectionPage.refs validation error",
JSON.stringify({
query: { lang, uid },
error: validatedData.error,
})
)
return null
}
getCollectionPageRefsSuccessCounter.add(1, { lang, uid })
console.info(
"contentstack.collectionPage.refs success",
JSON.stringify({
query: { lang, uid },
})
)
return validatedData.data
}
export function generatePageTags(
validatedData: CollectionPageRefs,
lang: Lang
): string[] {
const connections = getConnections(validatedData)
return [
generateTagsFromSystem(lang, connections),
generateTag(lang, validatedData.collection_page.system.uid),
].flat()
}
export function getConnections({ collection_page }: CollectionPageRefs) {
const connections: System["system"][] = [collection_page.system]
if (collection_page.blocks) {
collection_page.blocks.forEach((block) => {
switch (block.__typename) {
case CollectionPageEnum.ContentStack.blocks.Shortcuts: {
if (block.shortcuts.shortcuts.length) {
connections.push(...block.shortcuts.shortcuts)
}
break
}
case CollectionPageEnum.ContentStack.blocks.CardsGrid: {
if (block.cards_grid.length) {
connections.push(...block.cards_grid)
}
break
}
case CollectionPageEnum.ContentStack.blocks.UspGrid: {
if (block.usp_grid.length) {
connections.push(...block.usp_grid)
}
}
}
})
}
return connections
}

View File

@@ -162,6 +162,18 @@ export function getConnections({ content_page }: ContentPageRefs) {
}
}
break
case ContentPageEnum.ContentStack.blocks.CardsGrid: {
if (block.cards_grid.length) {
connections.push(...block.cards_grid)
}
break
}
case ContentPageEnum.ContentStack.blocks.DynamicContent: {
if (block.dynamic_content.link) {
connections.push(block.dynamic_content.link)
}
break
}
case ContentPageEnum.ContentStack.blocks.Shortcuts: {
if (block.shortcuts.shortcuts.length) {
connections.push(...block.shortcuts.shortcuts)

View File

@@ -4,6 +4,7 @@ import { accountPageRouter } from "./accountPage"
import { baseRouter } from "./base"
import { bookingwidgetRouter } from "./bookingwidget"
import { breadcrumbsRouter } from "./breadcrumbs"
import { collectionPageRouter } from "./collectionPage"
import { contentPageRouter } from "./contentPage"
import { hotelPageRouter } from "./hotelPage"
import { languageSwitcherRouter } from "./languageSwitcher"
@@ -21,6 +22,7 @@ export const contentstackRouter = router({
hotelPage: hotelPageRouter,
languageSwitcher: languageSwitcherRouter,
loyaltyPage: loyaltyPageRouter,
collectionPage: collectionPageRouter,
contentPage: contentPageRouter,
myPages: myPagesRouter,
metaData: metaDataRouter,

View File

@@ -7,6 +7,10 @@ import {
GetDaDeEnUrlsAccountPage,
GetFiNoSvUrlsAccountPage,
} from "@/lib/graphql/Query/AccountPage/AccountPage.graphql"
import {
GetDaDeEnUrlsCollectionPage,
GetFiNoSvUrlsCollectionPage,
} from "@/lib/graphql/Query/CollectionPage/CollectionPage.graphql"
import {
GetDaDeEnUrlsContentPage,
GetFiNoSvUrlsContentPage,
@@ -88,6 +92,10 @@ async function getLanguageSwitcher(options: LanguageSwitcherVariables) {
daDeEnDocument = GetDaDeEnUrlsContentPage
fiNoSvDocument = GetFiNoSvUrlsContentPage
break
case PageTypeEnum.collectionPage:
daDeEnDocument = GetDaDeEnUrlsCollectionPage
fiNoSvDocument = GetFiNoSvUrlsCollectionPage
break
default:
console.error(`type: [${options.contentType}]`)
console.error(`Trying to get a content type that is not supported`)

View File

@@ -11,6 +11,7 @@ import { imageContainerSchema } from "./imageContainer"
import { BlocksEnums } from "@/types/enums/blocks"
import { CardsGridEnum, CardsGridLayoutEnum } from "@/types/enums/cardsGrid"
import { scriptedCardThemeEnum } from "@/types/enums/scriptedCard"
export const cardBlockSchema = z.object({
__typename: z.literal(CardsGridEnum.cards.Card),
@@ -161,7 +162,7 @@ export const cardsGridSchema = z.object({
}),
layout: z.nativeEnum(CardsGridLayoutEnum),
preamble: z.string().optional().default(""),
theme: z.enum(["one", "two", "three"]).nullable(),
theme: z.nativeEnum(scriptedCardThemeEnum).nullable(),
title: z.string().optional().default(""),
})
.transform((data) => {

View File

@@ -5,6 +5,7 @@ import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks"
const linkUnionSchema = z.discriminatedUnion("__typename", [
pageLinks.contentPageSchema,
pageLinks.collectionPageSchema,
pageLinks.hotelPageSchema,
pageLinks.loyaltyPageSchema,
])

View File

@@ -30,6 +30,16 @@ export const extendedPageLinkSchema = pageLinkSchema.merge(
}),
})
)
export const collectionPageSchema = z
.object({
__typename: z.literal(ContentEnum.blocks.CollectionPage),
})
.merge(extendedPageLinkSchema)
export const collectionPageRefSchema = z.object({
__typename: z.literal(ContentEnum.blocks.CollectionPage),
system: systemSchema,
})
export const contentPageSchema = z
.object({
@@ -67,6 +77,7 @@ export const loyaltyPageRefSchema = z.object({
type Data =
| z.output<typeof accountPageSchema>
| z.output<typeof contentPageSchema>
| z.output<typeof collectionPageSchema>
| z.output<typeof hotelPageSchema>
| z.output<typeof loyaltyPageSchema>
| Object
@@ -83,6 +94,7 @@ export function transform(data: Data) {
url: removeMultipleSlashes(`/${data.system.locale}/${data.url}`),
}
case ContentEnum.blocks.ContentPage:
case ContentEnum.blocks.CollectionPage:
case ContentEnum.blocks.LoyaltyPage:
// TODO: Once all links use this transform
// `web` can be removed and not to be worried
@@ -102,6 +114,7 @@ export function transform(data: Data) {
type RefData =
| z.output<typeof accountPageRefSchema>
| z.output<typeof collectionPageRefSchema>
| z.output<typeof contentPageRefSchema>
| z.output<typeof hotelPageRefSchema>
| z.output<typeof loyaltyPageRefSchema>
@@ -112,6 +125,7 @@ export function transformRef(data: RefData) {
switch (data.__typename) {
case ContentEnum.blocks.AccountPage:
case ContentEnum.blocks.ContentPage:
case ContentEnum.blocks.CollectionPage:
case ContentEnum.blocks.HotelPage:
case ContentEnum.blocks.LoyaltyPage:
return data.system

View File

@@ -7,6 +7,7 @@ import {
transformCardBlockRefs,
} from "../blocks/cardsGrid"
import { scriptedCardThemeEnum } from "@/types/enums/scriptedCard"
import { SidebarEnums } from "@/types/enums/sidebar"
export const scriptedCardsSchema = z.object({
@@ -16,17 +17,7 @@ export const scriptedCardsSchema = z.object({
.default(SidebarEnums.blocks.ScriptedCard),
scripted_card: z
.object({
theme: z
.enum([
"one",
"two",
"three",
"primaryDim",
"primaryDark",
"primaryInverted",
"primaryStrong",
])
.nullable(),
theme: z.nativeEnum(scriptedCardThemeEnum).nullable(),
scripted_cardConnection: z.object({
edges: z.array(
z.object({

View File

@@ -1,4 +1,4 @@
import { CardsGrid } from "@/types/trpc/routers/contentstack/blocks"
import type { CardsGrid } from "@/types/trpc/routers/contentstack/blocks"
export interface CardsGridProps extends Pick<CardsGrid, "cards_grid"> {
firstItem?: boolean

View File

@@ -1,8 +1,13 @@
import type { Block as AccountPageBlock } from "@/types/trpc/routers/contentstack/accountPage"
import type { Block as CollectionPageBlock } from "@/types/trpc/routers/contentstack/collectionPage"
import type { Block as ContentPageBlock } from "@/types/trpc/routers/contentstack/contentPage"
import type { Block as LoyaltyPageBlock } from "@/types/trpc/routers/contentstack/loyaltyPage"
export type Blocks = AccountPageBlock | ContentPageBlock | LoyaltyPageBlock
export type Blocks =
| AccountPageBlock
| CollectionPageBlock
| ContentPageBlock
| LoyaltyPageBlock
export interface BlocksProps {
blocks: Blocks[]

View File

@@ -5,6 +5,7 @@ import type { Lang } from "@/constants/languages"
export enum TrackingChannelEnum {
"scandic-friends" = "scandic-friends",
"static-content-page" = "static-content-page",
"collection-page" = "collection-page",
}
export type TrackingChannel = keyof typeof TrackingChannelEnum

View File

@@ -0,0 +1,9 @@
export namespace CollectionPageEnum {
export namespace ContentStack {
export const enum blocks {
CardsGrid = "CollectionPageBlocksCardsGrid",
Shortcuts = "CollectionPageBlocksShortcuts",
UspGrid = "CollectionPageBlocksUspGrid",
}
}
}

View File

@@ -1,6 +1,7 @@
export namespace ContentEnum {
export const enum blocks {
AccountPage = "AccountPage",
CollectionPage = "CollectionPage",
ContentPage = "ContentPage",
HotelPage = "HotelPage",
ImageContainer = "ImageContainer",

View File

@@ -0,0 +1,9 @@
export enum scriptedCardThemeEnum {
one = "one",
two = "two",
three = "three",
primaryDim = "primaryDim",
primaryDark = "primaryDark",
primaryInverted = "primaryInverted",
primaryStrong = "primaryStrong",
}

View File

@@ -17,7 +17,11 @@ export type StatusParams = {
}
export type ContentTypeParams = {
contentType: "loyalty-page" | "content-page" | "hotel-page"
contentType:
| "loyalty-page"
| "content-page"
| "hotel-page"
| "collection-page"
}
export type ContentTypeWebviewParams = {

View File

@@ -2,6 +2,7 @@ export enum ContentTypeEnum {
accountPage = "account_page",
loyaltyPage = "loyalty_page",
hotelPage = "hotel_page",
collectionPage = "collection_page",
contentPage = "content_page",
currentBlocksPage = "current_blocks_page",
}

View File

@@ -14,6 +14,7 @@ const entryResolveSchema = z.object({
export const validateEntryResolveSchema = z.object({
all_account_page: entryResolveSchema,
all_collection_page: entryResolveSchema,
all_content_page: entryResolveSchema,
all_loyalty_page: entryResolveSchema,
all_current_blocks_page: entryResolveSchema,

View File

@@ -3,5 +3,6 @@ export enum PageTypeEnum {
loyaltyPage = "loyalty-page",
hotelPage = "hotel-page",
contentPage = "content-page",
collectionPage = "collection-page",
currentBlocksPage = "current-blocks-page",
}

View File

@@ -0,0 +1,20 @@
import { z } from "zod"
import {
blocksSchema,
collectionPageRefsSchema,
collectionPageSchema,
} from "@/server/routers/contentstack/collectionPage/output"
export interface GetCollectionPageRefsSchema
extends z.input<typeof collectionPageRefsSchema> {}
export interface CollectionPageRefs
extends z.output<typeof collectionPageRefsSchema> {}
export interface GetCollectionPageSchema
extends z.input<typeof collectionPageSchema> {}
export interface CollectionPage extends z.output<typeof collectionPageSchema> {}
export type Block = z.output<typeof blocksSchema>