Merged in feat/LOY-361-Promo-Campaign-Hero (pull request #2857)
Feat(LOY-363): Promo Campaign Hero * feat(LOY-361): Add Promo Campaign Hero * feat(LOY-361): auth cta's wip * fix(LOY-361): improve hero card css * fix(LOY-361): correct size for button * fix(LOY-361): Make Promo Hero Required * fix(LOY-361): semantic css classes Approved-by: Matilda Landström
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { Button } from "@scandic-hotels/design-system/Button"
|
||||||
|
|
||||||
|
import styles from "../hero.module.css"
|
||||||
|
|
||||||
|
// TODO: Trigger acivation.
|
||||||
|
export default function ActivateOfferButton() {
|
||||||
|
const intl = useIntl()
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="Tertiary"
|
||||||
|
size="Medium"
|
||||||
|
typography="Body/Paragraph/mdRegular"
|
||||||
|
className={styles.activateButton}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Activate offer",
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
.hero {
|
||||||
|
--card-float-offset: -170px;
|
||||||
|
--card-width: 90%;
|
||||||
|
--card-radius: var(--Corner-radius-Large);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
min-height: 478px;
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: var(--card-float-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
height: 478px;
|
||||||
|
position: relative;
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardSection {
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroLoggedIn .cardSection {
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heroLoggedOut .cardSection {
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: var(--card-width);
|
||||||
|
margin-inline: auto;
|
||||||
|
padding: var(--Space-x3);
|
||||||
|
transform: translateY(var(--card-float-offset));
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
align-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefitsCard {
|
||||||
|
background-color: var(--Surface-Brand-Accent-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.cardSection > .benefitsCard:only-child {
|
||||||
|
border-radius: var(--card-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardSection > .benefitsCard:not(:only-child) {
|
||||||
|
border-radius: var(--card-radius) var(--card-radius) 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.authCard {
|
||||||
|
background-color: var(--Surface-Brand-Primary-3-Default);
|
||||||
|
border-radius: 0 0 var(--card-radius) var(--card-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading {
|
||||||
|
color: var(--Text-Brand-OnAccent-Heading);
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefitList {
|
||||||
|
display: grid;
|
||||||
|
list-style-type: none;
|
||||||
|
gap: var(--Space-x05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefitList > li {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
color: var(--Surface-Brand-Primary-1-OnSurface-Accent-Secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
color: var(--Text-Brand-OnPrimary-1-Default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activateButton {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.authHeading {
|
||||||
|
color: var(--Text-Inverted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orSection {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orText {
|
||||||
|
color: var(--Text-Brand-OnPrimary-3-Default);
|
||||||
|
margin: 0;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.hero {
|
||||||
|
--card-float-offset: 0;
|
||||||
|
--card-width: 388px;
|
||||||
|
|
||||||
|
grid-template-columns: 1fr var(--card-width);
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.imageContainer {
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefitsCard {
|
||||||
|
border-radius: 0;
|
||||||
|
padding: var(--Space-x7) var(--Space-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.authCard {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import { cx } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { login } from "@scandic-hotels/common/constants/routes/handleAuth"
|
||||||
|
import { signup } from "@scandic-hotels/common/constants/routes/signup"
|
||||||
|
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||||
|
import { Divider } from "@scandic-hotels/design-system/Divider"
|
||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import Image from "@scandic-hotels/design-system/Image"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
import { getLang } from "@/i18n/serverContext"
|
||||||
|
import { isLoggedInUser } from "@/utils/isLoggedInUser"
|
||||||
|
|
||||||
|
import ActivateOfferButton from "./ActivateOfferButton"
|
||||||
|
|
||||||
|
import styles from "./hero.module.css"
|
||||||
|
|
||||||
|
import type { PromoHero } from "@scandic-hotels/trpc/types/promoCampaignPage"
|
||||||
|
|
||||||
|
interface PromoCampaignHeroProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
promoHero: PromoHero
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function PromoCampaignHero({
|
||||||
|
promoHero,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: PromoCampaignHeroProps) {
|
||||||
|
const { image, heading, benefits } = promoHero
|
||||||
|
const isLoggedIn = await isLoggedInUser()
|
||||||
|
const intl = await getIntl()
|
||||||
|
const lang = await getLang()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={cx(
|
||||||
|
styles.hero,
|
||||||
|
isLoggedIn ? styles.heroLoggedIn : styles.heroLoggedOut,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{image ? (
|
||||||
|
<div className={styles.imageContainer}>
|
||||||
|
<Image
|
||||||
|
src={image.url}
|
||||||
|
alt={image.meta.alt || image.meta.caption || ""}
|
||||||
|
className={styles.image}
|
||||||
|
fill
|
||||||
|
sizes="(min-width: 768px) 800px, 100vw"
|
||||||
|
focalPoint={image.focalPoint}
|
||||||
|
dimensions={image.dimensions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<div className={styles.cardSection}>
|
||||||
|
<div className={cx(styles.card, styles.benefitsCard)}>
|
||||||
|
<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="favorite"
|
||||||
|
isFilled
|
||||||
|
color="CurrentColor"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
<Typography
|
||||||
|
variant="Body/Paragraph/mdRegular"
|
||||||
|
className={styles.text}
|
||||||
|
>
|
||||||
|
<span>{benefit}</span>
|
||||||
|
</Typography>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{isLoggedIn && (
|
||||||
|
<div>
|
||||||
|
{/* TODO: Account for more activation states. */}
|
||||||
|
<ActivateOfferButton />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isLoggedIn && (
|
||||||
|
<div className={cx(styles.card, styles.authCard)}>
|
||||||
|
<Typography
|
||||||
|
variant="Title/Overline/sm"
|
||||||
|
className={styles.authHeading}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "TO ACTIVATE OFFER",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
<ButtonLink
|
||||||
|
href={login[lang]}
|
||||||
|
variant="Primary"
|
||||||
|
color="Inverted"
|
||||||
|
size="Medium"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Login",
|
||||||
|
})}
|
||||||
|
</ButtonLink>
|
||||||
|
<div className={styles.orSection}>
|
||||||
|
<Divider color="white" />
|
||||||
|
<Typography
|
||||||
|
variant="Body/Paragraph/mdRegular"
|
||||||
|
className={styles.orText}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "or",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</Typography>
|
||||||
|
<Divider color="white" />
|
||||||
|
</div>
|
||||||
|
<ButtonLink
|
||||||
|
href={signup[lang]}
|
||||||
|
variant="Secondary"
|
||||||
|
color="Inverted"
|
||||||
|
size="Medium"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "Sign up",
|
||||||
|
})}
|
||||||
|
</ButtonLink>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { TrackingSDK } from "@scandic-hotels/tracking/TrackingSDK"
|
|||||||
|
|
||||||
import { getPromoCampaignPage } from "@/lib/trpc/memoizedRequests"
|
import { getPromoCampaignPage } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
|
import PromoCampaignHero from "./Hero"
|
||||||
import PromoCampaignPageSkeleton from "./PromoCampaignPageSkeleton"
|
import PromoCampaignPageSkeleton from "./PromoCampaignPageSkeleton"
|
||||||
|
|
||||||
import styles from "./promoCampaignPage.module.css"
|
import styles from "./promoCampaignPage.module.css"
|
||||||
@@ -17,12 +18,13 @@ export default async function PromoCampaignPage() {
|
|||||||
}
|
}
|
||||||
//const isUserLoggedIn = await isLoggedInUser()
|
//const isUserLoggedIn = await isLoggedInUser()
|
||||||
const { promo_campaign_page, tracking } = pageData
|
const { promo_campaign_page, tracking } = pageData
|
||||||
const { heading, subheading } = promo_campaign_page
|
const { heading, subheading, promo_hero } = promo_campaign_page
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Suspense fallback={<PromoCampaignPageSkeleton />}>
|
<Suspense fallback={<PromoCampaignPageSkeleton />}>
|
||||||
<div className={styles.pageContainer}>
|
<div className={styles.pageContainer}>
|
||||||
|
<PromoCampaignHero promoHero={promo_hero} />
|
||||||
<div className={styles.intro}>
|
<div className={styles.intro}>
|
||||||
<div className={styles.headingWrapper}>
|
<div className={styles.headingWrapper}>
|
||||||
<Typography variant="Title/lg">
|
<Typography variant="Title/lg">
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ query GetPromoCampaignPage($locale: String!, $uid: String!) {
|
|||||||
title
|
title
|
||||||
heading
|
heading
|
||||||
subheading
|
subheading
|
||||||
|
promo_hero {
|
||||||
|
image
|
||||||
|
heading
|
||||||
|
benefits
|
||||||
|
}
|
||||||
page_settings {
|
page_settings {
|
||||||
booking_code
|
booking_code
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
|
import { transformedImageVaultAssetSchema } from "@scandic-hotels/common/utils/imageVault"
|
||||||
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
|
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
|
||||||
|
|
||||||
import { systemSchema } from "../schemas/system"
|
import { systemSchema } from "../schemas/system"
|
||||||
@@ -9,12 +10,22 @@ export const CAMPAIGN_TYPES = {
|
|||||||
POINT: "POINT",
|
POINT: "POINT",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
export const promoHeroSchema = z.object({
|
||||||
|
image: transformedImageVaultAssetSchema,
|
||||||
|
heading: z.string(),
|
||||||
|
benefits: z
|
||||||
|
.array(z.string())
|
||||||
|
.nullish()
|
||||||
|
.transform((data) => data || []),
|
||||||
|
})
|
||||||
|
|
||||||
export const promoCampaignPageSchema = z
|
export const promoCampaignPageSchema = z
|
||||||
.object({
|
.object({
|
||||||
promo_campaign_page: z.object({
|
promo_campaign_page: z.object({
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
heading: z.string(),
|
heading: z.string(),
|
||||||
subheading: z.string().nullish(),
|
subheading: z.string().nullish(),
|
||||||
|
promo_hero: promoHeroSchema,
|
||||||
page_settings: z
|
page_settings: z
|
||||||
.object({
|
.object({
|
||||||
booking_code: z.string().nullish(),
|
booking_code: z.string().nullish(),
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ export interface PromoCampaignPage
|
|||||||
|
|
||||||
export type PromoCampaignPageData = PromoCampaignPage["promo_campaign_page"]
|
export type PromoCampaignPageData = PromoCampaignPage["promo_campaign_page"]
|
||||||
|
|
||||||
|
export type PromoHero = NonNullable<PromoCampaignPageData["promo_hero"]>
|
||||||
|
|
||||||
/* REFS */
|
/* REFS */
|
||||||
export interface GetPromoCampaignPageRefsData
|
export interface GetPromoCampaignPageRefsData
|
||||||
extends z.input<typeof promoCampaignPageRefsSchema> {}
|
extends z.input<typeof promoCampaignPageRefsSchema> {}
|
||||||
|
|||||||
Reference in New Issue
Block a user