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 Hero from "../../Overview/Friend/Hero"
|
||||||
import MembershipNumber from "../../Overview/Friend/MembershipNumber"
|
import MembershipNumber from "../../Overview/Friend/MembershipNumber"
|
||||||
import Stats from "../../Overview/Stats"
|
import Stats from "../../Overview/Stats"
|
||||||
|
import PointsToSpendCard from "../PointsToSpendCard"
|
||||||
|
|
||||||
import styles from "./overview.module.css"
|
import styles from "./overview.module.css"
|
||||||
|
|
||||||
@@ -36,23 +37,23 @@ export default async function PointsOverview({
|
|||||||
headingAs={"h3"}
|
headingAs={"h3"}
|
||||||
headingLevel={"h1"}
|
headingLevel={"h1"}
|
||||||
/>
|
/>
|
||||||
{env.ENABLE_NEW_OVERVIEW_SECTION && (
|
{env.ENABLE_NEW_OVERVIEW_SECTION ? (
|
||||||
<div className={styles.membershipCardsContainer}>
|
<div className={styles.membershipCardsContainer}>
|
||||||
{/*TODO: Add PointsToSpendCard */}
|
<PointsToSpendCard user={user} />
|
||||||
<LevelProgressCard
|
<LevelProgressCard
|
||||||
color="Surface/Brand/Primary 1/Default"
|
color="Surface/Brand/Primary 1/Default"
|
||||||
user={user}
|
user={user}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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" />
|
<SectionLink link={link} variant="mobile" />
|
||||||
</SectionContainer>
|
</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`,
|
no: `/${Lang.no}/scandic-friends/partnere`,
|
||||||
sv: `/${Lang.sv}/scandic-friends/partners`,
|
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 KidsMocktailIcon from './Illustrations/KidsMocktail'
|
||||||
import MagicWandIcon from './Illustrations/MagicWand'
|
import MagicWandIcon from './Illustrations/MagicWand'
|
||||||
import MoneyHandIcon from './Illustrations/MoneyHand'
|
import MoneyHandIcon from './Illustrations/MoneyHand'
|
||||||
|
import MoneyHandEllipsisIcon from './Illustrations/MoneyHandEllipsis'
|
||||||
import TrophyIcon from './Illustrations/Trophy'
|
import TrophyIcon from './Illustrations/Trophy'
|
||||||
import VoucherIcon from './Illustrations/Voucher'
|
import VoucherIcon from './Illustrations/Voucher'
|
||||||
|
|
||||||
@@ -25,6 +26,8 @@ export function IllustrationByIconName(iconName: IconName | null) {
|
|||||||
return MagicWandIcon
|
return MagicWandIcon
|
||||||
case IconName.MoneyHand:
|
case IconName.MoneyHand:
|
||||||
return MoneyHandIcon
|
return MoneyHandIcon
|
||||||
|
case IconName.MoneyHandEllipsis:
|
||||||
|
return MoneyHandEllipsisIcon
|
||||||
case IconName.HandKey:
|
case IconName.HandKey:
|
||||||
return HandKeyIcon
|
return HandKeyIcon
|
||||||
case IconName.HotelNight:
|
case IconName.HotelNight:
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -125,6 +125,7 @@ export enum IconName {
|
|||||||
Minibar = 'Minibar',
|
Minibar = 'Minibar',
|
||||||
Minus = 'Minus',
|
Minus = 'Minus',
|
||||||
MoneyHand = 'MoneyHand',
|
MoneyHand = 'MoneyHand',
|
||||||
|
MoneyHandEllipsis = 'MoneyHandEllipsis',
|
||||||
Museum = 'Museum',
|
Museum = 'Museum',
|
||||||
Nature = 'Nature',
|
Nature = 'Nature',
|
||||||
Nightlife = 'Nightlife',
|
Nightlife = 'Nightlife',
|
||||||
|
|||||||
@@ -106,6 +106,7 @@
|
|||||||
"./Icons/MinimizeIcon": "./lib/components/Icons/Customised/UI/Minimize.tsx",
|
"./Icons/MinimizeIcon": "./lib/components/Icons/Customised/UI/Minimize.tsx",
|
||||||
"./Icons/MirrorIcon": "./lib/components/Icons/Customised/Amenities_Facilities/Mirror.tsx",
|
"./Icons/MirrorIcon": "./lib/components/Icons/Customised/Amenities_Facilities/Mirror.tsx",
|
||||||
"./Icons/MoneyHandIcon": "./lib/components/Icons/Illustrations/MoneyHand.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/MovingBedsIcon": "./lib/components/Icons/Customised/Amenities_Facilities/MovingBeds.tsx",
|
||||||
"./Icons/NoBreakfastBuffetIcon": "./lib/components/Icons/Illustrations/NoBreakfastBuffet.tsx",
|
"./Icons/NoBreakfastBuffetIcon": "./lib/components/Icons/Illustrations/NoBreakfastBuffet.tsx",
|
||||||
"./Icons/PalmTreeIcon": "./lib/components/Icons/Nucleo/Experiences/palm-tree-2.tsx",
|
"./Icons/PalmTreeIcon": "./lib/components/Icons/Nucleo/Experiences/palm-tree-2.tsx",
|
||||||
|
|||||||
Reference in New Issue
Block a user