feat(SW-2264): Added campaign overview page

Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-06-19 15:19:56 +00:00
parent 3c4ff0a792
commit 891108791c
42 changed files with 743 additions and 86 deletions

View File

@@ -0,0 +1,19 @@
import { Suspense } from "react"
import Breadcrumbs from "@/components/Breadcrumbs"
import BreadcrumbsSkeleton from "@/components/TempDesignSystem/Breadcrumbs/BreadcrumbsSkeleton"
import type { BreadcrumbsProps } from "@/components/TempDesignSystem/Breadcrumbs/breadcrumbs"
export default function CampaignOverviewPageBreadcrumbs() {
const variants: Pick<BreadcrumbsProps, "color" | "size"> = {
color: "Surface/Secondary/Default",
size: "contentWidth",
}
return (
<Suspense fallback={<BreadcrumbsSkeleton {...variants} />}>
<Breadcrumbs {...variants} />
</Suspense>
)
}

View File

@@ -0,0 +1,21 @@
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { env } from "@/env/server"
import CampaignOverviewPage from "@/components/ContentType/CampaignOverviewPage"
import CampaignOverviewPageSkeleton from "@/components/ContentType/CampaignOverviewPage/CampaignOverviewPageSkeleton"
export { generateMetadata } from "@/utils/generateMetadata"
export default async function CampaignOverviewPagePage() {
if (!env.CAMPAIGN_PAGES_ENABLED) {
notFound()
}
return (
<Suspense fallback={<CampaignOverviewPageSkeleton />}>
<CampaignOverviewPage />
</Suspense>
)
}

View File

@@ -0,0 +1,15 @@
"use client"
import SkeletonShimmer from "@/components/SkeletonShimmer"
import styles from "./campaignOverviewPage.module.css"
export default function CampaignOverviewPageSkeleton() {
return (
<div className={styles.pageContainer}>
<h1>
<SkeletonShimmer width="50%" height="2rem" />
</h1>
</div>
)
}

View File

@@ -0,0 +1,52 @@
.pageContainer {
display: grid;
gap: var(--Space-x5);
padding-bottom: var(--Space-x7);
}
.header {
background-color: var(--Base-Surface-Subtle-Normal);
padding-bottom: var(--Space-x4);
}
.headerContent {
display: grid;
gap: var(--Space-x3);
width: var(--max-width-content);
margin: 0 auto;
}
.headerIntro {
display: grid;
max-width: var(--max-width-text-block);
gap: var(--Space-x3);
}
.heading {
color: var(--Text-Heading);
text-wrap: balance;
hyphens: auto;
}
.mainContent {
display: grid;
gap: var(--Space-x6);
width: var(--max-width-content);
margin: 0 auto;
}
@media (min-width: 768px) {
.pageContainer {
gap: var(--Space-x9);
}
.headerIntro {
gap: var(--Space-x3);
}
}
@media screen and (min-width: 1367px) {
.mainContent {
gap: var(--Space-x9);
}
}

View File

@@ -0,0 +1,57 @@
import { notFound } from "next/navigation"
import { Suspense } from "react"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { getCampaignOverviewPage } from "@/lib/trpc/memoizedRequests"
import LinkChips from "@/components/TempDesignSystem/LinkChips"
import CampaignOverviewPageSkeleton from "./CampaignOverviewPageSkeleton"
import styles from "./campaignOverviewPage.module.css"
export default async function CampaignOverviewPage() {
const pageData = await getCampaignOverviewPage()
if (!pageData) {
notFound()
}
const { campaignOverviewPage } = pageData
const { header } = campaignOverviewPage
return (
<>
<Suspense fallback={<CampaignOverviewPageSkeleton />}>
<div className={styles.pageContainer}>
{header ? (
<header className={styles.header}>
<div className={styles.headerContent}>
<>
<div className={styles.headerIntro}>
<Typography variant="Title/lg">
<h1 className={styles.heading}>{header.heading}</h1>
</Typography>
<Typography variant="Body/Lead text">
<p>{header.preamble}</p>
</Typography>
</div>
{header.navigation_links ? (
<LinkChips chips={header.navigation_links} />
) : null}
</>
</div>
</header>
) : null}
<main className={styles.mainContent}>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
{">>> MAIN CONTENT <<<"}
</main>
</div>
</Suspense>
{/* <TrackingSDK pageData={tracking} /> */}
</>
)
}

View File

@@ -1,14 +0,0 @@
.linkChip {
display: flex;
align-items: center;
gap: var(--Spacing-x-half);
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
border-radius: var(--Corner-radius-sm);
background-color: var(--Base-Button-Inverted-Fill-Normal);
transition: background-color 0.3s;
text-decoration: none;
}
.linkChip:hover {
background-color: var(--Base-Button-Inverted-Fill-Hover-alt);
}

View File

@@ -1,4 +0,0 @@
export interface LinkChipProps {
url: string
title: string
}

View File

@@ -1,20 +0,0 @@
import Link from "next/link"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import styles from "./chip.module.css"
import type { LinkChipProps } from "./chip"
export default function LinkChip({ url, title }: LinkChipProps) {
return (
<Caption type="bold" color="burgundy" asChild>
<Link href={url} className={styles.linkChip}>
{title}
<MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
</Link>
</Caption>
)
}

View File

@@ -1,8 +1,15 @@
import LinkChip from "./Chip" "use client"
import styles from "./linkChips.module.css" import { ChipLink } from "@scandic-hotels/design-system/ChipLink"
import { Chips } from "@scandic-hotels/design-system/Chips"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import type { LinkChipsProps } from "./linkChips" interface LinkChipsProps {
chips: {
url: string
title: string
}[]
}
export default function LinkChips({ chips }: LinkChipsProps) { export default function LinkChips({ chips }: LinkChipsProps) {
if (!chips.length) { if (!chips.length) {
@@ -10,12 +17,13 @@ export default function LinkChips({ chips }: LinkChipsProps) {
} }
return ( return (
<ul className={styles.linkChips}> <Chips>
{chips.map(({ url, title }) => ( {chips.map(({ title, url }) => (
<li key={`link-chip-${title}`}> <ChipLink key={`${title}-${url}`} href={url}>
<LinkChip url={url} title={title} /> {title}
</li> <MaterialIcon icon="chevron_right" size={20} color="CurrentColor" />
</ChipLink>
))} ))}
</ul> </Chips>
) )
} }

View File

@@ -1,8 +0,0 @@
.linkChips {
list-style: none;
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-items: center;
gap: var(--Spacing-x1);
}

View File

@@ -1,5 +0,0 @@
import type { LinkChipProps } from "./Chip/chip"
export interface LinkChipsProps {
chips: LinkChipProps[]
}

View File

@@ -0,0 +1,70 @@
#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 "../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"
fragment NavigationLinks_CampaignOverviewPage on CampaignOverviewPageHeader {
navigation_links {
title
linkConnection {
edges {
node {
__typename
...AccountPageLink
...CampaignOverviewPageLink
...CampaignPageLink
...CollectionPageLink
...ContentPageLink
...DestinationCityPageLink
...DestinationCountryPageLink
...DestinationOverviewPageLink
...HotelPageLink
...LoyaltyPageLink
...StartPageLink
}
}
}
}
}
fragment NavigationLinksRef_CampaignOverviewPage on CampaignOverviewPageHeader {
navigation_links {
linkConnection {
edges {
node {
__typename
...AccountPageRef
...CampaignOverviewPageRef
...CampaignPageRef
...CollectionPageRef
...ContentPageRef
...DestinationCityPageRef
...DestinationCountryPageRef
...DestinationOverviewPageRef
...HotelPageRef
...LoyaltyPageRef
...StartPageRef
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,9 @@
#import "../System.graphql"
fragment CampaignOverviewPageLink on CampaignOverviewPage {
title
url
system {
...System
}
}

View File

@@ -0,0 +1,29 @@
#import "../../Fragments/Breadcrumbs/Breadcrumbs.graphql"
#import "../../Fragments/System.graphql"
query GetCampaignOverviewPageBreadcrumbs($locale: String!, $uid: String!) {
campaign_overview_page(locale: $locale, uid: $uid) {
url
web {
breadcrumbs {
...Breadcrumbs
}
}
system {
...System
}
}
}
query GetCampaignOverviewPageBreadcrumbsRefs($locale: String!, $uid: String!) {
campaign_overview_page(locale: $locale, uid: $uid) {
web {
breadcrumbs {
...BreadcrumbsRefs
}
}
system {
...System
}
}
}

View File

@@ -0,0 +1,57 @@
#import "../../Fragments/System.graphql"
#import "../../Fragments/CampaignOverviewPage/NavigationLinks.graphql"
query GetCampaignOverviewPage($locale: String!, $uid: String!) {
campaign_overview_page(uid: $uid, locale: $locale) {
title
header {
heading
preamble
...NavigationLinks_CampaignOverviewPage
}
system {
...System
created_at
updated_at
}
}
trackingProps: campaign_overview_page(locale: "en", uid: $uid) {
url
}
}
query GetCampaignOverviewPageRefs($locale: String!, $uid: String!) {
campaign_overview_page(locale: $locale, uid: $uid) {
header {
...NavigationLinksRef_CampaignOverviewPage
}
system {
...System
}
}
}
query GetDaDeEnUrlsCampaignOverviewPage($uid: String!) {
de: campaign_overview_page(locale: "de", uid: $uid) {
url
}
en: campaign_overview_page(locale: "en", uid: $uid) {
url
}
da: campaign_overview_page(locale: "da", uid: $uid) {
url
}
}
query GetFiNoSvUrlsCampaignOverviewPage($uid: String!) {
fi: campaign_overview_page(locale: "fi", uid: $uid) {
url
}
no: campaign_overview_page(locale: "no", uid: $uid) {
url
}
sv: campaign_overview_page(locale: "sv", uid: $uid) {
url
}
}

View File

@@ -0,0 +1,22 @@
#import "../../Fragments/Metadata.graphql"
#import "../../Fragments/System.graphql"
query GetCampaignOverviewPageMetadata($locale: String!, $uid: String!) {
campaign_overview_page(locale: $locale, uid: $uid) {
web {
breadcrumbs {
title
}
seo_metadata {
...Metadata
}
}
header {
heading
preamble
}
system {
...System
}
}
}

View File

@@ -8,6 +8,14 @@ query GetAccountPageSettings($uid: String!, $locale: String!) {
} }
} }
query GetCampaignOverviewPageSettings($uid: String!, $locale: String!) {
page: campaign_overview_page(uid: $uid, locale: $locale) {
settings: page_settings {
...PageSettings
}
}
}
query GetCampaignPageSettings($uid: String!, $locale: String!) { query GetCampaignPageSettings($uid: String!, $locale: String!) {
page: campaign_page(uid: $uid, locale: $locale) { page: campaign_page(uid: $uid, locale: $locale) {
settings: page_settings { settings: page_settings {

View File

@@ -92,4 +92,12 @@ query EntryByUrlBatch2($locale: String!, $url: String!) {
} }
total total
} }
all_campaign_overview_page(where: { url: $url }, locale: $locale) {
items {
system {
...System
}
}
total
}
} }

View File

@@ -422,3 +422,10 @@ export const getCampaignPage = cache(async function getMemoizedCampaignPage() {
const caller = await serverClient() const caller = await serverClient()
return caller.contentstack.campaignPage.get() return caller.contentstack.campaignPage.get()
}) })
export const getCampaignOverviewPage = cache(
async function getMemoizedCampaignOverviewPage() {
const caller = await serverClient()
return caller.contentstack.campaignOverviewPage.get()
}
)

View File

@@ -9,6 +9,10 @@ import {
GetMyPagesBreadcrumbs, GetMyPagesBreadcrumbs,
GetMyPagesBreadcrumbsRefs, GetMyPagesBreadcrumbsRefs,
} from "@/lib/graphql/Query/Breadcrumbs/AccountPage.graphql" } from "@/lib/graphql/Query/Breadcrumbs/AccountPage.graphql"
import {
GetCampaignOverviewPageBreadcrumbs,
GetCampaignOverviewPageBreadcrumbsRefs,
} from "@/lib/graphql/Query/Breadcrumbs/CampaignOverviewPage.graphql"
import { import {
GetCampaignPageBreadcrumbs, GetCampaignPageBreadcrumbs,
GetCampaignPageBreadcrumbsRefs, GetCampaignPageBreadcrumbsRefs,
@@ -158,6 +162,17 @@ export const breadcrumbsQueryRouter = router({
}, },
variables variables
) )
case PageContentTypeEnum.campaignOverviewPage:
return await getBreadcrumbs<{
campaign_overview_page: RawBreadcrumbsSchema
}>(
{
dataKey: "campaign_overview_page",
refQuery: GetCampaignOverviewPageBreadcrumbsRefs,
query: GetCampaignOverviewPageBreadcrumbs,
},
variables
)
case PageContentTypeEnum.campaignPage: case PageContentTypeEnum.campaignPage:
return await getBreadcrumbs<{ return await getBreadcrumbs<{
campaign_page: RawBreadcrumbsSchema campaign_page: RawBreadcrumbsSchema

View File

@@ -0,0 +1,7 @@
import { mergeRouters } from "@scandic-hotels/trpc"
import { campaignOverviewPageQueryRouter } from "./query"
export const campaignOverviewPageRouter = mergeRouters(
campaignOverviewPageQueryRouter
)

View File

@@ -0,0 +1,57 @@
import { z } from "zod"
import {
linkAndTitleSchema,
linkConnectionRefs,
} from "@/server/routers/contentstack/schemas/linkConnection"
import { systemSchema } from "../schemas/system"
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,
}))
})
export const campaignOverviewPageSchema = z.object({
campaign_overview_page: z.object({
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(),
})
),
}),
trackingProps: z.object({
url: z.string(),
}),
})
/** REFS */
const campaignOverviewPageHeaderRefs = z.object({
navigation_links: z.array(linkConnectionRefs),
})
export const campaignOverviewPageRefsSchema = z.object({
campaign_overview_page: z.object({
header: campaignOverviewPageHeaderRefs,
system: systemSchema,
}),
})

View File

@@ -0,0 +1,135 @@
import { createCounter } from "@scandic-hotels/common/telemetry"
import { router } from "@scandic-hotels/trpc"
import { notFound } from "@scandic-hotels/trpc/errors"
import { contentStackUidWithServiceProcedure } from "@scandic-hotels/trpc/procedures"
import {
GetCampaignOverviewPage,
GetCampaignOverviewPageRefs,
} from "@/lib/graphql/Query/CampaignOverviewPage/CampaignOverviewPage.graphql"
import { request } from "@/lib/graphql/request"
import { generateRefsResponseTag } from "@/utils/generateTag"
import {
campaignOverviewPageRefsSchema,
campaignOverviewPageSchema,
} from "./output"
import { generatePageTags } from "./utils"
import {
TrackingChannelEnum,
type TrackingSDKPageData,
} from "@/types/components/tracking"
import type {
GetCampaignOverviewPageData,
GetCampaignOverviewPageRefsData,
} from "@/types/trpc/routers/contentstack/campaignOverviewPage"
export const campaignOverviewPageQueryRouter = router({
get: contentStackUidWithServiceProcedure.query(async ({ ctx }) => {
const { lang, uid } = ctx
const getCampaignOverviewPageRefsCounter = createCounter(
"trpc.contentstack",
"campaignOverviewPage.get.refs"
)
const metricsGetCampaignOverviewPageRefs =
getCampaignOverviewPageRefsCounter.init({
lang,
uid,
})
metricsGetCampaignOverviewPageRefs.start()
const refsResponse = await request<GetCampaignOverviewPageRefsData>(
GetCampaignOverviewPageRefs,
{ locale: lang, uid },
{
key: generateRefsResponseTag(lang, uid),
ttl: "max",
}
)
if (!refsResponse.data) {
const notFoundError = notFound(refsResponse)
metricsGetCampaignOverviewPageRefs.noDataError()
throw notFoundError
}
const validatedRefsData = campaignOverviewPageRefsSchema.safeParse(
refsResponse.data
)
if (!validatedRefsData.success) {
metricsGetCampaignOverviewPageRefs.validationError(
validatedRefsData.error
)
return null
}
metricsGetCampaignOverviewPageRefs.success()
const tags = generatePageTags(validatedRefsData.data, lang)
const getCampaignOverviewPageCounter = createCounter(
"trpc.contentstack",
"campaignOverviewPage.get"
)
const metricsGetCampaignOverviewPage = getCampaignOverviewPageCounter.init({
lang,
uid,
})
metricsGetCampaignOverviewPage.start()
const response = await request<GetCampaignOverviewPageData>(
GetCampaignOverviewPage,
{
locale: lang,
uid,
},
{
key: tags,
ttl: "max",
}
)
if (!response.data) {
const notFoundError = notFound(response)
metricsGetCampaignOverviewPage.noDataError()
throw notFoundError
}
const validatedResponse = campaignOverviewPageSchema.safeParse(
response.data
)
if (!validatedResponse.success) {
metricsGetCampaignOverviewPage.validationError(validatedResponse.error)
return null
}
const campaignOverviewPage = validatedResponse.data.campaign_overview_page
metricsGetCampaignOverviewPage.success()
const system = campaignOverviewPage.system
const pageName = `campaign-overview-page`
const tracking: TrackingSDKPageData = {
pageId: system.uid,
domainLanguage: system.locale,
publishDate: system.updated_at,
createDate: system.created_at,
channel: TrackingChannelEnum["campaign-overview-page"],
pageType: "campaign-overview-page",
pageName,
siteSections: pageName,
siteVersion: "new-web",
}
return {
campaignOverviewPage,
tracking,
}
}),
})

View File

@@ -0,0 +1,33 @@
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
import type { Lang } from "@scandic-hotels/common/constants/language"
import type { System } from "@/types/requests/system"
import type { CampaignOverviewPageRefs } from "@/types/trpc/routers/contentstack/campaignOverviewPage"
export function generatePageTags(
validatedData: CampaignOverviewPageRefs,
lang: Lang
): string[] {
const connections = getConnections(validatedData)
return [
generateTagsFromSystem(lang, connections),
generateTag(lang, validatedData.campaign_overview_page.system.uid),
].flat()
}
export function getConnections({
campaign_overview_page,
}: CampaignOverviewPageRefs) {
const connections: System["system"][] = [campaign_overview_page.system]
if (campaign_overview_page.header.navigation_links) {
campaign_overview_page.header.navigation_links.forEach((link) => {
if (link.link) {
connections.push(link.link)
}
})
}
return connections
}

View File

@@ -3,6 +3,7 @@ import { router } from "@scandic-hotels/trpc"
import { accountPageRouter } from "./accountPage" import { accountPageRouter } from "./accountPage"
import { baseRouter } from "./base" import { baseRouter } from "./base"
import { breadcrumbsRouter } from "./breadcrumbs" import { breadcrumbsRouter } from "./breadcrumbs"
import { campaignOverviewPageRouter } from "./campaignOverviewPage"
import { campaignPageRouter } from "./campaignPage" import { campaignPageRouter } from "./campaignPage"
import { collectionPageRouter } from "./collectionPage" import { collectionPageRouter } from "./collectionPage"
import { contentPageRouter } from "./contentPage" import { contentPageRouter } from "./contentPage"
@@ -26,6 +27,7 @@ export const contentstackRouter = router({
hotelPage: hotelPageRouter, hotelPage: hotelPageRouter,
languageSwitcher: languageSwitcherRouter, languageSwitcher: languageSwitcherRouter,
loyaltyPage: loyaltyPageRouter, loyaltyPage: loyaltyPageRouter,
campaignOverviewPage: campaignOverviewPageRouter,
campaignPage: campaignPageRouter, campaignPage: campaignPageRouter,
collectionPage: collectionPageRouter, collectionPage: collectionPageRouter,
contentPage: contentPageRouter, contentPage: contentPageRouter,

View File

@@ -7,6 +7,10 @@ import {
GetDaDeEnUrlsAccountPage, GetDaDeEnUrlsAccountPage,
GetFiNoSvUrlsAccountPage, GetFiNoSvUrlsAccountPage,
} from "@/lib/graphql/Query/AccountPage/AccountPage.graphql" } from "@/lib/graphql/Query/AccountPage/AccountPage.graphql"
import {
GetDaDeEnUrlsCampaignOverviewPage,
GetFiNoSvUrlsCampaignOverviewPage,
} from "@/lib/graphql/Query/CampaignOverviewPage/CampaignOverviewPage.graphql"
import { import {
GetDaDeEnUrlsCampaignPage, GetDaDeEnUrlsCampaignPage,
GetFiNoSvUrlsCampaignPage, GetFiNoSvUrlsCampaignPage,
@@ -100,6 +104,10 @@ export async function getUrlsOfAllLanguages(
daDeEnDocument = GetDaDeEnUrlsCurrentBlocksPage daDeEnDocument = GetDaDeEnUrlsCurrentBlocksPage
fiNoSvDocument = GetFiNoSvUrlsCurrentBlocksPage fiNoSvDocument = GetFiNoSvUrlsCurrentBlocksPage
break break
case PageContentTypeEnum.campaignOverviewPage:
daDeEnDocument = GetDaDeEnUrlsCampaignOverviewPage
fiNoSvDocument = GetFiNoSvUrlsCampaignOverviewPage
break
case PageContentTypeEnum.campaignPage: case PageContentTypeEnum.campaignPage:
daDeEnDocument = GetDaDeEnUrlsCampaignPage daDeEnDocument = GetDaDeEnUrlsCampaignPage
fiNoSvDocument = GetFiNoSvUrlsCampaignPage fiNoSvDocument = GetFiNoSvUrlsCampaignPage

View File

@@ -7,6 +7,7 @@ import { contentStackUidWithServiceProcedure } from "@scandic-hotels/trpc/proced
import { env } from "@/env/server" import { env } from "@/env/server"
import { GetAccountPageMetadata } from "@/lib/graphql/Query/AccountPage/Metadata.graphql" import { GetAccountPageMetadata } from "@/lib/graphql/Query/AccountPage/Metadata.graphql"
import { GetCampaignOverviewPageMetadata } from "@/lib/graphql/Query/CampaignOverviewPage/Metadata.graphql"
import { GetCampaignPageMetadata } from "@/lib/graphql/Query/CampaignPage/Metadata.graphql" import { GetCampaignPageMetadata } from "@/lib/graphql/Query/CampaignPage/Metadata.graphql"
import { GetCollectionPageMetadata } from "@/lib/graphql/Query/CollectionPage/Metadata.graphql" import { GetCollectionPageMetadata } from "@/lib/graphql/Query/CollectionPage/Metadata.graphql"
import { GetContentPageMetadata } from "@/lib/graphql/Query/ContentPage/Metadata.graphql" import { GetContentPageMetadata } from "@/lib/graphql/Query/ContentPage/Metadata.graphql"
@@ -131,6 +132,14 @@ export const metadataQueryRouter = router({
accountPageResponse.account_page accountPageResponse.account_page
) )
break break
case PageContentTypeEnum.campaignOverviewPage:
const campaignOverviewPageResponse = await fetchMetadata<{
campaign_overview_page: RawMetadataSchema
}>(GetCampaignOverviewPageMetadata, variables)
metadata = await getTransformedMetadata(
campaignOverviewPageResponse.campaign_overview_page
)
break
case PageContentTypeEnum.campaignPage: case PageContentTypeEnum.campaignPage:
const campaignPageResponse = await fetchMetadata<{ const campaignPageResponse = await fetchMetadata<{
campaign_page: RawMetadataSchema campaign_page: RawMetadataSchema

View File

@@ -5,6 +5,7 @@ import { contentstackBaseProcedure } from "@scandic-hotels/trpc/procedures"
import { import {
GetAccountPageSettings, GetAccountPageSettings,
GetCampaignOverviewPageSettings,
GetCampaignPageSettings, GetCampaignPageSettings,
GetCollectionPageSettings, GetCollectionPageSettings,
GetContentPageSettings, GetContentPageSettings,
@@ -77,6 +78,7 @@ export const pageSettingsQueryRouter = router({
const graphqlQueriesForContentType: Record<PageContentTypeEnum, any> = { const graphqlQueriesForContentType: Record<PageContentTypeEnum, any> = {
[PageContentTypeEnum.accountPage]: GetAccountPageSettings, [PageContentTypeEnum.accountPage]: GetAccountPageSettings,
[PageContentTypeEnum.campaignOverviewPage]: GetCampaignOverviewPageSettings,
[PageContentTypeEnum.campaignPage]: GetCampaignPageSettings, [PageContentTypeEnum.campaignPage]: GetCampaignPageSettings,
[PageContentTypeEnum.collectionPage]: GetCollectionPageSettings, [PageContentTypeEnum.collectionPage]: GetCollectionPageSettings,
[PageContentTypeEnum.contentPage]: GetContentPageSettings, [PageContentTypeEnum.contentPage]: GetContentPageSettings,

View File

@@ -2,6 +2,8 @@ import { z } from "zod"
import { import {
accountPageSchema, accountPageSchema,
campaignOverviewPageSchema,
campaignPageSchema,
collectionPageSchema, collectionPageSchema,
contentPageSchema, contentPageSchema,
destinationCityPageSchema, destinationCityPageSchema,
@@ -20,6 +22,8 @@ export const contentEmbedsSchema = z
imageContainerSchema, imageContainerSchema,
sysAssetSchema, sysAssetSchema,
accountPageSchema, accountPageSchema,
campaignOverviewPageSchema,
campaignPageSchema,
collectionPageSchema, collectionPageSchema,
contentPageSchema, contentPageSchema,
destinationCityPageSchema, destinationCityPageSchema,

View File

@@ -32,6 +32,18 @@ export const extendedPageLinkSchema = pageLinkSchema.merge(
.default({ original_url: "" }), .default({ original_url: "" }),
}) })
) )
export const campaignOverviewPageSchema = z
.object({
__typename: z.literal(ContentEnum.blocks.CampaignOverviewPage),
})
.merge(extendedPageLinkSchema)
export const campaignOverviewPageRefSchema = z.object({
__typename: z.literal(ContentEnum.blocks.CampaignOverviewPage),
system: systemSchema,
})
export const collectionPageSchema = z export const collectionPageSchema = z
.object({ .object({
__typename: z.literal(ContentEnum.blocks.CollectionPage), __typename: z.literal(ContentEnum.blocks.CollectionPage),
@@ -133,6 +145,7 @@ export const startPageRefSchema = z.object({
export const linkUnionSchema = z.discriminatedUnion("__typename", [ export const linkUnionSchema = z.discriminatedUnion("__typename", [
accountPageSchema, accountPageSchema,
campaignOverviewPageSchema,
campaignPageSchema, campaignPageSchema,
collectionPageSchema, collectionPageSchema,
contentPageSchema, contentPageSchema,
@@ -146,6 +159,7 @@ export const linkUnionSchema = z.discriminatedUnion("__typename", [
type Data = type Data =
| z.output<typeof accountPageSchema> | z.output<typeof accountPageSchema>
| z.output<typeof campaignOverviewPageSchema>
| z.output<typeof campaignPageSchema> | z.output<typeof campaignPageSchema>
| z.output<typeof collectionPageSchema> | z.output<typeof collectionPageSchema>
| z.output<typeof contentPageSchema> | z.output<typeof contentPageSchema>
@@ -161,6 +175,7 @@ export function transformPageLink(data: Data) {
if (data && "__typename" in data) { if (data && "__typename" in data) {
switch (data.__typename) { switch (data.__typename) {
case ContentEnum.blocks.AccountPage: case ContentEnum.blocks.AccountPage:
case ContentEnum.blocks.CampaignOverviewPage:
case ContentEnum.blocks.CampaignPage: case ContentEnum.blocks.CampaignPage:
case ContentEnum.blocks.DestinationCityPage: case ContentEnum.blocks.DestinationCityPage:
case ContentEnum.blocks.DestinationCountryPage: case ContentEnum.blocks.DestinationCountryPage:
@@ -173,7 +188,6 @@ export function transformPageLink(data: Data) {
title: data.title, title: data.title,
url: removeMultipleSlashes(`/${data.system.locale}/${data.url}`), url: removeMultipleSlashes(`/${data.system.locale}/${data.url}`),
} }
case ContentEnum.blocks.CollectionPage: case ContentEnum.blocks.CollectionPage:
case ContentEnum.blocks.ContentPage: case ContentEnum.blocks.ContentPage:
case ContentEnum.blocks.LoyaltyPage: case ContentEnum.blocks.LoyaltyPage:
@@ -195,6 +209,7 @@ export function transformPageLink(data: Data) {
export const linkRefsUnionSchema = z.discriminatedUnion("__typename", [ export const linkRefsUnionSchema = z.discriminatedUnion("__typename", [
accountPageRefSchema, accountPageRefSchema,
campaignOverviewPageRefSchema,
campaignPageRefSchema, campaignPageRefSchema,
collectionPageRefSchema, collectionPageRefSchema,
contentPageRefSchema, contentPageRefSchema,
@@ -208,6 +223,7 @@ export const linkRefsUnionSchema = z.discriminatedUnion("__typename", [
type RefData = type RefData =
| z.output<typeof accountPageRefSchema> | z.output<typeof accountPageRefSchema>
| z.output<typeof campaignOverviewPageRefSchema>
| z.output<typeof campaignPageRefSchema> | z.output<typeof campaignPageRefSchema>
| z.output<typeof collectionPageRefSchema> | z.output<typeof collectionPageRefSchema>
| z.output<typeof contentPageRefSchema> | z.output<typeof contentPageRefSchema>
@@ -223,6 +239,7 @@ export function transformPageLinkRef(data: RefData) {
if (data && "__typename" in data) { if (data && "__typename" in data) {
switch (data.__typename) { switch (data.__typename) {
case ContentEnum.blocks.AccountPage: case ContentEnum.blocks.AccountPage:
case ContentEnum.blocks.CampaignOverviewPage:
case ContentEnum.blocks.CampaignPage: case ContentEnum.blocks.CampaignPage:
case ContentEnum.blocks.CollectionPage: case ContentEnum.blocks.CollectionPage:
case ContentEnum.blocks.ContentPage: case ContentEnum.blocks.ContentPage:

View File

@@ -9,6 +9,7 @@ export enum TrackingChannelEnum {
"hotelreservation" = "hotelreservation", "hotelreservation" = "hotelreservation",
"collection-page" = "collection-page", "collection-page" = "collection-page",
"campaign-page" = "campaign-page", "campaign-page" = "campaign-page",
"campaign-overview-page" = "campaign-overview-page",
"hotels" = "hotels", "hotels" = "hotels",
"homepage" = "homepage", "homepage" = "homepage",
} }

View File

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

View File

@@ -23,6 +23,8 @@ export type StatusParams = {
export type ContentTypeParams = { export type ContentTypeParams = {
contentType: contentType:
| PageContentTypeEnum.loyaltyPage | PageContentTypeEnum.loyaltyPage
| PageContentTypeEnum.campaignOverviewPage
| PageContentTypeEnum.campaignPage
| PageContentTypeEnum.contentPage | PageContentTypeEnum.contentPage
| PageContentTypeEnum.hotelPage | PageContentTypeEnum.hotelPage
| PageContentTypeEnum.collectionPage | PageContentTypeEnum.collectionPage

View File

@@ -1,12 +1,13 @@
export enum PageContentTypeEnum { export enum PageContentTypeEnum {
accountPage = "account_page", accountPage = "account_page",
campaignOverviewPage = "campaign_overview_page",
campaignPage = "campaign_page", campaignPage = "campaign_page",
collectionPage = "collection_page", collectionPage = "collection_page",
contentPage = "content_page", contentPage = "content_page",
currentBlocksPage = "current_blocks_page", currentBlocksPage = "current_blocks_page",
destinationOverviewPage = "destination_overview_page",
destinationCountryPage = "destination_country_page",
destinationCityPage = "destination_city_page", destinationCityPage = "destination_city_page",
destinationCountryPage = "destination_country_page",
destinationOverviewPage = "destination_overview_page",
hotelPage = "hotel_page", hotelPage = "hotel_page",
loyaltyPage = "loyalty_page", loyaltyPage = "loyalty_page",
startPage = "start_page", startPage = "start_page",

View File

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

View File

@@ -0,0 +1,19 @@
import type { z } from "zod"
import type {
campaignOverviewPageRefsSchema,
campaignOverviewPageSchema,
} from "@/server/routers/contentstack/campaignOverviewPage/output"
export interface GetCampaignOverviewPageData
extends z.input<typeof campaignOverviewPageSchema> {}
export interface CampaignOverviewPage
extends z.output<typeof campaignOverviewPageSchema> {}
export type CampaignOverviewPageData =
CampaignOverviewPage["campaign_overview_page"]
export interface GetCampaignOverviewPageRefsData
extends z.input<typeof campaignOverviewPageRefsSchema> {}
export interface CampaignOverviewPageRefs
extends z.output<typeof campaignOverviewPageRefsSchema> {}

View File

@@ -1,7 +1,7 @@
import { Button } from '@scandic-hotels/design-system/Button' import { Button } from '@scandic-hotels/design-system/Button'
import { Chips } from '@scandic-hotels/design-system/Chips'
import { ChipButton } from '@scandic-hotels/design-system/ChipButton' import { ChipButton } from '@scandic-hotels/design-system/ChipButton'
import { ChipLink } from '@scandic-hotels/design-system/ChipLink' import { ChipLink } from '@scandic-hotels/design-system/ChipLink'
import { Chips } from '@scandic-hotels/design-system/Chips'
import { Typography } from '@scandic-hotels/design-system/Typography' import { Typography } from '@scandic-hotels/design-system/Typography'
import styles from './header.module.css' import styles from './header.module.css'
@@ -10,8 +10,6 @@ import type { PressEvent } from 'react-aria-components'
export function Header() { export function Header() {
const onPress = (e: PressEvent) => alert(e.target.innerHTML) const onPress = (e: PressEvent) => alert(e.target.innerHTML)
const onClick = (e: React.MouseEvent<HTMLAnchorElement>) =>
alert(`Link to: ${e.currentTarget.href}`)
return ( return (
<header className={styles.header}> <header className={styles.header}>
@@ -42,27 +40,27 @@ export function Header() {
<div className={styles.chips}> <div className={styles.chips}>
<Chips> <Chips>
<ChipButton onPress={onPress}>Restaurants and bars</ChipButton> <ChipButton onPress={onPress}>Restaurants and bars</ChipButton>
<ChipLink onClick={onClick} href="/kristianshavn"> <ChipLink onPress={onPress} href="/kristianshavn">
Kristianshavn Kristianshavn
</ChipLink> </ChipLink>
<ChipButton onPress={onPress}>Tivoli Gardens</ChipButton> <ChipButton onPress={onPress}>Tivoli Gardens</ChipButton>
<ChipLink onClick={onClick} href="/copenhagen"> <ChipLink onPress={onPress} href="/copenhagen">
See all Copenhagen See all Copenhagen
</ChipLink> </ChipLink>
<ChipButton onPress={onPress}>See all Copenhagen</ChipButton> <ChipButton onPress={onPress}>See all Copenhagen</ChipButton>
<ChipLink onClick={onClick} href="/copenhagen"> <ChipLink onPress={onPress} href="/copenhagen">
See all Copenhagen See all Copenhagen
</ChipLink> </ChipLink>
<ChipButton onPress={onPress}>See all Copenhagen</ChipButton> <ChipButton onPress={onPress}>See all Copenhagen</ChipButton>
<ChipLink onClick={onClick} href="/copenhagen"> <ChipLink onPress={onPress} href="/copenhagen">
See all Copenhagen See all Copenhagen
</ChipLink> </ChipLink>
<ChipButton onPress={onPress}>See all Copenhagen</ChipButton> <ChipButton onPress={onPress}>See all Copenhagen</ChipButton>
<ChipLink onClick={onClick} href="/copenhagen"> <ChipLink onPress={onPress} href="/copenhagen">
See all Copenhagen See all Copenhagen
</ChipLink> </ChipLink>
<ChipButton onPress={onPress}>See all Copenhagen</ChipButton> <ChipButton onPress={onPress}>See all Copenhagen</ChipButton>
<ChipLink onClick={onClick} href="/copenhagen"> <ChipLink onPress={onPress} href="/copenhagen">
See all Copenhagen See all Copenhagen
</ChipLink> </ChipLink>
</Chips> </Chips>

View File

@@ -26,7 +26,6 @@
background-color: var(--Surface-Primary-Hover); background-color: var(--Surface-Primary-Hover);
} }
.Outlined:focus,
.Outlined:active { .Outlined:active {
border-color: var(--Border-Interactive-Selected); border-color: var(--Border-Interactive-Selected);
} }

View File

@@ -1,10 +1,10 @@
import type { Meta, StoryObj } from '@storybook/react' import type { Meta, StoryObj } from '@storybook/react'
import { ChipLink } from './ChipLink.tsx'
import { MaterialIcon } from '../Icons/MaterialIcon' import { MaterialIcon } from '../Icons/MaterialIcon'
import { ChipLink } from './ChipLink.tsx'
const meta: Meta<typeof ChipLink> = { const meta: Meta<typeof ChipLink> = {
title: 'Components/Chip/ChipLInk 🚧', title: 'Components/Chip/ChipLink',
component: ChipLink, component: ChipLink,
} }
@@ -15,7 +15,7 @@ type Story = StoryObj<typeof ChipLink>
export const Default: Story = { export const Default: Story = {
args: { args: {
href: '/', href: '/',
onClick: (e) => e.preventDefault(), onPress: (e) => console.log(e),
children: ( children: (
<> <>
Link Chip Link Chip

View File

@@ -1,19 +1,24 @@
import { cx } from 'class-variance-authority'
import { Typography } from '../Typography' import { Typography } from '../Typography'
import styles from './chip-link.module.css' import styles from './chip-link.module.css'
import type { PropsWithChildren } from 'react' import type { PropsWithChildren } from 'react'
import {
Link as LinkRAC,
LinkProps as LinkRACProps,
} from 'react-aria-components'
export function ChipLink({ export function ChipLink({
children, children,
className, className,
...props ...props
}: PropsWithChildren<React.AnchorHTMLAttributes<HTMLAnchorElement>>) { }: PropsWithChildren<LinkRACProps>) {
return ( return (
<Typography variant="Body/Supporting text (caption)/smBold"> <Typography variant="Body/Supporting text (caption)/smBold">
<a {...props} className={[styles.chip, className].join(' ')}> <LinkRAC {...props} className={cx(styles.chip, className)}>
{children} {children}
</a> </LinkRAC>
</Typography> </Typography>
) )
} }

View File

@@ -1,18 +1,21 @@
.chip { .chip {
background-color: var(--Component-Button-Inverted-Fill-Default); background-color: var(--Component-Button-Inverted-Fill-Default);
border-color: var(--Component-Button-Inverted-Border-Default); border: 1px solid var(--Component-Button-Inverted-Border-Default);
border-style: solid;
border-width: 1px;
border-radius: var(--Corner-radius-sm); border-radius: var(--Corner-radius-sm);
padding: var(--Space-x1) var(--Space-x15); padding: var(--Space-x1) var(--Space-x15);
color: var(--Text-Interactive-Default); color: var(--Text-Interactive-Default);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center;
cursor: pointer;
gap: var(--Space-x05);
} }
.chip:hover { .chip:hover {
/* TODO: change to proper Component-variable once it is available */ background-color: var(--Surface-Primary-Hover-Accent);
background-color: var(--Scandic-Peach-10); }
/* TODO: change to proper Component-variable once it is available */
color: var(--Scandic-Red-100); .chip:focus {
outline-offset: 4px;
outline-color: var(--Border-Interactive-Focus);
} }