Feat/BOOK-424 campaign banner

Approved-by: Bianca Widstam
This commit is contained in:
Erik Tiekstra
2025-10-29 12:47:40 +00:00
parent 377c8886ad
commit 4c10989e8e
29 changed files with 1052 additions and 22 deletions

View File

@@ -9,7 +9,7 @@
overflow-y: auto;
padding: var(--Spacing-x2) var(--Spacing-x3);
position: fixed;
top: calc(140px + max(var(--sitewide-alert-height), 25px));
top: calc(140px + max(var(--alert-and-banner-height), 25px));
width: 100%;
height: calc(100% - 200px);
z-index: 10010;

View File

@@ -46,7 +46,7 @@
.container[data-datepicker-open="true"] .hideWrapper {
border-radius: var(--Corner-radius-lg) var(--Corner-radius-lg) 0 0;
top: calc(max(var(--sitewide-alert-height), 20px));
top: calc(max(var(--alert-and-banner-height), 20px));
}
}

View File

@@ -15,7 +15,7 @@
background-color: var(--UI-Input-Controls-Surface-Normal);
border-radius: 0;
gap: var(--Spacing-x3);
height: calc(100dvh - max(var(--sitewide-alert-height), 20px));
height: calc(100dvh - max(var(--alert-and-banner-height), 20px));
width: 100%;
padding: var(--Spacing-x3) var(--Spacing-x2) var(--Spacing-x7);
position: fixed;

View File

@@ -0,0 +1,82 @@
#import "./PageLink/AccountPageLink.graphql"
#import "./PageLink/CampaignOverviewPageLink.graphql"
#import "./PageLink/CampaignPageLink.graphql"
#import "./PageLink/CollectionPageLink.graphql"
#import "./PageLink/ContentPageLink.graphql"
#import "./PageLink/DestinationCityPageLink.graphql"
#import "./PageLink/DestinationCountryPageLink.graphql"
#import "./PageLink/DestinationOverviewPageLink.graphql"
#import "./PageLink/HotelPageLink.graphql"
#import "./PageLink/LoyaltyPageLink.graphql"
#import "./PageLink/StartPageLink.graphql"
#import "./PageLink/PromoCampaignPageLink.graphql"
#import "./AccountPage/Ref.graphql"
#import "./CampaignOverviewPage/Ref.graphql"
#import "./CampaignPage/Ref.graphql"
#import "./CollectionPage/Ref.graphql"
#import "./ContentPage/Ref.graphql"
#import "./DestinationCityPage/Ref.graphql"
#import "./DestinationCountryPage/Ref.graphql"
#import "./DestinationOverviewPage/Ref.graphql"
#import "./HotelPage/Ref.graphql"
#import "./LoyaltyPage/Ref.graphql"
#import "./StartPage/Ref.graphql"
#import "./PromoCampaignPage/Ref.graphql"
fragment Banner on Banner {
tag
text
link {
title
linkConnection {
edges {
node {
__typename
...AccountPageLink
...CampaignOverviewPageLink
...CampaignPageLink
...CollectionPageLink
...ContentPageLink
...DestinationCityPageLink
...DestinationCountryPageLink
...DestinationOverviewPageLink
...HotelPageLink
...LoyaltyPageLink
...StartPageLink
...PromoCampaignPageLink
}
}
}
}
booking_code
visible_on
}
fragment BannerRef on Banner {
link {
linkConnection {
edges {
node {
__typename
...AccountPageRef
...CampaignOverviewPageRef
...CampaignPageRef
...CollectionPageRef
...ContentPageRef
...DestinationCityPageRef
...DestinationCountryPageRef
...DestinationOverviewPageRef
...HotelPageRef
...LoyaltyPageRef
...StartPageRef
...PromoCampaignPageRef
}
}
}
}
visible_on
system {
...System
}
}

View File

@@ -0,0 +1,34 @@
#import "../Fragments/System.graphql"
#import "../Fragments/Banner.graphql"
query GetSitewideCampaignBanner($locale: String!) {
all_sitewide_campaign_banner(limit: 1, locale: $locale) {
items {
bannerConnection {
edges {
node {
...Banner
}
}
}
}
}
}
query GetSitewideCampaignBannerRef($locale: String!) {
all_sitewide_campaign_banner(limit: 1, locale: $locale) {
items {
bannerConnection {
edges {
node {
...BannerRef
}
}
}
system {
...System
}
}
}
}

View File

@@ -16,6 +16,7 @@ import {
transformCardBlock,
transformCardBlockRefs,
} from "../schemas/blocks/cardsGrid"
import { linkConnectionRefsSchema } from "../schemas/blocks/utils/linkConnection"
import {
linkRefsUnionSchema,
linkUnionSchema,
@@ -594,7 +595,7 @@ export const alertSchema = z
}),
}),
}),
visible_on: z.array(z.string()).nullable().default([]),
visible_on: z.array(z.string()).nullish().default([]),
})
.transform(
({
@@ -673,13 +674,12 @@ export const siteConfigSchema = z
}
}
const { sitewide_alert } = data.all_site_config.items[0]
const sitewideAlertWeb = sitewide_alert.alerts?.find((alert) =>
alert.alertConnection.edges[0]?.node.visible_on?.includes(
AlertVisibleOnEnum.WEB
const sitewideAlertWeb =
data.all_site_config.items[0].sitewide_alert.alerts?.find((alert) =>
alert.alertConnection.edges[0]?.node.visible_on?.includes(
AlertVisibleOnEnum.WEB
)
)
)
return {
sitewideAlert: sitewideAlertWeb?.alertConnection.edges[0]?.node || null,
@@ -709,7 +709,7 @@ const alertConnectionRefSchema = z.object({
}),
})
),
visible_on: z.array(z.string()).nullable().default([]),
visible_on: z.array(z.string()).nullish().default([]),
})
export const siteConfigRefSchema = z.object({
@@ -737,3 +737,98 @@ export const siteConfigRefSchema = z.object({
),
}),
})
const bannerSchema = z
.object({
tag: z.string(),
text: z.string(),
link: linkAndTitleSchema,
booking_code: z.string().nullish(),
visible_on: z.array(z.string()).nullish().default([]),
})
.transform(({ tag, text, link, visible_on, booking_code }) => {
const linkUrl = link.link?.url || null
return {
tag,
text,
link: linkUrl
? {
url: linkUrl,
title: link.title,
}
: null,
booking_code,
visible_on,
}
})
const bannerRefSchema = z
.object({
link: linkConnectionRefsSchema,
visible_on: z.array(z.string()).nullish().default([]),
system: systemSchema,
})
.transform(({ link, visible_on, system }) => {
return {
linkSystem: link,
visible_on,
system,
}
})
export const sitewideCampaignBannerSchema = z
.object({
all_sitewide_campaign_banner: z.object({
items: z
.array(
z.object({
bannerConnection: z.object({
edges: z.array(
z.object({
node: bannerSchema,
})
),
}),
})
)
.max(1),
}),
})
.transform((data) => {
if (!data.all_sitewide_campaign_banner.items.length) {
return null
}
const sitewideCampaignBannerWeb =
data.all_sitewide_campaign_banner.items[0].bannerConnection.edges.find(
(banner) => banner.node.visible_on?.includes(AlertVisibleOnEnum.WEB)
)
return sitewideCampaignBannerWeb?.node ?? null
})
export const sitewideCampaignBannerRefSchema = z.object({
all_sitewide_campaign_banner: z
.object({
items: z.array(
z.object({
bannerConnection: z.object({
edges: z.array(
z.object({
node: bannerRefSchema,
})
),
}),
system: systemSchema,
})
),
})
.transform((data) => {
const webBanner = data.items.find((item) => {
const bannerNode = item.bannerConnection.edges[0]?.node
return bannerNode?.visible_on?.includes(AlertVisibleOnEnum.WEB)
})
return webBanner ?? null
}),
})

View File

@@ -11,7 +11,10 @@ import {
GetSiteConfig,
GetSiteConfigRef,
} from "../../../graphql/Query/SiteConfig.graphql"
// import { router } from "../../.."
import {
GetSitewideCampaignBanner,
GetSitewideCampaignBannerRef,
} from "../../../graphql/Query/SitewideCampaignBanner.graphql"
import { request } from "../../../graphql/request"
import { contentstackBaseProcedure } from "../../../procedures"
import { langInput } from "../../../utils"
@@ -27,6 +30,8 @@ import {
headerSchema,
siteConfigRefSchema,
siteConfigSchema,
sitewideCampaignBannerRefSchema,
sitewideCampaignBannerSchema,
validateContactConfigSchema,
validateFooterConfigSchema,
validateFooterRefConfigSchema,
@@ -36,6 +41,7 @@ import {
getConnections,
getFooterConnections,
getSiteConfigConnections,
getSitewideCampaignBannerConnections,
} from "./utils"
import type { Lang } from "@scandic-hotels/common/constants/language"
@@ -48,6 +54,8 @@ import type {
import type {
GetSiteConfigData,
GetSiteConfigRefData,
GetSitewideCampaignBannerData,
GetSitewideCampaignBannerRefData,
} from "../../../types/siteConfig"
const getContactConfig = cache(async (lang: Lang) => {
@@ -248,6 +256,97 @@ export const baseQueryRouter = router({
return validatedFooterConfig.data
}),
sitewideCampaignBanner: router({
get: contentstackBaseProcedure
.input(langInput)
.query(async ({ input, ctx }) => {
const lang = input.lang ?? ctx.lang
const getSitewideCampaignBannerRefsCounter = createCounter(
"trpc.contentstack",
"sitewideCampaignBanner.get.refs"
)
const metricsGetSitewideCampaignBannerRefs =
getSitewideCampaignBannerRefsCounter.init({
lang,
})
metricsGetSitewideCampaignBannerRefs.start()
const responseRef = await request<GetSitewideCampaignBannerRefData>(
GetSitewideCampaignBannerRef,
{ locale: lang },
{
key: generateRefsResponseTag(lang, "sitewide_campaign_banner"),
ttl: "max",
}
)
if (!responseRef.data) {
const notFoundError = notFound(responseRef)
metricsGetSitewideCampaignBannerRefs.noDataError()
throw notFoundError
}
const validatedSitewideCampaignBannerRef =
sitewideCampaignBannerRefSchema.safeParse(responseRef.data)
if (!validatedSitewideCampaignBannerRef.success) {
metricsGetSitewideCampaignBannerRefs.validationError(
validatedSitewideCampaignBannerRef.error
)
return null
}
const connections = getSitewideCampaignBannerConnections(
validatedSitewideCampaignBannerRef.data
)
const tags = [generateTagsFromSystem(lang, connections)].flat()
metricsGetSitewideCampaignBannerRefs.success()
const getSitewideCampaignBannerCounter = createCounter(
"trpc.contentstack",
"sitewideCampaignBanner.get"
)
const metricsGetSitewideCampaignBanner =
getSitewideCampaignBannerCounter.init({
lang,
})
metricsGetSitewideCampaignBanner.start()
const sitewideCampaignBannerResponse =
await request<GetSitewideCampaignBannerData>(
GetSitewideCampaignBanner,
{ locale: lang },
{ key: tags, ttl: "max" }
)
if (!sitewideCampaignBannerResponse.data) {
const notFoundError = notFound(sitewideCampaignBannerResponse)
metricsGetSitewideCampaignBanner.noDataError()
throw notFoundError
}
const validatedSitewideCampaignBanner =
sitewideCampaignBannerSchema.safeParse(
sitewideCampaignBannerResponse.data
)
if (!validatedSitewideCampaignBanner.success) {
metricsGetSitewideCampaignBanner.validationError(
validatedSitewideCampaignBanner.error
)
return null
}
metricsGetSitewideCampaignBanner.success()
return validatedSitewideCampaignBanner.data
}),
}),
siteConfig: contentstackBaseProcedure
.input(langInput)
.query(async ({ input, ctx }) => {

View File

@@ -12,6 +12,7 @@ import type { NodeRefs } from "../../../types/refs"
import type {
AlertOutput,
GetSiteConfigRefData,
GetSitewideCampaignBannerRefData,
} from "../../../types/siteConfig"
import type { System } from "../schemas/system"
import type { ContactConfig } from "./output"
@@ -138,3 +139,21 @@ export const safeUnion = <T extends z.ZodTypeAny>(schema: T) =>
return null
}
}, schema)
export function getSitewideCampaignBannerConnections(
refs: GetSitewideCampaignBannerRefData
) {
const system = refs.all_sitewide_campaign_banner?.system
const banner =
refs.all_sitewide_campaign_banner?.bannerConnection.edges[0]?.node
const connections: System["system"][] = []
if (system) {
connections.push(system)
}
if (banner?.system) {
connections.push(banner.system)
}
return connections
}

View File

@@ -1,5 +1,7 @@
import { z } from "zod"
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
import {
linkRefsUnionSchema,
linkUnionSchema,
@@ -8,7 +10,7 @@ import {
} from "./pageLinks"
const titleSchema = z.object({
title: z.string().optional().default(""),
title: nullableStringValidator,
})
export const linkConnectionSchema = z

View File

@@ -4,6 +4,8 @@ import type {
alertSchema,
siteConfigRefSchema,
siteConfigSchema,
sitewideCampaignBannerRefSchema,
sitewideCampaignBannerSchema,
} from "../routers/contentstack/base/output"
export type GetSiteConfigRefData = z.infer<typeof siteConfigRefSchema>
@@ -23,3 +25,10 @@ export type AlertPhoneContact = {
export type Alert = Omit<AlertOutput, "phoneContact"> & {
phoneContact: AlertPhoneContact | null
}
export type GetSitewideCampaignBannerRefData = z.infer<
typeof sitewideCampaignBannerRefSchema
>
export type GetSitewideCampaignBannerData = z.output<
typeof sitewideCampaignBannerSchema
>