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;
}
.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 {
display: grid;
gap: var(--Space-x5);

View File

@@ -8,6 +8,7 @@ import { getCampaignPage } from "@/lib/trpc/memoizedRequests"
import Blocks from "./Blocks"
import CampaignPageSkeleton from "./CampaignPageSkeleton"
import CampaignHero from "./Hero"
import styles from "./campaignPage.module.css"
@@ -19,14 +20,13 @@ export default async function CampaignPage() {
}
const { campaignPage } = pageData
const { heading, subheading, preamble, blocks } = campaignPage
const { heading, subheading, preamble, blocks, hero } = campaignPage
return (
<>
<Suspense fallback={<CampaignPageSkeleton />}>
<div className={styles.pageContainer}>
{/* eslint-disable-next-line formatjs/no-literal-string-in-jsx */}
<div className={styles.hero}>---PLACE FOR THE HERO---</div>
<CampaignHero {...hero} />
<div className={styles.intro}>
<div className={styles.headingWrapper}>
<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/Essentials.graphql"
#import "../../Fragments/Blocks/CarouselCards.graphql"
#import "../../Fragments/CampaignPage/Hero.graphql"
query GetCampaignPage($locale: String!, $uid: String!) {
campaign_page(uid: $uid, locale: $locale) {
@@ -26,6 +27,7 @@ query GetCampaignPage($locale: String!, $uid: String!) {
created_at
updated_at
}
...Hero_CampaignPage
}
trackingProps: campaign_page(locale: "en", uid: $uid) {
url
@@ -39,6 +41,7 @@ query GetCampaignPageRefs($locale: String!, $uid: String!) {
...CarouselCards_CampaignPageRefs
...Accordion_CampaignPageRefs
}
...HeroRef_CampaignPage
system {
...System
}

View File

@@ -11,6 +11,11 @@ import {
carouselCardsSchema,
} from "../schemas/blocks/carouselCards"
import { essentialsBlockSchema } from "../schemas/blocks/essentials"
import { tempImageVaultAssetSchema } from "../schemas/imageVault"
import {
linkConnectionRefs,
linkConnectionSchema,
} from "../schemas/linkConnection"
import { systemSchema } from "../schemas/system"
import { CampaignPageEnum } from "@/types/enums/campaignPage"
@@ -39,10 +44,25 @@ export const blocksSchema = z.discriminatedUnion("__typename", [
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({
campaign_page: z.object({
title: z.string(),
campaign_identifier: z.string().nullish(),
hero: heroSchema,
heading: z.string(),
subheading: z.string().nullish(),
preamble: z.object({
@@ -80,9 +100,13 @@ const campaignPageBlockRefsItem = z.discriminatedUnion("__typename", [
campaignPageCarouselCardsRef,
campaignPageAccordionRefs,
])
const heroRefsSchema = z.object({
button: linkConnectionRefs,
})
export const campaignPageRefsSchema = z.object({
campaign_page: z.object({
hero: heroRefsSchema,
blocks: discriminatedUnionArray(
campaignPageBlockRefsItem.options
).nullable(),

View File

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

View File

@@ -173,6 +173,7 @@ export function transformPageLink(data: Data) {
title: data.title,
url: removeMultipleSlashes(`/${data.system.locale}/${data.url}`),
}
case ContentEnum.blocks.CollectionPage:
case ContentEnum.blocks.ContentPage:
case ContentEnum.blocks.LoyaltyPage:

View File

@@ -4,6 +4,7 @@ import type {
blocksSchema,
campaignPageRefsSchema,
campaignPageSchema,
heroSchema,
} from "@/server/routers/contentstack/campaignPage/output"
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 EssentialsBlock = z.output<typeof essentialsSchema>["essentials"]
export type Hero = z.output<typeof heroSchema>

View File

@@ -36,3 +36,7 @@
.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,
'Border/Divider/Default': styles['Border-Divider-Default'],
'Border/Divider/Subtle': styles['Border-Divider-Subtle'],
'Surface/Brand/Primary 1/OnSurface/Accent Secondary':
styles['Surface-Brand-Primary-1-OnSurface-Accent-Secondary'],
},
variant: {
horizontal: styles.horizontal,