Merged in feat/LOY-336-Points-to-Spend-Card (pull request #2830)
feat(LOY-336): Add PointsToSpendCard * feat(LOY-366): Add PointsToSpendCard * feat(LOY-336): Add Expiring Points Table Sidepeek * fix(LOY-336): Hide old section * fix(LOY-336): description mobile styling * chore(LOY-336): css cleanup Approved-by: Matilda Landström
This commit is contained in:
@@ -12,6 +12,7 @@ import Friend from "../../Overview/Friend"
|
||||
import Hero from "../../Overview/Friend/Hero"
|
||||
import MembershipNumber from "../../Overview/Friend/MembershipNumber"
|
||||
import Stats from "../../Overview/Stats"
|
||||
import PointsToSpendCard from "../PointsToSpendCard"
|
||||
|
||||
import styles from "./overview.module.css"
|
||||
|
||||
@@ -36,23 +37,23 @@ export default async function PointsOverview({
|
||||
headingAs={"h3"}
|
||||
headingLevel={"h1"}
|
||||
/>
|
||||
{env.ENABLE_NEW_OVERVIEW_SECTION && (
|
||||
{env.ENABLE_NEW_OVERVIEW_SECTION ? (
|
||||
<div className={styles.membershipCardsContainer}>
|
||||
{/*TODO: Add PointsToSpendCard */}
|
||||
<PointsToSpendCard user={user} />
|
||||
<LevelProgressCard
|
||||
color="Surface/Brand/Primary 1/Default"
|
||||
user={user}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Hero color="burgundy">
|
||||
<Friend membership={user.membership} name={user.name}>
|
||||
<MembershipNumber color="red" membership={user.membership} />
|
||||
</Friend>
|
||||
<Divider className={styles.divider} color="peach" />
|
||||
<Stats user={user} />
|
||||
</Hero>
|
||||
)}
|
||||
{/*TODO: Once the PointsToSpendCard is added hide the Hero Section under the flag above. */}
|
||||
<Hero color="burgundy">
|
||||
<Friend membership={user.membership} name={user.name}>
|
||||
<MembershipNumber color="red" membership={user.membership} />
|
||||
</Friend>
|
||||
<Divider className={styles.divider} color="peach" />
|
||||
<Stats user={user} />
|
||||
</Hero>
|
||||
<SectionLink link={link} variant="mobile" />
|
||||
</SectionContainer>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client"
|
||||
|
||||
import { DialogTrigger } from "react-aria-components"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { Button } from "@scandic-hotels/design-system/Button"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import SidePeekSelfControlled from "@scandic-hotels/design-system/SidePeekSelfControlled"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import ExpiringPointsTable from "../ExpiringPoints/ExpiringPointsTable"
|
||||
|
||||
import styles from "./PointsToSpendCard.module.css"
|
||||
|
||||
interface ExpiringPointsSeeAllButtonProps {
|
||||
expiringPoints: number
|
||||
expiryDate: string
|
||||
}
|
||||
|
||||
export default function ExpiringPointsSeeAllButton({
|
||||
expiringPoints,
|
||||
expiryDate,
|
||||
}: ExpiringPointsSeeAllButtonProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button variant="Text" size="Medium" typography="Body/Paragraph/mdBold">
|
||||
{intl.formatMessage({ defaultMessage: "See all" })}
|
||||
<MaterialIcon icon="chevron_right" color="CurrentColor" />
|
||||
</Button>
|
||||
<SidePeekSelfControlled
|
||||
title={intl.formatMessage({ defaultMessage: "Expiring Points" })}
|
||||
>
|
||||
<div className={styles.sidePeekContent}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p>
|
||||
{intl.formatMessage({
|
||||
defaultMessage:
|
||||
"Points expire three years after they are earned, on the last day of that month. Expiring points do not affect your level.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
{/* TODO: The table will be rebuilt as part of the My Pages Optimisations 3 Epic. */}
|
||||
<ExpiringPointsTable
|
||||
points={expiringPoints}
|
||||
expirationDate={expiryDate}
|
||||
/>
|
||||
</div>
|
||||
</SidePeekSelfControlled>
|
||||
</DialogTrigger>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
.card {
|
||||
background: var(--Surface-Brand-Primary-1-Default);
|
||||
border-radius: var(--Corner-radius-lg);
|
||||
padding: var(--Space-x3) var(--Space-x2);
|
||||
position: relative;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x3);
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: var(--Space-x3);
|
||||
}
|
||||
|
||||
.textContent {
|
||||
display: grid;
|
||||
gap: var(--Space-x1);
|
||||
align-self: center;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--Text-Brand-OnPrimary-1-Heading);
|
||||
}
|
||||
|
||||
.pointsContainer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--Space-x1);
|
||||
}
|
||||
|
||||
.pointsValue {
|
||||
color: var(--Text-Accent-Primary);
|
||||
}
|
||||
|
||||
.pointsLabel {
|
||||
color: var(--Text-Brand-OnPrimary-1-Heading);
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: auto;
|
||||
padding: var(--Space-x2);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
background: var(--Surface-Primary-Default);
|
||||
}
|
||||
|
||||
.descriptionText {
|
||||
color: var(--Text-Brand-OnPrimary-1-Body);
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.expiringPointsCard {
|
||||
display: flex;
|
||||
padding: var(--Space-x2);
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
gap: var(--Space-x1);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
background: var(--Surface-Primary-Default);
|
||||
}
|
||||
|
||||
.expiryDate {
|
||||
color: var(--Text-Heading);
|
||||
}
|
||||
|
||||
.pointsRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.expiringPoints {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidePeekContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Space-x2);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.content {
|
||||
grid-template-columns: auto 1fr;
|
||||
place-items: start;
|
||||
}
|
||||
|
||||
.textContent {
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.expiringPointsCard {
|
||||
padding: var(--Space-x2) var(--Space-x4);
|
||||
}
|
||||
|
||||
.description {
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 1367px) {
|
||||
.card {
|
||||
padding: var(--Space-x3) var(--Space-x4);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import ButtonLink from "@scandic-hotels/design-system/ButtonLink"
|
||||
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||
import MoneyHandEllipsisIcon from "@scandic-hotels/design-system/Icons/MoneyHandEllipsisIcon"
|
||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||
|
||||
import { spendPoints } from "@/constants/webHrefs"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
import { getLang } from "@/i18n/serverContext"
|
||||
|
||||
import ExpiringPointsSeeAllButton from "./ExpiringPointsSeeAllButton"
|
||||
import { getExpiryLabel } from "./utils"
|
||||
|
||||
import styles from "./PointsToSpendCard.module.css"
|
||||
|
||||
import type { User } from "@scandic-hotels/trpc/types/user"
|
||||
|
||||
interface PointsToSpendCardProps {
|
||||
user: User
|
||||
}
|
||||
|
||||
export default async function PointsToSpendCard({
|
||||
user,
|
||||
}: PointsToSpendCardProps) {
|
||||
const intl = await getIntl()
|
||||
const lang = await getLang()
|
||||
|
||||
if (!user.membership) {
|
||||
return null
|
||||
}
|
||||
|
||||
const spendablePoints = user.membership.currentPoints
|
||||
const hasPointsToSpend = spendablePoints > 0
|
||||
|
||||
const expiringPoints = user.membership.pointsToExpire
|
||||
const expiryDate = user.membership.pointsExpiryDate
|
||||
const expiryDateText = getExpiryLabel(expiryDate, intl, lang)
|
||||
|
||||
return (
|
||||
<section
|
||||
className={styles.card}
|
||||
aria-labelledby="points-to-spend-card-title"
|
||||
>
|
||||
<div className={styles.content}>
|
||||
<MoneyHandEllipsisIcon
|
||||
width="140"
|
||||
height="128"
|
||||
className={styles.icon}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className={styles.textContent}>
|
||||
<Typography variant="Title/xs">
|
||||
<h2 id="points-to-spend-card-title" className={styles.title}>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "Points to spend",
|
||||
})}
|
||||
</h2>
|
||||
</Typography>
|
||||
<div className={styles.pointsContainer}>
|
||||
<Typography variant="Title/lg">
|
||||
<span className={styles.pointsValue}>
|
||||
{intl.formatNumber(spendablePoints)}
|
||||
</span>
|
||||
</Typography>
|
||||
{!hasPointsToSpend && (
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<span className={styles.pointsLabel}>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "POINTS",
|
||||
})}
|
||||
</span>
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
{hasPointsToSpend && (
|
||||
<ButtonLink href={spendPoints[lang]} target="_blank" variant="Text">
|
||||
{intl.formatMessage({
|
||||
defaultMessage: "How to spend points",
|
||||
})}
|
||||
<MaterialIcon
|
||||
icon="chevron_right"
|
||||
color="CurrentColor"
|
||||
size={24}
|
||||
/>
|
||||
</ButtonLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{!hasPointsToSpend && (
|
||||
<div className={styles.description}>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<p className={styles.descriptionText}>
|
||||
{intl.formatMessage({
|
||||
defaultMessage:
|
||||
"Earn points by staying at Scandic. Turn your points into free nights and memorable experiences.",
|
||||
})}
|
||||
</p>
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
{expiringPoints && expiryDate && (
|
||||
<div className={styles.expiringPointsCard}>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<p className={styles.expiryDate}>{expiryDateText}</p>
|
||||
</Typography>
|
||||
<div className={styles.pointsRow}>
|
||||
<Typography variant="Body/Lead text">
|
||||
<p className={styles.expiringPoints}>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "{expiringPoints} points expiring",
|
||||
},
|
||||
{
|
||||
expiringPoints: (
|
||||
<Typography
|
||||
key={`expiring-points-${expiringPoints}`}
|
||||
variant="Title/Subtitle/md"
|
||||
>
|
||||
<strong>{intl.formatNumber(expiringPoints)}</strong>
|
||||
</Typography>
|
||||
),
|
||||
}
|
||||
)}
|
||||
</p>
|
||||
</Typography>
|
||||
<ExpiringPointsSeeAllButton
|
||||
expiringPoints={expiringPoints}
|
||||
expiryDate={expiryDate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { dt } from "@scandic-hotels/common/dt"
|
||||
|
||||
import type { IntlShape } from "react-intl"
|
||||
|
||||
export const DAYS_UNTIL_EXPIRY_WARNING = 30
|
||||
|
||||
/**
|
||||
* Get expiry label for points based on the expiry date.
|
||||
* @returns The formatted expiry date text or empty string if no expiry date
|
||||
*/
|
||||
export function getExpiryLabel(
|
||||
pointsExpiryDate: string | undefined,
|
||||
intl: IntlShape,
|
||||
lang: string
|
||||
): string {
|
||||
if (!pointsExpiryDate) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const now = dt()
|
||||
const expiryDate = dt(pointsExpiryDate).locale(lang)
|
||||
const daysUntilExpiry = expiryDate.diff(now, "days")
|
||||
|
||||
if (daysUntilExpiry <= DAYS_UNTIL_EXPIRY_WARNING) {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "In {days} days",
|
||||
},
|
||||
{
|
||||
days: daysUntilExpiry,
|
||||
}
|
||||
)
|
||||
} else {
|
||||
return intl.formatMessage(
|
||||
{
|
||||
defaultMessage: "on {expiryDate}",
|
||||
},
|
||||
{
|
||||
expiryDate: expiryDate.format("DD MMM YYYY"),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -28,3 +28,12 @@ export const partners: LangRoute = {
|
||||
no: `/${Lang.no}/scandic-friends/partnere`,
|
||||
sv: `/${Lang.sv}/scandic-friends/partners`,
|
||||
}
|
||||
|
||||
export const spendPoints: LangRoute = {
|
||||
da: `/${Lang.da}/scandic-friends/brug-point`,
|
||||
de: `/${Lang.de}/scandic-friends/ausgeben`,
|
||||
en: `/${Lang.en}/scandic-friends/spend-points`,
|
||||
fi: `/${Lang.fi}/scandic-friends/hyodynna`,
|
||||
no: `/${Lang.no}/scandic-friends/bruk-poeng`,
|
||||
sv: `/${Lang.sv}/scandic-friends/anvand-poang`,
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import KidsIcon from './Illustrations/Kids'
|
||||
import KidsMocktailIcon from './Illustrations/KidsMocktail'
|
||||
import MagicWandIcon from './Illustrations/MagicWand'
|
||||
import MoneyHandIcon from './Illustrations/MoneyHand'
|
||||
import MoneyHandEllipsisIcon from './Illustrations/MoneyHandEllipsis'
|
||||
import TrophyIcon from './Illustrations/Trophy'
|
||||
import VoucherIcon from './Illustrations/Voucher'
|
||||
|
||||
@@ -25,6 +26,8 @@ export function IllustrationByIconName(iconName: IconName | null) {
|
||||
return MagicWandIcon
|
||||
case IconName.MoneyHand:
|
||||
return MoneyHandIcon
|
||||
case IconName.MoneyHandEllipsis:
|
||||
return MoneyHandEllipsisIcon
|
||||
case IconName.HandKey:
|
||||
return HandKeyIcon
|
||||
case IconName.HotelNight:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -125,6 +125,7 @@ export enum IconName {
|
||||
Minibar = 'Minibar',
|
||||
Minus = 'Minus',
|
||||
MoneyHand = 'MoneyHand',
|
||||
MoneyHandEllipsis = 'MoneyHandEllipsis',
|
||||
Museum = 'Museum',
|
||||
Nature = 'Nature',
|
||||
Nightlife = 'Nightlife',
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
"./Icons/MinimizeIcon": "./lib/components/Icons/Customised/UI/Minimize.tsx",
|
||||
"./Icons/MirrorIcon": "./lib/components/Icons/Customised/Amenities_Facilities/Mirror.tsx",
|
||||
"./Icons/MoneyHandIcon": "./lib/components/Icons/Illustrations/MoneyHand.tsx",
|
||||
"./Icons/MoneyHandEllipsisIcon": "./lib/components/Icons/Illustrations/MoneyHandEllipsis.tsx",
|
||||
"./Icons/MovingBedsIcon": "./lib/components/Icons/Customised/Amenities_Facilities/MovingBeds.tsx",
|
||||
"./Icons/NoBreakfastBuffetIcon": "./lib/components/Icons/Illustrations/NoBreakfastBuffet.tsx",
|
||||
"./Icons/PalmTreeIcon": "./lib/components/Icons/Nucleo/Experiences/palm-tree-2.tsx",
|
||||
|
||||
Reference in New Issue
Block a user