Merged in feat/LOY-315-Membership-Status-Card (pull request #2712)

Feat(LOY-315): Membership Overview Card

* fix(LOY-315): new divider variants

* feat(LOY-315): Add MembershipOverviewCard

* refactor(LOY-315): abstract sasbooststatus

* fix(LOY-315): correct space vars

* fix(LOY-315): date formatting fixes


Approved-by: Erik Tiekstra
This commit is contained in:
Chuma Mcphoy (We Ahead)
2025-09-01 13:42:50 +00:00
parent 9e3294b113
commit 914871607d
7 changed files with 205 additions and 16 deletions

View File

@@ -0,0 +1,44 @@
import { dt } from "@scandic-hotels/common/dt"
import { Divider } from "@scandic-hotels/design-system/Divider"
import { Typography } from "@scandic-hotels/design-system/Typography"
import { getLang } from "@/i18n/serverContext"
import styles from "./membershipOverviewCard.module.css"
import type { EurobonusMembership } from "@scandic-hotels/trpc/types/user"
import type { IntlShape } from "react-intl"
interface SasBoostStatusProps {
sasMembership: EurobonusMembership
intl: IntlShape
}
export default async function SasBoostStatus({
sasMembership,
intl,
}: SasBoostStatusProps) {
const lang = await getLang()
if (!sasMembership.boostedTierExpires) return null
const sasBoostExpiryText = intl.formatMessage(
{
defaultMessage: "Boosted by SAS until {date}",
},
{
date: dt(sasMembership.boostedTierExpires)
.locale(lang)
.format("D MMM YYYY"),
}
)
return (
<>
<Divider variant="vertical" color="Border/Divider/Accent" />
<Typography variant="Label/xsRegular">
<span className={styles.sasBoostText}>{sasBoostExpiryText}</span>
</Typography>
</>
)
}

View File

@@ -0,0 +1,85 @@
import { MembershipLevelEnum } from "@scandic-hotels/common/constants/membershipLevels"
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 { getEurobonusMembership } from "@scandic-hotels/trpc/routers/user/helpers"
import { membershipLevels } from "@/constants/membershipLevels"
import MembershipLevelIcon from "@/components/Levels/Icon"
import { getIntl } from "@/i18n"
import SasBoostStatus from "./SasBoostStatus"
import styles from "./membershipOverviewCard.module.css"
import type { User } from "@scandic-hotels/trpc/types/user"
interface MembershipOverviewCardProps {
user: User
}
export default async function MembershipOverviewCard({
user,
}: MembershipOverviewCardProps) {
const intl = await getIntl()
if (!user.membership?.membershipLevel) {
return null
}
const pointsToSpendText =
typeof user.membership.currentPoints === "number"
? intl.formatNumber(user.membership.currentPoints)
: intl.formatMessage({ defaultMessage: "N/A" })
const sasMembership = user.loyalty
? getEurobonusMembership(user.loyalty)
: null
return (
<section className={styles.card} aria-labelledby="membership-level">
<header className={styles.membershipHeader}>
{sasMembership && (
<MaterialIcon icon="travel" size={20} color="Icon/Accent" />
)}
<Typography variant="Title/Overline/sm">
<h2 className={styles.levelText} id="membership-level">
{intl.formatMessage(
{
defaultMessage: "LEVEL {level}",
},
{ level: membershipLevels[user.membership.membershipLevel] }
)}
</h2>
</Typography>
{sasMembership && (
<SasBoostStatus sasMembership={sasMembership} intl={intl} />
)}
</header>
<MembershipLevelIcon
level={MembershipLevelEnum[user.membership.membershipLevel]}
height="44"
width="268"
rows={1}
/>
<Divider
className={styles.divider}
color="Border/Divider/Brand/OnPrimary 3/Default"
/>
<Typography variant="Title/Overline/sm">
<h3 className={styles.pointsLabel}>
{intl.formatMessage({
defaultMessage: "POINTS TO SPEND",
})}
</h3>
</Typography>
<Typography variant="Title/lg">
<p className={styles.pointsValue}>{pointsToSpendText}</p>
</Typography>
</section>
)
}

View File

@@ -0,0 +1,41 @@
.card {
background-color: var(--Surface-Brand-Primary-3-Default);
border-radius: var(--Corner-radius-lg);
padding: var(--Space-x3) var(--Space-x2);
position: relative;
min-height: 200px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.membershipHeader {
display: flex;
align-items: center;
gap: var(--Space-x1);
margin-bottom: var(--Space-x05);
}
.levelText,
.sasBoostText,
.pointsLabel {
color: var(--Text-Brand-OnPrimary-3-Heading);
}
.levelText {
text-transform: uppercase;
}
.divider {
margin: var(--Space-x4) 0;
}
.pointsValue {
color: var(--Text-Brand-OnPrimary-3-Accent);
}
@media (min-width: 1367px) {
.card {
padding: var(--Space-x3) var(--Space-x4);
}
}

View File

@@ -2,7 +2,7 @@ import { Divider } from "@scandic-hotels/design-system/Divider"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { env } from "@/env/server"
import { getProfile } from "@/lib/trpc/memoizedRequests"
import { getProfileWithExtendedPartnerData } from "@/lib/trpc/memoizedRequests"
import { TeamMemberCardTrigger } from "@/components/DigitalTeamMemberCard/Trigger"
import DigitalTeamMemberCard from "@/components/MyPages/DigitalTeamMemberCard"
@@ -15,6 +15,7 @@ import { getIntl } from "@/i18n"
import Hero from "./Friend/Hero"
import MembershipNumber from "./Friend/MembershipNumber"
import Friend from "./Friend"
import MembershipOverviewCard from "./MembershipOverviewCard"
import Stats from "./Stats"
import UserBaseInfo from "./UserBaseInfo"
@@ -28,7 +29,7 @@ export default async function Overview({
title,
}: AccountPageComponentProps) {
const intl = await getIntl()
const user = await getProfile()
const user = await getProfileWithExtendedPartnerData()
if (!user || "error" in user) {
return null
}
@@ -58,8 +59,17 @@ export default async function Overview({
</>
</TeamMemberCardTrigger>
</DigitalTeamMemberCard>
{env.ENABLE_NEW_OVERVIEW_SECTION ? <UserBaseInfo user={user} /> : null}
{/*TODO: Replace Hero Section Cards with New ones. */}
{env.ENABLE_NEW_OVERVIEW_SECTION ? (
<>
<UserBaseInfo user={user} />
<div className={styles.membershipCardsContainer}>
<MembershipOverviewCard user={user} />
{/* LevelProgressCard will be added here in the next iteration */}
</div>
</>
) : null}
{/*TODO: Replace hero section with new section above. */}
<Hero color="red">
<Friend membership={user.membership} name={user.name}>
<MembershipNumber color="burgundy" membership={user.membership} />

View File

@@ -1,5 +1,5 @@
.divider {
margin-top: var(--Spacing-x2);
margin-top: var(--Space-x2);
}
.teamMemberCardButton {
@@ -12,17 +12,9 @@
}
}
@media screen and (max-width: 767px) {
.container {
/* Full-width override styling */
left: 50%;
margin-left: -50vw;
margin-right: -50vw;
padding: 0 var(--Spacing-x2);
position: relative;
right: 50%;
width: 100dvw;
}
.membershipCardsContainer {
display: grid;
gap: var(--Space-x2);
}
@media screen and (min-width: 768px) {
@@ -30,3 +22,9 @@
display: none;
}
}
@media screen and (min-width: 1367px) {
.membershipCardsContainer {
grid-template-columns: 1fr 1fr;
}
}

View File

@@ -29,6 +29,10 @@
background-color: var(--UI-Opacity-White-100);
}
.Border-Divider-Accent {
background-color: var(--Border-Divider-Accent);
}
.Border-Divider-Subtle {
background-color: var(--Border-Divider-Subtle);
}
@@ -37,6 +41,10 @@
background-color: var(--Border-Divider-Default);
}
.Border-Divider-Brand-OnPrimary-3-Default {
background: var(--Border-Divider-Brand-OnPrimary-3-Default);
}
.Surface-Brand-Primary-1-OnSurface-Accent-Secondary {
background-color: var(--Surface-Brand-Primary-1-OnSurface-Accent-Secondary);
}

View File

@@ -9,7 +9,10 @@ export const dividerVariants = cva(styles.divider, {
pale: styles.pale,
peach: styles.peach,
white: styles.white,
'Border/Divider/Accent': styles['Border-Divider-Accent'],
'Border/Divider/Default': styles['Border-Divider-Default'],
'Border/Divider/Brand/OnPrimary 3/Default':
styles['Border-Divider-Brand-OnPrimary-3-Default'],
'Border/Divider/Subtle': styles['Border-Divider-Subtle'],
'Surface/Brand/Primary 1/OnSurface/Accent Secondary':
styles['Surface-Brand-Primary-1-OnSurface-Accent-Secondary'],