Merged in monorepo-step-1 (pull request #1080)

Migrate to a monorepo setup - step 1

* Move web to subfolder /apps/scandic-web

* Yarn + transitive deps

- Move to yarn
- design-system package removed for now since yarn doesn't
support the parameter for token (ie project currently broken)
- Add missing transitive dependencies as Yarn otherwise
prevents these imports
- VS Code doesn't pick up TS path aliases unless you open
/apps/scandic-web instead of root (will be fixed with monorepo)

* Pin framer-motion to temporarily fix typing issue

https://github.com/adobe/react-spectrum/issues/7494

* Pin zod to avoid typ error

There seems to have been a breaking change in the types
returned by zod where error is now returned as undefined
instead of missing in the type. We should just handle this
but to avoid merge conflicts just pin the dependency for
now.

* Pin react-intl version

Pin version of react-intl to avoid tiny type issue where formatMessage
does not accept a generic any more. This will be fixed in a future
commit, but to avoid merge conflicts just pin for now.

* Pin typescript version

Temporarily pin version as newer versions as stricter and results in
a type error. Will be fixed in future commit after merge.

* Setup workspaces

* Add design-system as a monorepo package

* Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN

* Fix husky for monorepo setup

* Update netlify.toml

* Add lint script to root package.json

* Add stub readme

* Fix react-intl formatMessage types

* Test netlify.toml in root

* Remove root toml

* Update netlify.toml publish path

* Remove package-lock.json

* Update build for branch/preview builds


Approved-by: Linus Flood
This commit is contained in:
Anton Gunnarsson
2025-02-26 10:36:17 +00:00
committed by Linus Flood
parent 667cab6fb6
commit 80100e7631
2731 changed files with 30986 additions and 23708 deletions

View File

@@ -0,0 +1,184 @@
import { z } from "zod"
import {
linkRefsUnionSchema,
linkUnionSchema,
transformPageLink,
} from "../pageLinks"
import { BlocksEnums } from "@/types/enums/blocks"
export const accordionItemsSchema = z.array(
z.object({
question: z.string(),
answer: z.object({
json: z.any(), // JSON
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: linkUnionSchema.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
})
)
export type Accordion = z.infer<typeof accordionSchema>
enum AccordionEnum {
ContentPageBlocksAccordionBlockAccordionsGlobalAccordion = "ContentPageBlocksAccordionBlockAccordionsGlobalAccordion",
ContentPageBlocksAccordionBlockAccordionsSpecificAccordion = "ContentPageBlocksAccordionBlockAccordionsSpecificAccordion",
DestinationCityPageBlocksAccordionBlockAccordionsGlobalAccordion = "DestinationCityPageBlocksAccordionBlockAccordionsGlobalAccordion",
DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion = "DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion",
DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion = "DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion",
DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion = "DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion",
}
export const accordionSchema = z.object({
typename: z
.literal(BlocksEnums.block.Accordion)
.optional()
.default(BlocksEnums.block.Accordion),
accordion: z
.object({
title: z.string().optional().default(""),
accordions: z.array(
z.object({
__typename: z.nativeEnum(AccordionEnum),
global_accordion: z
.object({
global_accordionConnection: z.object({
edges: z.array(
z.object({
node: z.object({
questions: accordionItemsSchema,
}),
})
),
}),
})
.optional(),
specific_accordion: z
.object({
questions: accordionItemsSchema,
})
.optional(),
})
),
})
.transform((data) => {
return {
...data,
accordions: data.accordions.flatMap((acc) => {
switch (acc.__typename) {
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion:
return (
acc.global_accordion?.global_accordionConnection.edges.flatMap(
({ node: accordionConnection }) => {
return accordionConnection.questions
}
) || []
)
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion:
return acc.specific_accordion?.questions || []
}
}),
}
}),
})
export const globalAccordionConnectionRefs = z.object({
edges: z.array(
z.object({
node: z.object({
questions: z.array(
z.object({
answer: z.object({
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: linkRefsUnionSchema,
})
),
}),
}),
})
),
}),
})
),
})
export const specificAccordionConnectionRefs = z.object({
questions: z.array(
z.object({
answer: z.object({
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: linkRefsUnionSchema,
})
),
}),
}),
})
),
})
export const accordionRefsSchema = z.object({
accordion: z
.object({
accordions: z.array(
z.object({
__typename: z.nativeEnum(AccordionEnum),
global_accordion: z
.object({
global_accordionConnection: globalAccordionConnectionRefs,
})
.optional(),
specific_accordion: specificAccordionConnectionRefs.optional(),
})
),
})
.transform((data) => {
return data.accordions.flatMap((accordion) => {
switch (accordion.__typename) {
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsGlobalAccordion:
case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsGlobalAccordion:
return (
accordion.global_accordion?.global_accordionConnection.edges.flatMap(
({ node: accordionConnection }) => {
return accordionConnection.questions.flatMap((question) =>
question.answer.embedded_itemsConnection.edges.flatMap(
({ node }) => node.system
)
)
}
) || []
)
case AccordionEnum.ContentPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationCityPageBlocksAccordionBlockAccordionsSpecificAccordion:
case AccordionEnum.DestinationCountryPageBlocksAccordionBlockAccordionsSpecificAccordion:
return (
accordion.specific_accordion?.questions.flatMap((question) =>
question.answer.embedded_itemsConnection.edges.flatMap(
({ node }) => node.system
)
) || []
)
}
})
}),
})

View File

@@ -0,0 +1,83 @@
import { z } from "zod"
import { removeMultipleSlashes } from "@/utils/url"
import { tempImageVaultAssetSchema } from "../imageVault"
import { contentPageRefSchema, contentPageSchema } from "../pageLinks"
import { HotelPageEnum } from "@/types/enums/hotelPage"
export const activitiesCardSchema = z.object({
typename: z
.literal(HotelPageEnum.ContentStack.blocks.ActivitiesCard)
.optional()
.default(HotelPageEnum.ContentStack.blocks.ActivitiesCard),
upcoming_activities_card: z
.object({
background_image: tempImageVaultAssetSchema,
body_text: z.string(),
cta_text: z.string(),
sidepeek_cta_text: z.string(),
heading: z.string(),
scripted_title: z.string().optional(),
sidepeek_slug: z.string(),
hotel_page_activities_content_pageConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [
contentPageSchema.extend({
header: z.object({
preamble: z.string(),
}),
}),
]),
})
),
}),
})
.transform((data) => {
let contentPage = { href: "", preamble: "" }
if (data.hotel_page_activities_content_pageConnection.edges.length) {
const page =
data.hotel_page_activities_content_pageConnection.edges[0].node
contentPage.preamble = page.header.preamble
if (page.web.original_url) {
contentPage.href = page.web.original_url
} else {
contentPage.href = removeMultipleSlashes(
`/${page.system.locale}/${page.url}`
)
}
}
return {
backgroundImage: data.background_image,
bodyText: data.body_text,
contentPage,
ctaText: data.cta_text,
sidepeekCtaText: data.sidepeek_cta_text,
sidepeekSlug: data.sidepeek_slug,
heading: data.heading,
scriptedTopTitle: data.scripted_title,
}
}),
})
export const activitiesCardRefSchema = z.object({
upcoming_activities_card: z
.object({
hotel_page_activities_content_pageConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [contentPageRefSchema]),
})
),
}),
})
.transform((data) => {
return (
data.hotel_page_activities_content_pageConnection.edges.flatMap(
({ node }) => node.system
) || []
)
}),
})

View File

@@ -0,0 +1,95 @@
import { z } from "zod"
import {
contentCardRefSchema,
contentCardSchema,
transformContentCard,
} from "./cards/contentCard"
import { buttonSchema } from "./utils/buttonLinkSchema"
import { linkConnectionRefsSchema } from "./utils/linkConnection"
import { BlocksEnums } from "@/types/enums/blocks"
import {
type CardGalleryFilter,
CardGalleryFilterEnum,
} from "@/types/enums/cardGallery"
export const cardGallerySchema = z.object({
typename: z
.literal(BlocksEnums.block.CardGallery)
.optional()
.default(BlocksEnums.block.CardGallery),
card_gallery: z
.object({
heading: z.string().optional(),
link: buttonSchema.optional(),
card_groups: z.array(
z.object({
filter_identifier: z.nativeEnum(CardGalleryFilterEnum),
filter_label: z.string(),
cardConnection: z.object({
edges: z.array(z.object({ node: contentCardSchema })),
}),
})
),
})
.transform((data) => {
const filterCategories = data.card_groups.reduce<
Array<{
identifier: CardGalleryFilter
label: string
}>
>((acc, group) => {
const identifier = group.filter_identifier
if (!acc.some((category) => category.identifier === identifier)) {
acc.push({
identifier,
label: group.filter_label,
})
}
return acc
}, [])
return {
heading: data.heading,
filterCategories,
cards: data.card_groups.flatMap((group) =>
group.cardConnection.edges
.map((edge) => transformContentCard(edge.node))
.filter((card): card is NonNullable<typeof card> => card !== null)
.map((card) => ({
...card,
filterId: group.filter_identifier,
}))
),
defaultFilter:
data.card_groups[0]?.filter_identifier ??
filterCategories[0]?.identifier,
link:
data.link?.href && data.link.title
? { href: data.link.href, text: data.link.title }
: undefined,
}
}),
})
export const cardGalleryRefsSchema = z.object({
typename: z
.literal(BlocksEnums.block.CardGallery)
.optional()
.default(BlocksEnums.block.CardGallery),
card_gallery: z.object({
card_groups: z.array(
z.object({
cardConnection: z.object({
edges: z.array(
z.object({
node: contentCardRefSchema,
})
),
}),
})
),
link: linkConnectionRefsSchema.optional(),
}),
})

View File

@@ -0,0 +1,47 @@
import { z } from "zod"
import { tempImageVaultAssetSchema } from "../../imageVault"
import { systemSchema } from "../../system"
import { buttonSchema } from "../utils/buttonLinkSchema"
import { linkConnectionRefsSchema } from "../utils/linkConnection"
import { CardsEnum } from "@/types/enums/cards"
export const contentCardSchema = z.object({
__typename: z.literal(CardsEnum.ContentCard),
title: z.string(),
heading: z.string(),
image: tempImageVaultAssetSchema,
body_text: z.string(),
promo_text: z.string().optional(),
has_card_link: z.boolean(),
card_link: buttonSchema,
system: systemSchema,
})
export const contentCardRefSchema = z.object({
__typename: z.literal(CardsEnum.ContentCard),
card_link: linkConnectionRefsSchema,
system: systemSchema,
})
export function transformContentCard(card: typeof contentCardSchema._type) {
// Return null if image or image URL is missing
if (!card.image?.url) return null
return {
__typename: card.__typename,
title: card.title,
heading: card.heading,
image: card.image,
bodyText: card.body_text,
promoText: card.promo_text,
link: card.has_card_link
? {
href: card.card_link.href,
openInNewTab: card.card_link.openInNewTab,
isExternal: card.card_link.isExternal,
}
: undefined,
}
}

View File

@@ -0,0 +1,46 @@
import { z } from "zod"
import { tempImageVaultAssetSchema } from "../../imageVault"
import { systemSchema } from "../../system"
import { buttonSchema } from "../utils/buttonLinkSchema"
import { linkConnectionRefsSchema } from "../utils/linkConnection"
import { INFO_CARD_THEMES } from "@/types/components/blocks/infoCard"
import { CardsEnum } from "@/types/enums/cards"
export const infoCardBlockSchema = z.object({
__typename: z.literal(CardsEnum.InfoCard),
scripted_top_title: z.string().optional(),
heading: z.string().optional().default(""),
body_text: z.string().optional().default(""),
image: tempImageVaultAssetSchema,
theme: z.enum(INFO_CARD_THEMES).nullable(),
title: z.string().optional(),
primary_button: buttonSchema.optional().nullable(),
secondary_button: buttonSchema.optional().nullable(),
system: systemSchema,
})
export function transformInfoCardBlock(card: typeof infoCardBlockSchema._type) {
return {
__typename: card.__typename,
scriptedTopTitle: card.scripted_top_title,
heading: card.heading,
bodyText: card.body_text,
image: card.image,
theme: card.theme,
title: card.title,
primaryButton: card.primary_button?.href ? card.primary_button : undefined,
secondaryButton: card.secondary_button?.href
? card.secondary_button
: undefined,
system: card.system,
}
}
export const infoCardBlockRefsSchema = z.object({
__typename: z.literal(CardsEnum.InfoCard),
primary_button: linkConnectionRefsSchema,
secondary_button: linkConnectionRefsSchema,
system: systemSchema,
})

View File

@@ -0,0 +1,25 @@
import { z } from "zod"
import { tempImageVaultAssetSchema } from "../../imageVault"
import { systemSchema } from "../../system"
import { buttonSchema } from "../utils/buttonLinkSchema"
import { linkConnectionRefsSchema } from "../utils/linkConnection"
import { CardsEnum } from "@/types/enums/cards"
export const loyaltyCardBlockSchema = z.object({
__typename: z.literal(CardsEnum.LoyaltyCard),
body_text: z.string().optional(),
heading: z.string().optional().default(""),
// JSON - ImageVault Image
image: tempImageVaultAssetSchema,
link: buttonSchema,
system: systemSchema,
title: z.string().optional(),
})
export const loyaltyCardBlockRefsSchema = z.object({
__typename: z.literal(CardsEnum.LoyaltyCard),
link: linkConnectionRefsSchema,
system: systemSchema,
})

View File

@@ -0,0 +1,121 @@
import { z } from "zod"
import { tempImageVaultAssetSchema } from "../../imageVault"
import {
accountPageSchema,
collectionPageSchema,
contentPageSchema,
destinationCityPageSchema,
destinationCountryPageSchema,
destinationOverviewPageSchema,
hotelPageSchema,
loyaltyPageSchema,
startPageSchema,
transformPageLink,
} from "../../pageLinks"
import { systemSchema } from "../../system"
import { imageSchema } from "../image"
import { imageContainerSchema } from "../imageContainer"
import { buttonSchema } from "../utils/buttonLinkSchema"
import { linkConnectionRefsSchema } from "../utils/linkConnection"
import { CardsEnum } from "@/types/enums/cards"
export const teaserCardBlockSchema = z.object({
__typename: z.literal(CardsEnum.TeaserCard),
heading: z.string().default(""),
body_text: z.string().default(""),
image: tempImageVaultAssetSchema,
primary_button: buttonSchema,
secondary_button: buttonSchema,
has_primary_button: z.boolean().default(false),
has_secondary_button: z.boolean().default(false),
has_sidepeek_button: z.boolean().default(false),
sidepeek_button: z
.object({
call_to_action_text: z.string().optional().default(""),
})
.optional(),
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", [
imageContainerSchema,
imageSchema,
accountPageSchema,
collectionPageSchema,
contentPageSchema,
destinationCityPageSchema,
destinationCountryPageSchema,
destinationOverviewPageSchema,
hotelPageSchema,
loyaltyPageSchema,
startPageSchema,
])
.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
has_primary_button: z.boolean().default(false),
primary_button: buttonSchema,
has_secondary_button: z.boolean().default(false),
secondary_button: buttonSchema,
})
.optional()
.transform((data) => {
if (!data) {
return
}
return {
...data,
primary_button: data.has_primary_button
? data.primary_button
: undefined,
secondary_button: data.has_secondary_button
? data.secondary_button
: undefined,
}
}),
system: systemSchema,
})
export function transformTeaserCardBlock(
card: typeof teaserCardBlockSchema._type
) {
return {
__typename: card.__typename,
body_text: card.body_text,
heading: card.heading,
primaryButton: card.has_primary_button ? card.primary_button : undefined,
secondaryButton: card.has_secondary_button
? card.secondary_button
: undefined,
sidePeekButton: card.has_sidepeek_button ? card.sidepeek_button : undefined,
sidePeekContent: card.has_sidepeek_button
? card.sidepeek_content
: undefined,
image: card.image,
system: card.system,
}
}
export const teaserCardBlockRefsSchema = z.object({
__typename: z.literal(CardsEnum.TeaserCard),
primary_button: linkConnectionRefsSchema,
secondary_button: linkConnectionRefsSchema,
system: systemSchema,
})

View File

@@ -0,0 +1,168 @@
import { z } from "zod"
import { tempImageVaultAssetSchema } from "../imageVault"
import { systemSchema } from "../system"
import {
infoCardBlockRefsSchema,
infoCardBlockSchema,
transformInfoCardBlock,
} from "./cards/infoCard"
import {
loyaltyCardBlockRefsSchema,
loyaltyCardBlockSchema,
} from "./cards/loyaltyCard"
import {
teaserCardBlockRefsSchema,
teaserCardBlockSchema,
transformTeaserCardBlock,
} from "./cards/teaserCard"
import { buttonSchema } from "./utils/buttonLinkSchema"
import { linkConnectionRefsSchema } from "./utils/linkConnection"
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),
// JSON - ImageVault Image
background_image: tempImageVaultAssetSchema,
body_text: z.string().optional().default(""),
has_primary_button: z.boolean().default(false),
has_secondary_button: z.boolean().default(false),
heading: z.string().optional().default(""),
primary_button: buttonSchema,
scripted_top_title: z.string().optional(),
secondary_button: buttonSchema,
system: systemSchema,
title: z.string().optional(),
})
export function transformCardBlock(card: typeof cardBlockSchema._type) {
return {
__typename: card.__typename,
backgroundImage: card.background_image,
body_text: card.body_text,
heading: card.heading,
primaryButton: card.has_primary_button ? card.primary_button : undefined,
scripted_top_title: card.scripted_top_title,
secondaryButton: card.has_secondary_button
? card.secondary_button
: undefined,
system: card.system,
title: card.title,
}
}
export const cardsGridSchema = z.object({
typename: z
.literal(BlocksEnums.block.CardsGrid)
.optional()
.default(BlocksEnums.block.CardsGrid),
cards_grid: z
.object({
cardConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [
cardBlockSchema,
loyaltyCardBlockSchema,
teaserCardBlockSchema,
infoCardBlockSchema,
]),
})
),
}),
layout: z.nativeEnum(CardsGridLayoutEnum),
preamble: z.string().optional().default(""),
theme: z.nativeEnum(scriptedCardThemeEnum).nullable(),
title: z.string().optional().default(""),
})
.transform((data) => {
return {
layout: data.layout,
preamble: data.preamble,
theme: data.theme,
title: data.title,
cards: data.cardConnection.edges.map((card) => {
if (card.node.__typename === CardsGridEnum.cards.Card) {
return transformCardBlock(card.node)
} else if (card.node.__typename === CardsGridEnum.cards.TeaserCard) {
return transformTeaserCardBlock(card.node)
} else if (card.node.__typename === CardsGridEnum.cards.InfoCard) {
return transformInfoCardBlock(card.node)
} else {
return {
__typename: card.node.__typename,
body_text: card.node.body_text,
heading: card.node.heading,
image: card.node.image,
link: card.node.link,
system: card.node.system,
title: card.node.title,
}
}
}),
}
}),
})
export const cardBlockRefsSchema = z.object({
__typename: z.literal(CardsGridEnum.cards.Card),
primary_button: linkConnectionRefsSchema,
secondary_button: linkConnectionRefsSchema,
system: systemSchema,
})
export function transformCardBlockRefs(
card:
| typeof cardBlockRefsSchema._type
| typeof teaserCardBlockRefsSchema._type
| typeof infoCardBlockRefsSchema._type
) {
const cards = [card.system]
if (card.primary_button) {
cards.push(card.primary_button)
}
if (card.secondary_button) {
cards.push(card.secondary_button)
}
return cards
}
export const cardGridRefsSchema = z.object({
cards_grid: z
.object({
cardConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [
cardBlockRefsSchema,
loyaltyCardBlockRefsSchema,
teaserCardBlockRefsSchema,
infoCardBlockRefsSchema,
]),
})
),
}),
})
.transform((data) => {
return data.cardConnection.edges
.map(({ node }) => {
if (
node.__typename === CardsGridEnum.cards.Card ||
node.__typename === CardsGridEnum.cards.TeaserCard ||
node.__typename === CardsGridEnum.cards.InfoCard
) {
return transformCardBlockRefs(node)
} else {
const loyaltyCards = [node.system]
if (node.link) {
loyaltyCards.push(node.link)
}
return loyaltyCards
}
})
.flat()
}),
})

View File

@@ -0,0 +1,139 @@
import { z } from "zod"
import {
contentCardRefSchema,
contentCardSchema,
transformContentCard,
} from "./cards/contentCard"
import { buttonSchema } from "./utils/buttonLinkSchema"
import { linkConnectionRefsSchema } from "./utils/linkConnection"
import { BlocksEnums } from "@/types/enums/blocks"
import {
type CarouselCardFilter,
CarouselCardFilterEnum,
} from "@/types/enums/carouselCards"
const commonFields = {
heading: z.string().optional(),
link: buttonSchema.optional(),
} as const
const carouselCardsWithFilters = z.object({
...commonFields,
enable_filters: z.literal(true),
card_groups: z.array(
z.object({
filter_identifier: z.nativeEnum(CarouselCardFilterEnum),
filter_label: z.string(),
cardConnection: z.object({
edges: z.array(z.object({ node: contentCardSchema })),
}),
})
),
})
const carouselCardsWithoutFilters = z.object({
...commonFields,
enable_filters: z.literal(false),
card_groups: z.array(
z.object({
filter_identifier: z.null(),
filter_label: z.string(),
cardConnection: z.object({
edges: z.array(z.object({ node: contentCardSchema })),
}),
})
),
})
export const carouselCardsSchema = z.object({
typename: z
.literal(BlocksEnums.block.CarouselCards)
.optional()
.default(BlocksEnums.block.CarouselCards),
carousel_cards: z
.discriminatedUnion("enable_filters", [
carouselCardsWithFilters,
carouselCardsWithoutFilters,
])
.transform((data) => {
if (!data.enable_filters) {
return {
heading: data.heading,
enableFilters: false,
filterCategories: [],
cards: data.card_groups
.flatMap((group) =>
group.cardConnection.edges.map((edge) =>
transformContentCard(edge.node)
)
)
.filter((card): card is NonNullable<typeof card> => card !== null),
link:
data.link?.href && data.link.title
? { href: data.link.href, text: data.link.title }
: undefined,
}
}
const filterCategories = data.card_groups.reduce<
Array<{
identifier: CarouselCardFilter
label: string
}>
>((acc, group) => {
const identifier = group.filter_identifier
if (!acc.some((category) => category.identifier === identifier)) {
acc.push({
identifier,
label: group.filter_label,
})
}
return acc
}, [])
return {
heading: data.heading,
enableFilters: true,
filterCategories,
cards: data.card_groups.flatMap((group) =>
group.cardConnection.edges
.map((edge) => transformContentCard(edge.node))
.filter((card): card is NonNullable<typeof card> => card !== null)
.map((card) => ({
...card,
filterId: group.filter_identifier,
}))
),
defaultFilter:
data.card_groups[0]?.filter_identifier ??
filterCategories[0]?.identifier,
link:
data.link?.href && data.link.title
? { href: data.link.href, text: data.link.title }
: undefined,
}
}),
})
export const carouselCardsRefsSchema = z.object({
typename: z
.literal(BlocksEnums.block.CarouselCards)
.optional()
.default(BlocksEnums.block.CarouselCards),
carousel_cards: z.object({
card_groups: z.array(
z.object({
cardConnection: z.object({
edges: z.array(
z.object({
node: contentCardRefSchema,
})
),
}),
})
),
link: linkConnectionRefsSchema.optional(),
}),
})

View File

@@ -0,0 +1,103 @@
import { z } from "zod"
import {
accountPageSchema,
collectionPageSchema,
contentPageSchema,
destinationCityPageSchema,
destinationCountryPageSchema,
destinationOverviewPageSchema,
hotelPageSchema,
loyaltyPageSchema,
startPageSchema,
transformPageLink,
} from "../pageLinks"
import { imageRefsSchema, imageSchema } from "./image"
import {
imageContainerRefsSchema,
imageContainerSchema,
} from "./imageContainer"
import { BlocksEnums } from "@/types/enums/blocks"
import { ContentEnum } from "@/types/enums/content"
export const contentSchema = z.object({
typename: z
.literal(BlocksEnums.block.Content)
.optional()
.default(BlocksEnums.block.Content),
content: z
.object({
content: z.object({
json: z.any(), // JSON
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: z
.discriminatedUnion("__typename", [
imageContainerSchema,
imageSchema,
accountPageSchema,
collectionPageSchema,
contentPageSchema,
destinationCityPageSchema,
destinationCountryPageSchema,
destinationOverviewPageSchema,
hotelPageSchema,
loyaltyPageSchema,
startPageSchema,
])
.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
})
.transform((data) => {
return data.content
}),
})
export const contentRefsSchema = z.object({
content: z
.object({
content: z.object({
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [
imageRefsSchema,
imageContainerRefsSchema,
accountPageSchema,
collectionPageSchema,
contentPageSchema,
destinationCityPageSchema,
destinationCountryPageSchema,
destinationOverviewPageSchema,
hotelPageSchema,
loyaltyPageSchema,
startPageSchema,
]),
})
),
}),
}),
})
.transform((data) => {
return data.content.embedded_itemsConnection.edges
.filter(({ node }) => node.__typename !== ContentEnum.blocks.SysAsset)
.map(({ node }) => {
if ("system" in node) {
return node.system
}
return null
})
.filter((node) => !!node)
}),
})

View File

@@ -0,0 +1,38 @@
import { z } from "zod"
import {
accountPageSchema,
collectionPageSchema,
contentPageSchema,
destinationCityPageSchema,
destinationCountryPageSchema,
destinationOverviewPageSchema,
hotelPageSchema,
loyaltyPageSchema,
startPageSchema,
transformPageLink,
} from "../pageLinks"
import { imageSchema } from "./image"
import { imageContainerSchema } from "./imageContainer"
export const contentEmbedsSchema = z
.discriminatedUnion("__typename", [
imageContainerSchema,
imageSchema,
accountPageSchema,
collectionPageSchema,
contentPageSchema,
destinationCityPageSchema,
destinationCountryPageSchema,
destinationOverviewPageSchema,
hotelPageSchema,
loyaltyPageSchema,
startPageSchema,
])
.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
})

View File

@@ -0,0 +1,71 @@
import { z } from "zod"
import {
linkRefsUnionSchema,
linkUnionSchema,
transformPageLink,
} from "../pageLinks"
import { BlocksEnums } from "@/types/enums/blocks"
import { DynamicContentEnum } from "@/types/enums/dynamicContent"
export const dynamicContentSchema = z.object({
typename: z
.literal(BlocksEnums.block.DynamicContent)
.optional()
.default(BlocksEnums.block.DynamicContent),
dynamic_content: z.object({
component: z.enum(DynamicContentEnum.Blocks.enums),
subtitle: z.string().optional().default(""),
title: z.string().optional().default(""),
link: z
.object({
text: z.string().optional().default(""),
linkConnection: z.object({
edges: z.array(
z.object({
node: linkUnionSchema.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
})
.transform((data) => {
if (data.linkConnection?.edges.length) {
const link = data.linkConnection.edges?.[0]?.node
return {
href: link.url,
text: data.text,
title: link.title,
}
}
return undefined
}),
}),
})
export const dynamicContentRefsSchema = z.object({
dynamic_content: z.object({
link: z
.object({
linkConnection: z.object({
edges: z.array(
z.object({
node: linkRefsUnionSchema,
})
),
}),
})
.transform((data) => {
if (data.linkConnection?.edges.length) {
return data.linkConnection.edges[0].node.system
}
return null
}),
}),
})

View File

@@ -0,0 +1,65 @@
import { z } from "zod"
import * as pageLinks from "@/server/routers/contentstack/schemas/pageLinks"
import { tempImageVaultAssetSchema } from "../imageVault"
import { systemSchema } from "../system"
import { buttonSchema } from "./utils/buttonLinkSchema"
import { BlocksEnums } from "@/types/enums/blocks"
export const fullWidthCampaignSchema = z.object({
full_width_campaign: z
.object({
full_width_campaignConnection: z.object({
edges: z.array(
z.object({
node: z.object({
background_image: tempImageVaultAssetSchema,
heading: z.string().optional(),
body_text: z.string().optional(),
scripted_top_title: z.string().optional(),
has_primary_button: z.boolean().default(false),
primary_button: buttonSchema,
has_secondary_button: z.boolean().default(false),
secondary_button: buttonSchema,
system: systemSchema,
}),
})
),
}),
})
.transform((data) => {
return data.full_width_campaignConnection.edges[0]?.node || null
}),
})
export const fullWidthCampaignBlockSchema = z
.object({
typename: z
.literal(BlocksEnums.block.FullWidthCampaign)
.optional()
.default(BlocksEnums.block.FullWidthCampaign),
})
.merge(fullWidthCampaignSchema)
export const fullWidthCampaignBlockRefsSchema = z.object({
full_width_campaign: z.object({
full_width_campaignConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [
pageLinks.accountPageRefSchema,
pageLinks.contentPageRefSchema,
pageLinks.loyaltyPageRefSchema,
pageLinks.collectionPageRefSchema,
pageLinks.hotelPageRefSchema,
pageLinks.destinationCityPageRefSchema,
pageLinks.destinationCountryPageRefSchema,
pageLinks.destinationOverviewPageRefSchema,
]),
})
),
}),
}),
})

View File

@@ -0,0 +1,76 @@
import { z } from "zod"
import {
accordionItemsSchema,
globalAccordionConnectionRefs,
specificAccordionConnectionRefs,
} from "./accordion"
import { BlocksEnums } from "@/types/enums/blocks"
import { HotelPageEnum } from "@/types/enums/hotelPage"
export const hotelFaqSchema = z
.object({
typename: z
.literal(BlocksEnums.block.Accordion)
.optional()
.default(BlocksEnums.block.Accordion),
title: z.string().optional().default(""),
global_faqConnection: z
.object({
edges: z.array(
z.object({
node: z.object({
questions: accordionItemsSchema,
}),
})
),
})
.optional(),
specific_faq: z
.object({
questions: accordionItemsSchema,
})
.optional()
.nullable(),
})
.transform((data) => {
const array = []
array.push(
data.global_faqConnection?.edges.flatMap(({ node: faqConnection }) => {
return faqConnection.questions
}) || []
)
array.push(data.specific_faq?.questions || [])
return { ...data, accordions: array.flat(2) }
})
export const hotelFaqRefsSchema = z
.object({
__typename: z
.literal(HotelPageEnum.ContentStack.blocks.Faq)
.optional()
.default(HotelPageEnum.ContentStack.blocks.Faq),
global_faqConnection: globalAccordionConnectionRefs.optional(),
specific_faq: specificAccordionConnectionRefs.optional().nullable(),
})
.transform((data) => {
const array = []
array.push(
data.global_faqConnection?.edges.flatMap(({ node: faqConnection }) => {
return faqConnection.questions.flatMap((question) =>
question.answer.embedded_itemsConnection.edges.flatMap(
({ node }) => node.system
)
)
}) || []
)
array.push(
data.specific_faq?.questions.flatMap((question) =>
question.answer.embedded_itemsConnection.edges.flatMap(
({ node }) => node.system
)
) || []
)
return array.flat(2)
})

View File

@@ -0,0 +1,62 @@
import { z } from "zod"
import { BlocksEnums } from "@/types/enums/blocks"
import { Country } from "@/types/enums/country"
export const locationFilterSchema = z
.object({
country: z.nativeEnum(Country).nullable(),
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(),
excluded: z.array(z.string()),
})
.transform((data) => {
const cities = [
data.city_denmark,
data.city_finland,
data.city_germany,
data.city_poland,
data.city_norway,
data.city_sweden,
].filter((city): city is string => Boolean(city))
// When there are multiple city values, we return null as the filter is invalid.
if (cities.length > 1) {
return null
}
return {
country: cities.length ? null : data.country,
city: cities.length ? cities[0] : null,
excluded: data.excluded,
}
})
export const hotelListingSchema = z.object({
typename: z
.literal(BlocksEnums.block.HotelListing)
.default(BlocksEnums.block.HotelListing),
hotel_listing: z
.object({
heading: z.string().optional(),
location_filter: locationFilterSchema,
manual_filter: z
.object({
hotels: z.array(z.string()),
})
.transform((data) => ({ hotels: data.hotels.filter(Boolean) })),
content_type: z.enum(["hotel", "restaurant", "meeting"]),
})
.transform(({ heading, location_filter, manual_filter, content_type }) => {
return {
heading,
locationFilter: location_filter,
hotelsToInclude: manual_filter.hotels,
contentType: content_type,
}
}),
})

View File

@@ -0,0 +1,30 @@
import { z } from "zod"
import { ContentEnum } from "@/types/enums/content"
export const imageSchema = z.object({
__typename: z.literal(ContentEnum.blocks.SysAsset),
content_type: z.string(),
description: z.string().nullable().optional(),
dimension: z
.object({
height: z.number(),
width: z.number(),
})
.nullable(),
metadata: z.any(), // JSON
// system for SysAssets is not the same type
// as for all other types eventhough they have
// the exact same structure, that's why systemSchema
// is not used as that correlates to the
// EntrySystemField type
system: z.object({
uid: z.string(),
}),
title: z.string().optional(),
url: z.string().optional(),
})
export const imageRefsSchema = z.object({
__typename: z.literal(ContentEnum.blocks.SysAsset),
})

View File

@@ -0,0 +1,21 @@
import { z } from "zod"
import { tempImageVaultAssetSchema } from "../imageVault"
import { systemSchema } from "../system"
import { ContentEnum } from "@/types/enums/content"
export const imageContainerSchema = z.object({
__typename: z.literal(ContentEnum.blocks.ImageContainer),
// JSON - ImageVault Image
image_left: tempImageVaultAssetSchema,
// JSON - ImageVault Image
image_right: tempImageVaultAssetSchema,
system: systemSchema,
title: z.string().optional(),
})
export const imageContainerRefsSchema = z.object({
__typename: z.literal(ContentEnum.blocks.ImageContainer),
system: systemSchema,
})

View File

@@ -0,0 +1,36 @@
import { z } from "zod"
import { tempImageVaultAssetSchema } from "../imageVault"
import { buttonSchema } from "./utils/buttonLinkSchema"
import { linkConnectionRefsSchema } from "./utils/linkConnection"
import { BlocksEnums } from "@/types/enums/blocks"
export const joinScandicFriendsSchema = z.object({
join_scandic_friends: z.object({
show_header: z.boolean().default(false),
scripted_top_title: z.string(),
title: z.string(),
preamble: z.string(),
image: tempImageVaultAssetSchema,
show_usp: z.boolean().default(false),
usp: z.array(z.string()),
has_primary_button: z.boolean().default(false),
primary_button: buttonSchema,
}),
})
export const joinScandicFriendsBlockSchema = z
.object({
typename: z
.literal(BlocksEnums.block.JoinScandicFriends)
.optional()
.default(BlocksEnums.block.JoinScandicFriends),
})
.merge(joinScandicFriendsSchema)
export const joinScandicFriendsBlockRefsSchema = z.object({
join_scandic_friends: z.object({
primary_button: linkConnectionRefsSchema,
}),
})

View File

@@ -0,0 +1,90 @@
import { z } from "zod"
import {
linkRefsUnionSchema,
linkUnionSchema,
transformPageLink,
} from "../pageLinks"
import { BlocksEnums } from "@/types/enums/blocks"
export const shortcutsBlockSchema = z.object({
shortcuts: z
.object({
subtitle: z.string().nullable(),
title: z.string().nullable(),
two_column_list: z.boolean().nullable().default(false),
shortcuts: z
.array(
z.object({
open_in_new_tab: z.boolean(),
text: z.string().optional().default(""),
linkConnection: z.object({
edges: z.array(
z.object({
node: linkUnionSchema.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
})
)
.transform((data) => {
return data
.filter((node) => node.linkConnection.edges.length)
.map((node) => {
const link = node.linkConnection.edges[0].node
return {
openInNewTab: node.open_in_new_tab,
text: node.text,
title: link.title,
url: link.url,
}
})
}),
})
.transform(({ two_column_list, ...rest }) => {
return {
...rest,
hasTwoColumns: !!two_column_list,
}
}),
})
export const shortcutsSchema = z
.object({
typename: z
.literal(BlocksEnums.block.Shortcuts)
.optional()
.default(BlocksEnums.block.Shortcuts),
})
.merge(shortcutsBlockSchema)
export const shortcutsRefsSchema = z.object({
shortcuts: z.object({
shortcuts: z
.array(
z.object({
linkConnection: z.object({
edges: z.array(
z.object({
node: linkRefsUnionSchema,
})
),
}),
})
)
.transform((data) =>
data
.map((shortcut) => {
return shortcut.linkConnection.edges.map(({ node }) => node.system)
})
.flat()
),
}),
})

View File

@@ -0,0 +1,68 @@
import { z } from "zod"
import { removeMultipleSlashes } from "@/utils/url"
import { collectionPageRefSchema, contentPageRefSchema } from "../pageLinks"
import { HotelPageEnum } from "@/types/enums/hotelPage"
export const spaPageSchema = z.object({
typename: z
.literal(HotelPageEnum.ContentStack.blocks.SpaPage)
.optional()
.default(HotelPageEnum.ContentStack.blocks.SpaPage),
spa_page: z
.object({
button_cta: z.string(),
pageConnection: z.object({
edges: z.array(
z.object({
node: z.object({
title: z.string(),
url: z.string(),
system: z.object({
content_type_uid: z.string(),
locale: z.string(),
uid: z.string(),
}),
web: z.object({ original_url: z.string() }),
}),
})
),
}),
})
.transform((data) => {
let url = ""
if (data.pageConnection.edges.length) {
const page = data.pageConnection.edges[0].node
if (page.web.original_url) {
url = page.web.original_url
} else {
url = removeMultipleSlashes(`/${page.system.locale}/${page.url}`)
}
}
return {
buttonCTA: data.button_cta,
url: url,
}
}),
})
export const spaPageRefSchema = z.object({
spa_page: z
.object({
pageConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [
contentPageRefSchema,
collectionPageRefSchema,
]),
})
),
}),
})
.transform((data) => {
return data.pageConnection.edges.flatMap(({ node }) => node.system) || []
}),
})

View File

@@ -0,0 +1,58 @@
import { z } from "zod"
import { BlocksEnums } from "@/types/enums/blocks"
export const tableSchema = z.object({
typename: z
.literal(BlocksEnums.block.Table)
.optional()
.default(BlocksEnums.block.Table),
table: z
.object({
heading: z.string().optional(),
preamble: z.string().optional().default(""),
column_widths: z.array(z.number()),
table: z.object({
tableState: z.object({
columns: z.array(
z.object({
id: z.string(),
label: z.string().default(""),
accessor: z.string(),
dataType: z.string(),
})
),
data: z.array(z.object({}).catchall(z.string())),
skipReset: z.boolean(),
tableActionEnabled: z.boolean(),
headerRowAdded: z.boolean().optional().default(false),
}),
}),
})
.transform((data) => {
const totalWidth = data.column_widths.reduce(
(acc, width) => acc + width,
0
)
const columns = data.table.tableState.columns.map((col, idx) => ({
id: col.id,
header: col.label || "",
width: data.column_widths[idx] || 0,
}))
const rows = data.table.tableState.data.map((rowData) =>
columns.reduce<Record<string, string>>((transformedRow, column) => {
transformedRow[column.id] = rowData[column.id] || ""
return transformedRow
}, {})
)
return {
heading: data.heading,
preamble: data.preamble,
columns,
rows,
totalWidth,
}
}),
})

View File

@@ -0,0 +1,82 @@
import { z } from "zod"
import {
linkRefsUnionSchema,
linkUnionSchema,
transformPageLink,
} from "../pageLinks"
import { imageRefsSchema, imageSchema } from "./image"
import { BlocksEnums } from "@/types/enums/blocks"
import { ContentEnum } from "@/types/enums/content"
export const textColsSchema = z.object({
typename: z
.literal(BlocksEnums.block.TextCols)
.optional()
.default(BlocksEnums.block.TextCols),
text_cols: z.object({
columns: z.array(
z.object({
title: z.string().optional().default(""),
text: z.object({
json: z.any(), // JSON
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: z
.discriminatedUnion("__typename", [
imageSchema,
...linkUnionSchema.options,
])
.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
})
),
}),
})
type Refs = {
node: z.TypeOf<typeof linkUnionSchema>
}
export const textColsRefsSchema = z.object({
text_cols: z
.object({
columns: z.array(
z.object({
text: z.object({
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [
imageRefsSchema,
...linkRefsUnionSchema.options,
]),
})
),
}),
}),
})
),
})
.transform((data) => {
return data.columns
.map((column) => {
const filtered = column.text.embedded_itemsConnection.edges.filter(
(block) => block.node.__typename !== ContentEnum.blocks.SysAsset
) as unknown as Refs[] // TS issue with filtered out types
return filtered.map(({ node }) => node.system)
})
.flat()
}),
})

View File

@@ -0,0 +1,25 @@
import { z } from "zod"
import { imageSchema } from "./image"
import { BlocksEnums } from "@/types/enums/blocks"
export const textContentSchema = z.object({
typename: z
.literal(BlocksEnums.block.TextContent)
.optional()
.default(BlocksEnums.block.TextContent),
text_content: z.object({
content: z.object({
json: z.any(),
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: z.discriminatedUnion("__typename", [imageSchema]),
})
),
totalCount: z.number(),
}),
}),
}),
})

View File

@@ -0,0 +1,92 @@
import { z } from "zod"
import {
linkRefsUnionSchema,
linkUnionSchema,
transformPageLink,
} from "../pageLinks"
import { BlocksEnums } from "@/types/enums/blocks"
import { UspGridEnum } from "@/types/enums/uspGrid"
const uspCardSchema = z.object({
icon: UspGridEnum.uspIcons,
text: z.object({
json: z.any(), // JSON
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: linkUnionSchema.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
})
export const uspGridSchema = z.object({
typename: z
.literal(BlocksEnums.block.UspGrid)
.optional()
.default(BlocksEnums.block.UspGrid),
usp_grid: z
.object({
cardsConnection: z.object({
edges: z.array(
z.object({
node: z.object({
usp_card: z.array(uspCardSchema),
}),
})
),
}),
})
.transform((data) => {
return {
usp_card: data.cardsConnection.edges.flatMap(
(edge) => edge.node.usp_card
),
}
}),
})
export const uspGridRefsSchema = z.object({
usp_grid: z
.object({
cardsConnection: z.object({
edges: z.array(
z.object({
node: z.object({
usp_card: z.array(
z.object({
text: z.object({
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: linkRefsUnionSchema,
})
),
}),
}),
})
),
}),
})
),
}),
})
.transform((data) => {
return data.cardsConnection.edges.flatMap(({ node }) =>
node.usp_card.flatMap((card) =>
card.text.embedded_itemsConnection.edges.map(
({ node }) => node.system
)
)
)
}),
})

View File

@@ -0,0 +1,52 @@
import { z } from "zod"
import { linkUnionSchema, transformPageLink } from "../../pageLinks"
export const buttonSchema = z
.object({
cta_text: z.string().optional().default(""),
open_in_new_tab: z.boolean().default(false),
is_contentstack_link: z.boolean().optional(),
external_link: z
.object({
href: z.string().optional().default(""),
title: z.string().optional(),
})
.optional(),
linkConnection: z.object({
edges: z.array(
z.object({
node: linkUnionSchema.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
})
.transform((data) => {
if (
data.linkConnection?.edges?.length &&
data.is_contentstack_link !== false
) {
const link = data.linkConnection.edges[0].node
return {
href: link.url,
isExternal: false,
openInNewTab: data.open_in_new_tab,
title: data.cta_text ? data.cta_text : "",
}
} else {
return {
href: data.external_link?.href ?? "",
isExternal: true,
openInNewTab: data.open_in_new_tab,
title: data.external_link?.title
? data.external_link?.title
: data.cta_text,
}
}
})

View File

@@ -0,0 +1,21 @@
import { z } from "zod"
import { linkRefsUnionSchema } from "../../pageLinks"
export const linkConnectionRefsSchema = z
.object({
linkConnection: z.object({
edges: z.array(
z.object({
node: linkRefsUnionSchema,
})
),
}),
})
.transform((data) => {
if (!data.linkConnection.edges.length) {
return null
}
return data.linkConnection.edges[0].node.system
})