feat/BOOK-755 alert content pages

* feat(BOOK-755): Added alert block on Collection pages
* feat(BOOK-755): Added alert block on Content pages
* feat(BOOK-755): Added alert functionality for RTE

Approved-by: Bianca Widstam
This commit is contained in:
Erik Tiekstra
2026-01-28 07:47:49 +00:00
parent 0d357a116b
commit e87bb03c6f
22 changed files with 250 additions and 101 deletions

View File

@@ -12,9 +12,11 @@ import { HotelPageLink } from "./PageLink/HotelPageLink.graphql"
import { LoyaltyPageLink } from "./PageLink/LoyaltyPageLink.graphql"
import { PromoCampaignPageLink } from "./PageLink/PromoCampaignPageLink.graphql"
import { StartPageLink } from "./PageLink/StartPageLink.graphql"
import { System } from "./System.graphql"
export const Alert = gql`
fragment Alert on Alert {
__typename
type
heading
text
@@ -76,6 +78,9 @@ export const Alert = gql`
}
}
visible_on
system {
...System
}
}
${AccountPageLink}
@@ -90,4 +95,5 @@ export const Alert = gql`
${LoyaltyPageLink}
${StartPageLink}
${PromoCampaignPageLink}
${System}
`

View File

@@ -0,0 +1,35 @@
import { gql } from "graphql-tag"
import { Alert } from "../Alert.graphql"
export const Alert_ContentPage = gql`
fragment Alert_ContentPage on ContentPageBlocksAlert {
__typename
alert {
alertConnection {
edges {
node {
...Alert
}
}
}
}
}
${Alert}
`
export const Alert_CollectionPage = gql`
fragment Alert_CollectionPage on CollectionPageBlocksAlert {
__typename
alert {
alertConnection {
edges {
node {
...Alert
}
}
}
}
}
${Alert}
`

View File

@@ -1,5 +1,6 @@
import { gql } from "graphql-tag"
import { Alert } from "../Alert.graphql"
import { ImageContainer } from "../ImageContainer.graphql"
import { AccountPageLink } from "../PageLink/AccountPageLink.graphql"
import { CampaignOverviewPageLink } from "../PageLink/CampaignOverviewPageLink.graphql"
@@ -24,6 +25,7 @@ export const Content_ContentPage = gql`
node {
__typename
...SysAsset
...Alert
...ImageContainer
...AccountPageLink
...CampaignOverviewPageLink
@@ -45,6 +47,7 @@ export const Content_ContentPage = gql`
}
}
${SysAsset}
${Alert}
${ImageContainer}
${AccountPageLink}
${CampaignOverviewPageLink}

View File

@@ -1,5 +1,6 @@
import { gql } from "graphql-tag"
import { Alert_CollectionPage } from "../../Fragments/Blocks/Alert.graphql"
import { CardsGrid_CollectionPage } from "../../Fragments/Blocks/CardsGrid.graphql"
import { DynamicContent_CollectionPage } from "../../Fragments/Blocks/DynamicContent.graphql"
import { Shortcuts_CollectionPage } from "../../Fragments/Blocks/Shortcuts.graphql"
@@ -35,6 +36,7 @@ export const GetCollectionPage = gql`
...UspGrid_CollectionPage
...DynamicContent_CollectionPage
...VideoCard_CollectionPage
...Alert_CollectionPage
}
system {
...System
@@ -55,6 +57,7 @@ export const GetCollectionPage = gql`
${DynamicContent_CollectionPage}
${VideoCard_CollectionPage}
${Video}
${Alert_CollectionPage}
`
export const GetDaDeEnUrlsCollectionPage = gql`

View File

@@ -1,6 +1,7 @@
import { gql } from "graphql-tag"
import { Accordion_ContentPage } from "../../Fragments/Blocks/Accordion.graphql"
import { Alert_ContentPage } from "../../Fragments/Blocks/Alert.graphql"
import { CardsGrid_ContentPage } from "../../Fragments/Blocks/CardsGrid.graphql"
import { Content_ContentPage } from "../../Fragments/Blocks/Content.graphql"
import { DynamicContent_ContentPage } from "../../Fragments/Blocks/DynamicContent.graphql"
@@ -112,6 +113,7 @@ export const GetContentPageBlocksBatch2 = gql`
...Table_ContentPage
...TextCols_ContentPage
...UspGrid_ContentPage
...Alert_ContentPage
}
}
}
@@ -120,6 +122,7 @@ export const GetContentPageBlocksBatch2 = gql`
${Table_ContentPage}
${TextCols_ContentPage}
${UspGrid_ContentPage}
${Alert_ContentPage}
`
export const GetDaDeEnUrlsContentPage = gql`

View File

@@ -1,15 +1,13 @@
import { z, ZodError, ZodIssueCode } from "zod"
import {
AlertTypeEnum,
AlertVisibleOnEnum,
} from "@scandic-hotels/common/constants/alert"
import { AlertVisibleOnEnum } from "@scandic-hotels/common/constants/alert"
import { Lang } from "@scandic-hotels/common/constants/language"
import { logger } from "@scandic-hotels/common/logger"
import { removeMultipleSlashes } from "@scandic-hotels/common/utils/url"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import { discriminatedUnion } from "../../../utils/discriminatedUnion"
import { transformedAlertSchema } from "../schemas/alert"
import {
infoCardBlockSchema,
transformInfoCardBlock,
@@ -410,85 +408,6 @@ export const headerSchema = z
}
})
export const alertSchema = z
.object({
type: z.nativeEnum(AlertTypeEnum),
text: z.string(),
heading: z.string(),
phone_contact: z.object({
display_text: z.string(),
phone_number: z.string().nullable(),
footnote: z.string().nullable(),
}),
has_link: z.boolean(),
link: linkAndTitleSchema,
has_sidepeek_button: z.boolean(),
sidepeek_button: z.object({
cta_text: z.string(),
}),
sidepeek_content: z.object({
heading: z.string(),
content: z.object({
json: z.any(),
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: linkUnionSchema.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
}),
visible_on: z.array(z.string()).nullish().default([]),
})
.transform(
({
type,
heading,
text,
phone_contact,
has_link,
link,
has_sidepeek_button,
sidepeek_button,
sidepeek_content,
visible_on,
}) => {
const hasLink = has_link && link.link
return {
type,
text,
heading,
visible_on,
phoneContact:
phone_contact.display_text && phone_contact.phone_number
? {
displayText: phone_contact.display_text,
phoneNumber: phone_contact.phone_number,
footnote: phone_contact.footnote,
}
: null,
hasSidepeekButton: !!has_sidepeek_button,
link: hasLink
? {
url: link.link.url,
title: link.title,
}
: null,
sidepeekButton:
!hasLink && has_sidepeek_button ? sidepeek_button : null,
sidepeekContent:
!hasLink && has_sidepeek_button ? sidepeek_content : null,
}
}
)
export const siteConfigSchema = z
.object({
all_site_config: z.object({
@@ -503,7 +422,7 @@ export const siteConfigSchema = z
alertConnection: z.object({
edges: z.array(
z.object({
node: alertSchema,
node: transformedAlertSchema,
})
),
}),

View File

@@ -5,11 +5,11 @@ import { logger } from "@scandic-hotels/common/logger"
import { getValueFromContactConfig } from "../../../utils/contactConfig"
import type { AlertOutput } from "../../../types/siteConfig"
import type { Alert } from "../schemas/alert"
import type { ContactConfig } from "./output"
export function getAlertPhoneContactData(
alert: AlertOutput,
alert: Alert,
contactConfig: ContactConfig
) {
if (alert.phoneContact) {

View File

@@ -4,6 +4,7 @@ import { transformedImageVaultAssetSchema } from "@scandic-hotels/common/utils/i
import { CollectionPageEnum } from "../../../types/collectionPage"
import { discriminatedUnionArray } from "../../../utils/discriminatedUnion"
import { alertBlockSchema } from "../schemas/blocks/alert"
import { cardsGridSchema } from "../schemas/blocks/cardsGrid"
import { dynamicContentSchema as blockDynamicContentSchema } from "../schemas/blocks/dynamicContent"
import { shortcutsSchema } from "../schemas/blocks/shortcuts"
@@ -47,12 +48,19 @@ export const collectionPageVideoCard = z
})
.merge(videoCardSchema)
export const collectionPageAlert = z
.object({
__typename: z.literal(CollectionPageEnum.ContentStack.blocks.Alert),
})
.merge(alertBlockSchema)
export const blocksSchema = z.discriminatedUnion("__typename", [
collectionPageCards,
collectionPageDynamicContent,
collectionPageShortcuts,
collectionPageUspGrid,
collectionPageVideoCard,
collectionPageAlert,
])
const navigationLinksSchema = z

View File

@@ -5,6 +5,7 @@ import { transformedImageVaultAssetSchema } from "@scandic-hotels/common/utils/i
import { ContentPageEnum } from "../../../types/contentPage"
import { discriminatedUnionArray } from "../../../utils/discriminatedUnion"
import { accordionSchema } from "../schemas/blocks/accordion"
import { alertBlockSchema } from "../schemas/blocks/alert"
import { cardsGridSchema } from "../schemas/blocks/cardsGrid"
import { contentSchema as blockContentSchema } from "../schemas/blocks/content"
import { dynamicContentSchema as blockDynamicContentSchema } from "../schemas/blocks/dynamicContent"
@@ -101,6 +102,12 @@ export const contentPageVideo = z
})
.merge(videoBlockSchema)
export const contentPageAlert = z
.object({
__typename: z.literal(ContentPageEnum.ContentStack.blocks.Alert),
})
.merge(alertBlockSchema)
export const blocksSchema = z.discriminatedUnion("__typename", [
contentPageAccordion,
contentPageCards,
@@ -114,6 +121,7 @@ export const blocksSchema = z.discriminatedUnion("__typename", [
contentPageHotelListing,
contentPageVideoCard,
contentPageVideo,
contentPageAlert,
])
export const contentPageSidebarContent = z

View File

@@ -0,0 +1,92 @@
import z from "zod"
import { AlertTypeEnum } from "@scandic-hotels/common/constants/alert"
import { linkAndTitleSchema } from "./linkConnection"
import { linkUnionSchema, transformPageLink } from "./pageLinks"
import { systemSchema } from "./system"
export const alertSchema = z.object({
type: z.nativeEnum(AlertTypeEnum),
text: z.string(),
heading: z.string(),
phone_contact: z.object({
display_text: z.string(),
phone_number: z.string().nullish(),
footnote: z.string().nullish(),
}),
has_link: z.boolean(),
link: linkAndTitleSchema,
has_sidepeek_button: z.boolean(),
sidepeek_button: z.object({
cta_text: z.string(),
}),
sidepeek_content: z.object({
heading: z.string(),
content: z.object({
json: z.any(),
embedded_itemsConnection: z.object({
edges: z.array(
z.object({
node: linkUnionSchema.transform((data) => {
const link = transformPageLink(data)
if (link) {
return link
}
return data
}),
})
),
}),
}),
}),
visible_on: z.array(z.string()).nullish().default([]),
system: systemSchema,
})
export const transformedAlertSchema =
alertSchema.transform(transformAlertSchema)
export function transformAlertSchema(data: typeof alertSchema._type) {
const {
type,
heading,
text,
phone_contact,
has_link,
link,
has_sidepeek_button,
sidepeek_button,
sidepeek_content,
visible_on,
system,
} = data
const hasLink = has_link && link.link
return {
type,
text,
heading,
visible_on,
phoneContact:
phone_contact.display_text && phone_contact.phone_number
? {
displayText: phone_contact.display_text,
phoneNumber: phone_contact.phone_number,
footnote: phone_contact.footnote,
}
: null,
hasSidepeekButton: !!has_sidepeek_button,
link: hasLink
? {
url: link.link.url,
title: link.title,
}
: null,
sidepeekButton: !hasLink && has_sidepeek_button ? sidepeek_button : null,
sidepeekContent: !hasLink && has_sidepeek_button ? sidepeek_content : null,
system,
}
}
export type Alert = z.output<typeof transformedAlertSchema>

View File

@@ -0,0 +1,25 @@
import { z } from "zod"
import { BlocksEnums } from "../../../../types/blocksEnum"
import { transformedAlertSchema } from "../alert"
export const alertBlockSchema = z.object({
typename: z.literal(BlocksEnums.block.Alert).default(BlocksEnums.block.Alert),
alert: z
.object({
alertConnection: z.object({
edges: z.array(
z.object({
node: transformedAlertSchema,
})
),
}),
})
.transform((data) => {
const alert = data.alertConnection.edges[0]?.node
if (!alert) {
return null
}
return alert
}),
})

View File

@@ -1,6 +1,8 @@
import { z } from "zod"
import { BlocksEnums } from "../../../../types/blocksEnum"
import { ContentEnum } from "../../../../types/content"
import { alertSchema, transformAlertSchema } from "../alert"
import { rawLinkUnionSchema, transformPageLink } from "../pageLinks"
import { imageContainerSchema } from "./imageContainer"
import { sysAssetSchema } from "./sysAsset"
@@ -22,6 +24,11 @@ export const contentSchema = z.object({
.discriminatedUnion("__typename", [
imageContainerSchema,
sysAssetSchema,
alertSchema.merge(
z.object({
__typename: z.literal(ContentEnum.blocks.Alert),
})
),
...rawLinkUnionSchema.options,
])
.transform((data) => {
@@ -29,6 +36,12 @@ export const contentSchema = z.object({
if (link) {
return link
}
if (data.__typename === ContentEnum.blocks.Alert) {
return {
__typename: data.__typename,
...transformAlertSchema(data),
}
}
return data
}),
})

View File

@@ -12,6 +12,7 @@ import type { uspGridSchema } from "@scandic-hotels/trpc/routers/contentstack/sc
import type { videoCardSchema } from "@scandic-hotels/trpc/routers/contentstack/schemas/blocks/videoCard"
import type { z } from "zod"
import type { alertBlockSchema } from "../routers/contentstack/schemas/blocks/alert"
import type { videoBlockSchema } from "../routers/contentstack/schemas/blocks/video"
export interface TeaserCard extends z.output<typeof teaserCardBlockSchema> {}
@@ -32,3 +33,4 @@ export interface CarouselCards extends z.output<typeof carouselCardsSchema> {}
export interface CardGallery extends z.output<typeof cardGallerySchema> {}
export interface VideoCard extends z.output<typeof videoCardSchema> {}
export interface VideoBlock extends z.output<typeof videoBlockSchema> {}
export interface AlertBlock extends z.output<typeof alertBlockSchema> {}

View File

@@ -21,5 +21,6 @@ export namespace BlocksEnums {
Essentials = "Essentials",
VideoCard = "VideoCard",
Video = "Video",
Alert = "Alert",
}
}

View File

@@ -21,6 +21,7 @@ export namespace CollectionPageEnum {
Shortcuts = "CollectionPageBlocksShortcuts",
UspGrid = "CollectionPageBlocksUspGrid",
VideoCard = "CollectionPageBlocksVideoCard",
Alert = "CollectionPageBlocksAlert",
}
}
}

View File

@@ -14,5 +14,6 @@ export namespace ContentEnum {
StartPage = "StartPage",
PromoCampaignPage = "PromoCampaignPage",
SysAsset = "SysAsset",
Alert = "Alert",
}
}

View File

@@ -22,6 +22,7 @@ export namespace ContentPageEnum {
export namespace ContentStack {
export const enum blocks {
Accordion = "ContentPageBlocksAccordion",
Alert = "ContentPageBlocksAlert",
CardsGrid = "ContentPageBlocksCardsGrid",
Content = "ContentPageBlocksContent",
DynamicContent = "ContentPageBlocksDynamicContent",

View File

@@ -1,7 +1,6 @@
import type { z } from "zod"
import type {
alertSchema,
siteConfigSchema,
sitewideCampaignBannerSchema,
} from "../routers/contentstack/base/output"
@@ -9,19 +8,6 @@ import type {
export type GetSiteConfigData = z.input<typeof siteConfigSchema>
export type SiteConfig = z.output<typeof siteConfigSchema>
export type AlertOutput = z.output<typeof alertSchema>
export type SidepeekContent = AlertOutput["sidepeekContent"]
export type AlertPhoneContact = {
displayText: string
phoneNumber?: string
footnote?: string | null
}
export type Alert = Omit<AlertOutput, "phoneContact"> & {
phoneContact: AlertPhoneContact | null
}
export type GetSitewideCampaignBannerData = z.output<
typeof sitewideCampaignBannerSchema
>