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 PromoCampaignHero from "./Hero"
|
||||
import PromoCampaignPageSkeleton from "./PromoCampaignPageSkeleton"
|
||||
|
||||
import styles from "./promoCampaignPage.module.css"
|
||||
@@ -17,12 +18,13 @@ export default async function PromoCampaignPage() {
|
||||
}
|
||||
//const isUserLoggedIn = await isLoggedInUser()
|
||||
const { promo_campaign_page, tracking } = pageData
|
||||
const { heading, subheading } = promo_campaign_page
|
||||
const { heading, subheading, promo_hero } = promo_campaign_page
|
||||
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={<PromoCampaignPageSkeleton />}>
|
||||
<div className={styles.pageContainer}>
|
||||
<PromoCampaignHero promoHero={promo_hero} />
|
||||
<div className={styles.intro}>
|
||||
<div className={styles.headingWrapper}>
|
||||
<Typography variant="Title/lg">
|
||||
|
||||
@@ -13,6 +13,11 @@ query GetPromoCampaignPage($locale: String!, $uid: String!) {
|
||||
title
|
||||
heading
|
||||
subheading
|
||||
promo_hero {
|
||||
image
|
||||
heading
|
||||
benefits
|
||||
}
|
||||
page_settings {
|
||||
booking_code
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { transformedImageVaultAssetSchema } from "@scandic-hotels/common/utils/imageVault"
|
||||
import { nullableStringValidator } from "@scandic-hotels/common/utils/zod/stringValidator"
|
||||
|
||||
import { systemSchema } from "../schemas/system"
|
||||
@@ -9,12 +10,22 @@ export const CAMPAIGN_TYPES = {
|
||||
POINT: "POINT",
|
||||
} 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
|
||||
.object({
|
||||
promo_campaign_page: z.object({
|
||||
title: z.string(),
|
||||
heading: z.string(),
|
||||
subheading: z.string().nullish(),
|
||||
promo_hero: promoHeroSchema,
|
||||
page_settings: z
|
||||
.object({
|
||||
booking_code: z.string().nullish(),
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface PromoCampaignPage
|
||||
|
||||
export type PromoCampaignPageData = PromoCampaignPage["promo_campaign_page"]
|
||||
|
||||
export type PromoHero = NonNullable<PromoCampaignPageData["promo_hero"]>
|
||||
|
||||
/* REFS */
|
||||
export interface GetPromoCampaignPageRefsData
|
||||
extends z.input<typeof promoCampaignPageRefsSchema> {}
|
||||
|
||||
Reference in New Issue
Block a user