diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/Overview/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/Overview/index.tsx index 3b6d127d6..1e0ac081a 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Points/Overview/index.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/Overview/index.tsx @@ -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 ? (
- {/*TODO: Add PointsToSpendCard */} +
+ ) : ( + + + + + + + )} - {/*TODO: Once the PointsToSpendCard is added hide the Hero Section under the flag above. */} - - - - - - - ) diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/ExpiringPointsSeeAllButton.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/ExpiringPointsSeeAllButton.tsx new file mode 100644 index 000000000..c51eed574 --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/ExpiringPointsSeeAllButton.tsx @@ -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 ( + + + +
+ +

+ {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.", + })} +

+
+ {/* TODO: The table will be rebuilt as part of the My Pages Optimisations 3 Epic. */} + +
+
+
+ ) +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/PointsToSpendCard.module.css b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/PointsToSpendCard.module.css new file mode 100644 index 000000000..52adf7422 --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/PointsToSpendCard.module.css @@ -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); + } +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/index.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/index.tsx new file mode 100644 index 000000000..f917f6939 --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/index.tsx @@ -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 ( +
+
+
+ {!hasPointsToSpend && ( +
+ +

+ {intl.formatMessage({ + defaultMessage: + "Earn points by staying at Scandic. Turn your points into free nights and memorable experiences.", + })} +

+
+
+ )} + {expiringPoints && expiryDate && ( +
+ +

{expiryDateText}

+
+
+ +

+ {intl.formatMessage( + { + defaultMessage: "{expiringPoints} points expiring", + }, + { + expiringPoints: ( + + {intl.formatNumber(expiringPoints)} + + ), + } + )} +

+
+ +
+
+ )} +
+ ) +} diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/utils.ts b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/utils.ts new file mode 100644 index 000000000..146b8a94c --- /dev/null +++ b/apps/scandic-web/components/Blocks/DynamicContent/Points/PointsToSpendCard/utils.ts @@ -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"), + } + ) + } +} diff --git a/apps/scandic-web/constants/webHrefs.ts b/apps/scandic-web/constants/webHrefs.ts index 1185f0563..06610db53 100644 --- a/apps/scandic-web/constants/webHrefs.ts +++ b/apps/scandic-web/constants/webHrefs.ts @@ -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`, +} diff --git a/packages/design-system/lib/components/Icons/IllustrationByIconName.ts b/packages/design-system/lib/components/Icons/IllustrationByIconName.ts index aa48e03ba..fbe7ccd0c 100644 --- a/packages/design-system/lib/components/Icons/IllustrationByIconName.ts +++ b/packages/design-system/lib/components/Icons/IllustrationByIconName.ts @@ -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: diff --git a/packages/design-system/lib/components/Icons/Illustrations/MoneyHandEllipsis.tsx b/packages/design-system/lib/components/Icons/Illustrations/MoneyHandEllipsis.tsx new file mode 100644 index 000000000..956081420 --- /dev/null +++ b/packages/design-system/lib/components/Icons/Illustrations/MoneyHandEllipsis.tsx @@ -0,0 +1,22 @@ +import type { IllustrationProps } from '../icon' + +export default function MoneyHandEllipsisIcon(props: IllustrationProps) { + return ( + + + + + + ) +} diff --git a/packages/design-system/lib/components/Icons/iconName.ts b/packages/design-system/lib/components/Icons/iconName.ts index 5227efa2d..0dc4e11a7 100644 --- a/packages/design-system/lib/components/Icons/iconName.ts +++ b/packages/design-system/lib/components/Icons/iconName.ts @@ -125,6 +125,7 @@ export enum IconName { Minibar = 'Minibar', Minus = 'Minus', MoneyHand = 'MoneyHand', + MoneyHandEllipsis = 'MoneyHandEllipsis', Museum = 'Museum', Nature = 'Nature', Nightlife = 'Nightlife', diff --git a/packages/design-system/package.json b/packages/design-system/package.json index c5a589941..46c1e86fc 100644 --- a/packages/design-system/package.json +++ b/packages/design-system/package.json @@ -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",