Merged in feat/SW-2266-campaign-hero (pull request #2344)

Feat/SW-2266 campaign hero

Approved-by: Erik Tiekstra
This commit is contained in:
Matilda Landström
2025-06-18 13:35:38 +00:00
parent db0e92ff12
commit 61317e0c94
14 changed files with 329 additions and 14 deletions

View File

@@ -0,0 +1,86 @@
.content {
border-radius: var(--Corner-radius-Large);
transform: translateY(-170px);
margin: 0 auto -170px;
width: 90%;
padding: var(--Space-x3);
display: grid;
gap: var(--Space-x2);
}
.imageContainer {
overflow: hidden;
width: 100%;
height: 478px;
position: relative;
border-radius: var(--Corner-radius-Large);
}
.benefitList {
display: grid;
list-style-type: none;
gap: var(--Space-x05);
}
.benefitList > li {
display: flex;
gap: var(--Space-x1);
align-items: center;
}
.peach {
.content {
background-color: var(--Surface-Brand-Accent-Default);
}
.heading {
color: var(--Text-Brand-OnAccent-Heading);
}
.text {
color: var(--Text-Brand-OnPrimary-1-Default);
}
.benefitList > li {
color: var(--Surface-Brand-Primary-1-OnSurface-Accent-Secondary);
}
}
.burgundy {
.content {
background-color: var(--Surface-Brand-Primary-3-Default);
}
.heading {
color: var(--Text-Brand-OnPrimary-3-Accent);
}
.benefitList > li,
.text {
color: var(--Text-Brand-OnPrimary-3-Default);
}
}
@media screen and (min-width: 768px) {
.content {
position: relative;
width: 100%;
transform: none;
border-radius: 0;
margin: 0;
align-content: center;
}
.hero {
border-radius: var(--Corner-radius-Large);
display: grid;
grid-template-columns: repeat(3, 1fr);
overflow: hidden;
}
.imageContainer {
grid-column: span 2;
position: relative;
border-radius: 0;
}
}

View File

@@ -0,0 +1,99 @@
/* eslint-disable formatjs/no-literal-string-in-jsx */
import { Divider } from "@scandic-hotels/design-system/Divider"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { Typography } from "@scandic-hotels/design-system/Typography"
import ButtonLink from "@/components/ButtonLink"
import Image from "@/components/Image"
import { variants } from "./variants"
import styles from "./hero.module.css"
import type { HeroProps } from "./types"
export default async function CampaignHero({
image,
heading,
benefits,
rate_text,
button,
theme,
}: HeroProps) {
const classNames = variants({
theme,
})
return (
<header className={classNames}>
{image ? (
<div className={styles.imageContainer}>
<Image
src={image.url}
alt={image.meta.alt || image.meta.caption || ""}
className={styles.image}
fill
sizes="(min-width: 768px) 413px, 100vw"
focalPoint={image.focalPoint}
/>
</div>
) : null}
<div className={styles.content}>
<Typography variant="Title/xs" className={styles.heading}>
<h2>{heading}</h2>
</Typography>
{benefits?.length ? (
<ul className={styles.benefitList}>
{benefits.map((benefit) => (
<li key={benefit}>
<MaterialIcon
icon="check_circle"
color="CurrentColor"
size={20}
/>
<Typography
variant="Body/Paragraph/mdRegular"
className={styles.text}
>
<span>{benefit}</span>
</Typography>
</li>
))}
</ul>
) : null}
<Divider
color={
theme === "Peach"
? "Surface/Brand/Primary 1/OnSurface/Accent Secondary"
: "white"
}
/>
{rate_text ? (
<span className={styles.heading}>
{rate_text.bold_text ? (
<Typography variant="Title/Subtitle/lg">
<span>{rate_text.bold_text}</span>
</Typography>
) : null}
{rate_text.bold_text && rate_text.text ? " " : null}
{rate_text.text ? (
<Typography variant="Tag/sm">
<span>{rate_text.text}</span>
</Typography>
) : null}
</span>
) : null}
{button.link ? (
<ButtonLink
variant="Secondary"
color={theme === "Peach" ? "Primary" : "Inverted"}
href={button.link.url}
typography="Body/Paragraph/mdBold"
size="Medium"
>
{button.cta}
</ButtonLink>
) : null}
</div>
</header>
)
}

View File

@@ -0,0 +1,9 @@
import type { VariantProps } from "class-variance-authority"
import type { Hero } from "@/types/trpc/routers/contentstack/campaignPage"
import type { variants } from "./variants"
export interface HeroProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof variants>,
Omit<Hero, "theme"> {}

View File

@@ -0,0 +1,17 @@
import { cva } from "class-variance-authority"
import styles from "./hero.module.css"
const config = {
variants: {
theme: {
Peach: styles.peach,
Burgundy: styles.burgundy,
},
},
defaultVariants: {
theme: "Peach",
},
} as const
export const variants = cva(styles.hero, config)

View File

@@ -6,16 +6,6 @@
margin: 0 auto; margin: 0 auto;
} }
.hero {
/* Temporary styles until we add the hero */
width: 100%;
height: 478px;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--Surface-Brand-Accent-Default);
}
.intro { .intro {
display: grid; display: grid;
gap: var(--Space-x5); gap: var(--Space-x5);

View File

@@ -8,6 +8,7 @@ import { getCampaignPage } from "@/lib/trpc/memoizedRequests"
import Blocks from "./Blocks" import Blocks from "./Blocks"
import CampaignPageSkeleton from "./CampaignPageSkeleton" import CampaignPageSkeleton from "./CampaignPageSkeleton"
import CampaignHero from "./Hero"
import styles from "./campaignPage.module.css" import styles from "./campaignPage.module.css"
@@ -19,14 +20,13 @@ export default async function CampaignPage() {
} }
const { campaignPage } = pageData const { campaignPage } = pageData
const { heading, subheading, preamble, blocks } = campaignPage const { heading, subheading, preamble, blocks, hero } = campaignPage
return ( return (
<> <>
<Suspense fallback={<CampaignPageSkeleton />}> <Suspense fallback={<CampaignPageSkeleton />}>
<div className={styles.pageContainer}> <div className={styles.pageContainer}>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */} <CampaignHero {...hero} />
<div className={styles.hero}>---PLACE FOR THE HERO---</div>
<div className={styles.intro}> <div className={styles.intro}>
<div className={styles.headingWrapper}> <div className={styles.headingWrapper}>
<Typography variant="Title/lg"> <Typography variant="Title/lg">

View File

@@ -0,0 +1,78 @@
#import "../PageLink/AccountPageLink.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 "../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 Hero_CampaignPage on CampaignPage {
hero {
image
heading
theme
benefits
rate_text {
bold_text
text
}
button {
cta
linkConnection {
edges {
node {
__typename
...AccountPageLink
...CampaignPageLink
...CollectionPageLink
...ContentPageLink
...DestinationCityPageLink
...DestinationCountryPageLink
...DestinationOverviewPageLink
...HotelPageLink
...LoyaltyPageLink
...StartPageLink
}
}
}
}
}
}
fragment HeroRef_CampaignPage on CampaignPage {
hero {
button {
linkConnection {
edges {
node {
__typename
...AccountPageRef
...CampaignPageRef
...CollectionPageRef
...ContentPageRef
...DestinationCityPageRef
...DestinationCountryPageRef
...DestinationOverviewPageRef
...HotelPageRef
...LoyaltyPageRef
...StartPageRef
}
}
}
}
}
}

View File

@@ -3,6 +3,7 @@
#import "../../Fragments/Blocks/Accordion.graphql" #import "../../Fragments/Blocks/Accordion.graphql"
#import "../../Fragments/Blocks/Essentials.graphql" #import "../../Fragments/Blocks/Essentials.graphql"
#import "../../Fragments/Blocks/CarouselCards.graphql" #import "../../Fragments/Blocks/CarouselCards.graphql"
#import "../../Fragments/CampaignPage/Hero.graphql"
query GetCampaignPage($locale: String!, $uid: String!) { query GetCampaignPage($locale: String!, $uid: String!) {
campaign_page(uid: $uid, locale: $locale) { campaign_page(uid: $uid, locale: $locale) {
@@ -26,6 +27,7 @@ query GetCampaignPage($locale: String!, $uid: String!) {
created_at created_at
updated_at updated_at
} }
...Hero_CampaignPage
} }
trackingProps: campaign_page(locale: "en", uid: $uid) { trackingProps: campaign_page(locale: "en", uid: $uid) {
url url
@@ -39,6 +41,7 @@ query GetCampaignPageRefs($locale: String!, $uid: String!) {
...CarouselCards_CampaignPageRefs ...CarouselCards_CampaignPageRefs
...Accordion_CampaignPageRefs ...Accordion_CampaignPageRefs
} }
...HeroRef_CampaignPage
system { system {
...System ...System
} }

View File

@@ -11,6 +11,11 @@ import {
carouselCardsSchema, carouselCardsSchema,
} from "../schemas/blocks/carouselCards" } from "../schemas/blocks/carouselCards"
import { essentialsBlockSchema } from "../schemas/blocks/essentials" import { essentialsBlockSchema } from "../schemas/blocks/essentials"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import {
linkConnectionRefs,
linkConnectionSchema,
} from "../schemas/linkConnection"
import { systemSchema } from "../schemas/system" import { systemSchema } from "../schemas/system"
import { CampaignPageEnum } from "@/types/enums/campaignPage" import { CampaignPageEnum } from "@/types/enums/campaignPage"
@@ -39,10 +44,25 @@ export const blocksSchema = z.discriminatedUnion("__typename", [
campaignPageAccordion, campaignPageAccordion,
]) ])
export const heroSchema = z.object({
image: tempImageVaultAssetSchema,
heading: z.string(),
theme: z.enum(["Peach", "Burgundy"]).default("Peach"),
benefits: z.array(z.string()).nullish(),
rate_text: z
.object({
bold_text: z.string().nullish(),
text: z.string().nullish(),
})
.nullish(),
button: z.intersection(z.object({ cta: z.string() }), linkConnectionSchema),
})
export const campaignPageSchema = z.object({ export const campaignPageSchema = z.object({
campaign_page: z.object({ campaign_page: z.object({
title: z.string(), title: z.string(),
campaign_identifier: z.string().nullish(), campaign_identifier: z.string().nullish(),
hero: heroSchema,
heading: z.string(), heading: z.string(),
subheading: z.string().nullish(), subheading: z.string().nullish(),
preamble: z.object({ preamble: z.object({
@@ -80,9 +100,13 @@ const campaignPageBlockRefsItem = z.discriminatedUnion("__typename", [
campaignPageCarouselCardsRef, campaignPageCarouselCardsRef,
campaignPageAccordionRefs, campaignPageAccordionRefs,
]) ])
const heroRefsSchema = z.object({
button: linkConnectionRefs,
})
export const campaignPageRefsSchema = z.object({ export const campaignPageRefsSchema = z.object({
campaign_page: z.object({ campaign_page: z.object({
hero: heroRefsSchema,
blocks: discriminatedUnionArray( blocks: discriminatedUnionArray(
campaignPageBlockRefsItem.options campaignPageBlockRefsItem.options
).nullable(), ).nullable(),

View File

@@ -46,7 +46,6 @@ export const campaignPageQueryRouter = router({
ttl: "max", ttl: "max",
} }
) )
if (!refsResponse.data) { if (!refsResponse.data) {
const notFoundError = notFound(refsResponse) const notFoundError = notFound(refsResponse)
metricsGetCampaignPageRefs.noDataError() metricsGetCampaignPageRefs.noDataError()

View File

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

View File

@@ -4,6 +4,7 @@ import type {
blocksSchema, blocksSchema,
campaignPageRefsSchema, campaignPageRefsSchema,
campaignPageSchema, campaignPageSchema,
heroSchema,
} from "@/server/routers/contentstack/campaignPage/output" } from "@/server/routers/contentstack/campaignPage/output"
import type { essentialsSchema } from "@/server/routers/contentstack/schemas/blocks/essentials" import type { essentialsSchema } from "@/server/routers/contentstack/schemas/blocks/essentials"
@@ -21,3 +22,5 @@ export interface CampaignPageRefs
export type Block = z.output<typeof blocksSchema> export type Block = z.output<typeof blocksSchema>
export type EssentialsBlock = z.output<typeof essentialsSchema>["essentials"] export type EssentialsBlock = z.output<typeof essentialsSchema>["essentials"]
export type Hero = z.output<typeof heroSchema>

View File

@@ -36,3 +36,7 @@
.Border-Divider-Default { .Border-Divider-Default {
background-color: var(--Border-Divider-Default); background-color: var(--Border-Divider-Default);
} }
.Surface-Brand-Primary-1-OnSurface-Accent-Secondary {
background-color: var(--Surface-Brand-Primary-1-OnSurface-Accent-Secondary);
}

View File

@@ -11,6 +11,8 @@ export const dividerVariants = cva(styles.divider, {
white: styles.white, white: styles.white,
'Border/Divider/Default': styles['Border-Divider-Default'], 'Border/Divider/Default': styles['Border-Divider-Default'],
'Border/Divider/Subtle': styles['Border-Divider-Subtle'], 'Border/Divider/Subtle': styles['Border-Divider-Subtle'],
'Surface/Brand/Primary 1/OnSurface/Accent Secondary':
styles['Surface-Brand-Primary-1-OnSurface-Accent-Secondary'],
}, },
variant: { variant: {
horizontal: styles.horizontal, horizontal: styles.horizontal,