Merged in feat/LOY-365-promo-campaign-eligible-levels (pull request #2864)
feat(LOY-365): Add support for eligible levels for promo campaign pages * feat(LOY-365): Add support for eligible levels for promo campaign pages * fix(LOY-365): update to most recent copy * fix(LOY-365): cleanup css * fix(LOY-365): Move ineligible message to the bottom * fix(LOY-365): remove uneeded type Approved-by: Erik Tiekstra Approved-by: Matilda Landström
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import { useIntl } from "react-intl"
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { isMembershipLevel } from "@scandic-hotels/common/utils/membershipLevels"
|
||||||
|
|
||||||
import { TIER_TO_FRIEND_MAP } from "@/constants/membershipLevels"
|
import { TIER_TO_FRIEND_MAP } from "@/constants/membershipLevels"
|
||||||
|
|
||||||
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
|
import BiroScript from "@/components/TempDesignSystem/Text/BiroScript"
|
||||||
import { isMembershipLevel } from "@/utils/membershipLevels"
|
|
||||||
|
|
||||||
import type { ScriptedRewardTextProps } from "@/types/components/myPages/myPage/accountPage"
|
import type { ScriptedRewardTextProps } from "@/types/components/myPages/myPage/accountPage"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { getIntl } from "@/i18n"
|
||||||
|
|
||||||
|
import styles from "../hero.module.css"
|
||||||
|
|
||||||
|
// @TODO: The ineligble message has not been finalized yet and is prone to change.
|
||||||
|
export default async function IneligibleMessage() {
|
||||||
|
const intl = await getIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.ineligibleMessage}>
|
||||||
|
<span className={styles.ineligibleIcon}>
|
||||||
|
<MaterialIcon
|
||||||
|
icon="info"
|
||||||
|
isFilled
|
||||||
|
color="Icon/Feedback/Information"
|
||||||
|
size={20}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div className={styles.ineligibleContent}>
|
||||||
|
<Typography
|
||||||
|
variant="Body/Paragraph/mdBold"
|
||||||
|
className={styles.ineligibleTitle}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage: "This campaign is not available for you",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="Body/Paragraph/mdRegular"
|
||||||
|
className={styles.ineligibleSubtitle}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{intl.formatMessage({
|
||||||
|
defaultMessage:
|
||||||
|
"See terms & conditions for all qualifying requirements.",
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -47,6 +47,18 @@
|
|||||||
|
|
||||||
.benefitsCard {
|
.benefitsCard {
|
||||||
background-color: var(--Surface-Brand-Accent-Default);
|
background-color: var(--Surface-Brand-Accent-Default);
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefitsCard:has(.ineligibleMessage) {
|
||||||
|
grid-template-rows: 1fr auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.benefitsContent {
|
||||||
|
gap: var(--Space-x2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
@@ -88,6 +100,27 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ineligibleMessage {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 20px 1fr;
|
||||||
|
padding: var(--Space-x15);
|
||||||
|
gap: var(--Space-x1);
|
||||||
|
background-color: var(--Surface-Primary-Default);
|
||||||
|
border-radius: var(--Corner-radius-md);
|
||||||
|
border: 1px solid var(--Border-Default);
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ineligibleIcon {
|
||||||
|
justify-self: center;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ineligibleTitle,
|
||||||
|
.ineligibleSubtitle {
|
||||||
|
color: var(--Text-Default);
|
||||||
|
}
|
||||||
|
|
||||||
.authHeading {
|
.authHeading {
|
||||||
color: var(--Text-Inverted);
|
color: var(--Text-Inverted);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,27 +8,38 @@ import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
|||||||
import Image from "@scandic-hotels/design-system/Image"
|
import Image from "@scandic-hotels/design-system/Image"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
|
import { getProfileSafely } from "@/lib/trpc/memoizedRequests"
|
||||||
|
|
||||||
import { getIntl } from "@/i18n"
|
import { getIntl } from "@/i18n"
|
||||||
import { getLang } from "@/i18n/serverContext"
|
import { getLang } from "@/i18n/serverContext"
|
||||||
import { isLoggedInUser } from "@/utils/isLoggedInUser"
|
|
||||||
|
|
||||||
import ActivateOfferButton from "./ActivateOfferButton"
|
import ActivateOfferButton from "./ActivateOfferButton"
|
||||||
|
import IneligibleMessage from "./IneligibleMessage"
|
||||||
|
import { isUserEligibleForPromo } from "./utils"
|
||||||
|
|
||||||
import styles from "./hero.module.css"
|
import styles from "./hero.module.css"
|
||||||
|
|
||||||
|
import type { MembershipLevel } from "@scandic-hotels/common/constants/membershipLevels"
|
||||||
import type { PromoHero } from "@scandic-hotels/trpc/types/promoCampaignPage"
|
import type { PromoHero } from "@scandic-hotels/trpc/types/promoCampaignPage"
|
||||||
|
|
||||||
interface PromoCampaignHeroProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface PromoCampaignHeroProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
promoHero: PromoHero
|
promoHero: PromoHero
|
||||||
|
eligibleLevels: MembershipLevel[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function PromoCampaignHero({
|
export default async function PromoCampaignHero({
|
||||||
promoHero,
|
promoHero,
|
||||||
|
eligibleLevels,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: PromoCampaignHeroProps) {
|
}: PromoCampaignHeroProps) {
|
||||||
const { image, heading, benefits } = promoHero
|
const { image, heading, benefits } = promoHero
|
||||||
const isLoggedIn = await isLoggedInUser()
|
const profile = await getProfileSafely()
|
||||||
|
const isLoggedIn = !!profile
|
||||||
|
const userMembershipLevel = profile?.membership?.membershipLevel
|
||||||
|
const isEligible = isLoggedIn
|
||||||
|
? isUserEligibleForPromo(userMembershipLevel, eligibleLevels)
|
||||||
|
: false
|
||||||
const intl = await getIntl()
|
const intl = await getIntl()
|
||||||
const lang = await getLang()
|
const lang = await getLang()
|
||||||
|
|
||||||
@@ -57,36 +68,34 @@ export default async function PromoCampaignHero({
|
|||||||
|
|
||||||
<div className={styles.cardSection}>
|
<div className={styles.cardSection}>
|
||||||
<div className={cx(styles.card, styles.benefitsCard)}>
|
<div className={cx(styles.card, styles.benefitsCard)}>
|
||||||
<Typography variant="Title/xs" className={styles.heading}>
|
<div className={styles.benefitsContent}>
|
||||||
<h2>{heading}</h2>
|
<Typography variant="Title/xs" className={styles.heading}>
|
||||||
</Typography>
|
<h2>{heading}</h2>
|
||||||
{benefits?.length ? (
|
</Typography>
|
||||||
<ul className={styles.benefitList}>
|
{benefits?.length ? (
|
||||||
{benefits.map((benefit) => (
|
<ul className={styles.benefitList}>
|
||||||
<li key={benefit}>
|
{benefits.map((benefit) => (
|
||||||
<MaterialIcon
|
<li key={benefit}>
|
||||||
icon="favorite"
|
<MaterialIcon
|
||||||
isFilled
|
icon="favorite"
|
||||||
color="CurrentColor"
|
isFilled
|
||||||
size={20}
|
color="CurrentColor"
|
||||||
/>
|
size={20}
|
||||||
<Typography
|
/>
|
||||||
variant="Body/Paragraph/mdRegular"
|
<Typography
|
||||||
className={styles.text}
|
variant="Body/Paragraph/mdRegular"
|
||||||
>
|
className={styles.text}
|
||||||
<span>{benefit}</span>
|
>
|
||||||
</Typography>
|
<span>{benefit}</span>
|
||||||
</li>
|
</Typography>
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
) : null}
|
</ul>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
{isLoggedIn && (
|
{isLoggedIn &&
|
||||||
<div>
|
(isEligible ? <ActivateOfferButton /> : <IneligibleMessage />)}
|
||||||
{/* TODO: Account for more activation states. */}
|
|
||||||
<ActivateOfferButton />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isLoggedIn && (
|
{!isLoggedIn && (
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { MembershipLevel } from "@scandic-hotels/common/constants/membershipLevels"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a user's membership level is eligible for a promotional campaign.
|
||||||
|
* @param userLevel - The user's current membership level
|
||||||
|
* @param eligibleLevels - Array of membership levels eligible for the promo
|
||||||
|
* @returns true if user is eligible, false otherwise
|
||||||
|
*/
|
||||||
|
export function isUserEligibleForPromo(
|
||||||
|
userLevel: MembershipLevel | undefined,
|
||||||
|
eligibleLevels: MembershipLevel[]
|
||||||
|
): boolean {
|
||||||
|
if (!userLevel || eligibleLevels.length === 0) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return eligibleLevels.includes(userLevel)
|
||||||
|
}
|
||||||
@@ -18,9 +18,10 @@ export default async function PromoCampaignPage() {
|
|||||||
if (!pageData) {
|
if (!pageData) {
|
||||||
notFound()
|
notFound()
|
||||||
}
|
}
|
||||||
//const isUserLoggedIn = await isLoggedInUser()
|
|
||||||
const { promo_campaign_page, tracking } = pageData
|
const { promo_campaign_page, tracking } = pageData
|
||||||
const { heading, subheading, enddate, promo_hero } = promo_campaign_page
|
|
||||||
|
const { heading, subheading, enddate, promo_hero, eligibleLevels } =
|
||||||
|
promo_campaign_page
|
||||||
|
|
||||||
const isCampaignExpired = enddate
|
const isCampaignExpired = enddate
|
||||||
? dt().isAfter(dt(enddate).endOf("day"))
|
? dt().isAfter(dt(enddate).endOf("day"))
|
||||||
@@ -30,7 +31,14 @@ export default async function PromoCampaignPage() {
|
|||||||
<>
|
<>
|
||||||
<Suspense fallback={<PromoCampaignPageSkeleton />}>
|
<Suspense fallback={<PromoCampaignPageSkeleton />}>
|
||||||
<div className={styles.pageContainer}>
|
<div className={styles.pageContainer}>
|
||||||
<PromoCampaignHero promoHero={promo_hero} />
|
{isCampaignExpired ? (
|
||||||
|
<ExpiredPromoCampaign />
|
||||||
|
) : (
|
||||||
|
<PromoCampaignHero
|
||||||
|
promoHero={promo_hero}
|
||||||
|
eligibleLevels={eligibleLevels}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className={styles.intro}>
|
<div className={styles.intro}>
|
||||||
<div className={styles.headingWrapper}>
|
<div className={styles.headingWrapper}>
|
||||||
<Typography variant="Title/lg">
|
<Typography variant="Title/lg">
|
||||||
@@ -43,7 +51,6 @@ export default async function PromoCampaignPage() {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isCampaignExpired ? <ExpiredPromoCampaign /> : null}
|
|
||||||
</div>
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<TrackingSDK pageData={tracking} />
|
<TrackingSDK pageData={tracking} />
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import { MembershipLevelEnum } from "@scandic-hotels/common/constants/membershipLevels"
|
|
||||||
|
|
||||||
export function isMembershipLevel(value: string): value is MembershipLevelEnum {
|
|
||||||
return Object.values(MembershipLevelEnum).some((level) => level === value)
|
|
||||||
}
|
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
"./utils/isValidJson": "./utils/isValidJson.ts",
|
"./utils/isValidJson": "./utils/isValidJson.ts",
|
||||||
"./utils/languages": "./utils/languages.ts",
|
"./utils/languages": "./utils/languages.ts",
|
||||||
"./utils/maskValue": "./utils/maskValue.ts",
|
"./utils/maskValue": "./utils/maskValue.ts",
|
||||||
|
"./utils/membershipLevels": "./utils/membershipLevels.ts",
|
||||||
"./utils/numberFormatting": "./utils/numberFormatting.ts",
|
"./utils/numberFormatting": "./utils/numberFormatting.ts",
|
||||||
"./utils/rangeArray": "./utils/rangeArray.ts",
|
"./utils/rangeArray": "./utils/rangeArray.ts",
|
||||||
"./utils/safeTry": "./utils/safeTry.ts",
|
"./utils/safeTry": "./utils/safeTry.ts",
|
||||||
|
|||||||
10
packages/common/utils/membershipLevels.ts
Normal file
10
packages/common/utils/membershipLevels.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { MembershipLevelEnum } from "../constants/membershipLevels"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to check if a string value is a valid MembershipLevel
|
||||||
|
* @param value - The string value to check
|
||||||
|
* @returns true if the value is a valid MembershipLevel, false otherwise
|
||||||
|
*/
|
||||||
|
export function isMembershipLevel(value: string): value is MembershipLevelEnum {
|
||||||
|
return Object.values(MembershipLevelEnum).some((level) => level === value)
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ query GetPromoCampaignPage($locale: String!, $uid: String!) {
|
|||||||
promo_code
|
promo_code
|
||||||
startdate
|
startdate
|
||||||
enddate
|
enddate
|
||||||
|
level_selection
|
||||||
system {
|
system {
|
||||||
...System
|
...System
|
||||||
created_at
|
created_at
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
|
|
||||||
import { transformedImageVaultAssetSchema } from "@scandic-hotels/common/utils/imageVault"
|
import { transformedImageVaultAssetSchema } from "@scandic-hotels/common/utils/imageVault"
|
||||||
|
import { isMembershipLevel } from "@scandic-hotels/common/utils/membershipLevels"
|
||||||
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"
|
||||||
@@ -35,6 +36,13 @@ export const promoCampaignPageSchema = z
|
|||||||
promo_code: z.string(),
|
promo_code: z.string(),
|
||||||
startdate: nullableStringValidator,
|
startdate: nullableStringValidator,
|
||||||
enddate: nullableStringValidator,
|
enddate: nullableStringValidator,
|
||||||
|
level_selection: z
|
||||||
|
.array(z.string())
|
||||||
|
.nullish()
|
||||||
|
.transform((data) => {
|
||||||
|
if (!data) return []
|
||||||
|
return data.filter(isMembershipLevel)
|
||||||
|
}),
|
||||||
system: systemSchema.merge(
|
system: systemSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
created_at: z.string(),
|
created_at: z.string(),
|
||||||
@@ -47,13 +55,15 @@ export const promoCampaignPageSchema = z
|
|||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.transform(({ promo_campaign_page, ...data }) => {
|
.transform(({ promo_campaign_page, ...data }) => {
|
||||||
const { page_settings, ...promoCampaignPageData } = promo_campaign_page
|
const { page_settings, level_selection, ...promoCampaignPageData } =
|
||||||
|
promo_campaign_page
|
||||||
const bookingCode = page_settings?.booking_code || null
|
const bookingCode = page_settings?.booking_code || null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...data,
|
...data,
|
||||||
promo_campaign_page: {
|
promo_campaign_page: {
|
||||||
bookingCode,
|
bookingCode,
|
||||||
|
eligibleLevels: level_selection,
|
||||||
...promoCampaignPageData,
|
...promoCampaignPageData,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user