From b45157818f72b1abee46e88edb04edd5e5aa835b Mon Sep 17 00:00:00 2001 From: Christian Andolf Date: Mon, 10 Mar 2025 13:18:54 +0100 Subject: [PATCH] feat(LOY-154): add expiration date to rewards --- .../Rewards/CurrentRewards/Client.tsx | 64 +++++++++++-------- .../components/MyPages/Surprises/Slide.tsx | 40 ++---------- .../MyPages/Surprises/surprises.module.css | 10 --- .../ExpirationDate/expirationDate.module.css | 9 +++ .../Rewards/ExpirationDate/index.tsx | 36 +++++++++++ .../routers/contentstack/reward/output.ts | 11 +++- .../routers/contentstack/reward/query.ts | 2 + apps/scandic-web/utils/rewards.ts | 53 +++++++++++---- 8 files changed, 139 insertions(+), 86 deletions(-) create mode 100644 apps/scandic-web/components/Rewards/ExpirationDate/expirationDate.module.css create mode 100644 apps/scandic-web/components/Rewards/ExpirationDate/index.tsx diff --git a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx index c264f34fd..9428a5cba 100644 --- a/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx +++ b/apps/scandic-web/components/Blocks/DynamicContent/Rewards/CurrentRewards/Client.tsx @@ -7,9 +7,11 @@ import { trpc } from "@/lib/trpc/client" import { RewardIcon } from "@/components/Blocks/DynamicContent/Rewards/RewardIcon" import ScriptedRewardText from "@/components/Blocks/DynamicContent/Rewards/ScriptedRewardText" import Pagination from "@/components/MyPages/Pagination" +import ExpirationDate from "@/components/Rewards/ExpirationDate" import Grids from "@/components/TempDesignSystem/Grids" import Title from "@/components/TempDesignSystem/Text/Title" import useLang from "@/hooks/useLang" +import { getEarliestExpirationDate } from "@/utils/rewards" import Redeem from "../Redeem" @@ -67,32 +69,44 @@ export default function ClientCurrentRewards({ return (
- {currentRewards.map((reward, idx) => ( -
-
- - {showRedeem && ( - - )} - - {reward.label} - -
- {showRedeem && "redeem_description" in reward && ( -
- + {currentRewards.map((reward, idx) => { + const earliestExpirationDate = + "coupons" in reward + ? getEarliestExpirationDate(reward.coupons) + : null + + return ( +
+
+ + {showRedeem && ( + + )} + + {reward.label} + + + {earliestExpirationDate ? ( + + ) : null}
- )} -
- ))} + + {showRedeem && "redeem_description" in reward && ( +
+ +
+ )} +
+ ) + })}
{totalPages > 1 && ( expiresAt) - .filter((expiresAt): expiresAt is string => !!expiresAt) - .reduce((earliestDate: Dayjs | null, expiresAt) => { - const expiresAtDate = dt(expiresAt) - if (!earliestDate) { - return expiresAtDate - } - - return earliestDate.isBefore(expiresAtDate) ? earliestDate : expiresAtDate - }, null) + const earliestExpirationDate = getEarliestExpirationDate(surprise.coupons) return ( {surprise.description} {earliestExpirationDate ? ( -
- - {intl.formatMessage( - { id: "Valid through {expirationDate}" }, - { - expirationDate: dt(earliestExpirationDate) - .locale(lang) - .format("D MMM YYYY"), - } - )} - -
+ ) : null}
) diff --git a/apps/scandic-web/components/MyPages/Surprises/surprises.module.css b/apps/scandic-web/components/MyPages/Surprises/surprises.module.css index 99f290ceb..8b5d24e50 100644 --- a/apps/scandic-web/components/MyPages/Surprises/surprises.module.css +++ b/apps/scandic-web/components/MyPages/Surprises/surprises.module.css @@ -125,16 +125,6 @@ transform: rotate(180deg); } -.badge { - padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); - display: flex; - flex-direction: column; - align-items: center; - gap: var(--Spacing-x-half); - background-color: var(--Base-Surface-Secondary-light-Normal); - border-radius: var(--Corner-radius-Small); -} - .close { background: none; border: none; diff --git a/apps/scandic-web/components/Rewards/ExpirationDate/expirationDate.module.css b/apps/scandic-web/components/Rewards/ExpirationDate/expirationDate.module.css new file mode 100644 index 000000000..6587d7645 --- /dev/null +++ b/apps/scandic-web/components/Rewards/ExpirationDate/expirationDate.module.css @@ -0,0 +1,9 @@ +.badge { + padding: var(--Spacing-x1) var(--Spacing-x-one-and-half); + display: flex; + flex-direction: column; + align-items: center; + gap: var(--Spacing-x-half); + background-color: var(--Base-Surface-Secondary-light-Normal); + border-radius: var(--Corner-radius-Small); +} diff --git a/apps/scandic-web/components/Rewards/ExpirationDate/index.tsx b/apps/scandic-web/components/Rewards/ExpirationDate/index.tsx new file mode 100644 index 000000000..48f81a3be --- /dev/null +++ b/apps/scandic-web/components/Rewards/ExpirationDate/index.tsx @@ -0,0 +1,36 @@ +"use client" + +import { useIntl } from "react-intl" + +import { dt } from "@/lib/dt" + +import Body from "@/components/TempDesignSystem/Text/Body" +import useLang from "@/hooks/useLang" + +import styles from "./expirationDate.module.css" + +import type { Dayjs } from "dayjs" + +export default function ExpirationDate({ + expirationDate, +}: { + expirationDate: Dayjs | string +}) { + const intl = useIntl() + const lang = useLang() + + return ( +
+ + {intl.formatMessage( + { id: "Valid through {expirationDate}" }, + { + expirationDate: dt(expirationDate) + .locale(lang) + .format("D MMM YYYY"), + } + )} + +
+ ) +} diff --git a/apps/scandic-web/server/routers/contentstack/reward/output.ts b/apps/scandic-web/server/routers/contentstack/reward/output.ts index 3d6a0b20e..6bc19fbe2 100644 --- a/apps/scandic-web/server/routers/contentstack/reward/output.ts +++ b/apps/scandic-web/server/routers/contentstack/reward/output.ts @@ -212,6 +212,7 @@ export type Reward = CMSReward & { rewardTierLevel: string | undefined operaRewardId: string couponCode: string | undefined + coupons: Coupon[] } export type RewardWithRedeem = CMSRewardWithRedeem & { @@ -221,10 +222,18 @@ export type RewardWithRedeem = CMSRewardWithRedeem & { rewardTierLevel: string | undefined operaRewardId: string couponCode: string | undefined + coupons: Coupon[] +} + +export interface Coupon { + couponCode?: string + expiresAt?: string + unwrapped: boolean + state: "claimed" | "redeemed" | "viewed" } export interface Surprise extends Omit { - coupons: { couponCode?: string | undefined; expiresAt?: string }[] + coupons: Coupon[] } // New endpoint related types and schemas. diff --git a/apps/scandic-web/server/routers/contentstack/reward/query.ts b/apps/scandic-web/server/routers/contentstack/reward/query.ts index b8f8b9a5f..21d7be257 100644 --- a/apps/scandic-web/server/routers/contentstack/reward/query.ts +++ b/apps/scandic-web/server/routers/contentstack/reward/query.ts @@ -287,6 +287,8 @@ export const rewardQueryRouter = router({ ? apiReward.operaRewardId : "", couponCode: firstRedeemableCouponToExpire, + coupons: + apiReward && "coupon" in apiReward ? apiReward.coupon || [] : [], } }) diff --git a/apps/scandic-web/utils/rewards.ts b/apps/scandic-web/utils/rewards.ts index 8bde3f671..557cc17b4 100644 --- a/apps/scandic-web/utils/rewards.ts +++ b/apps/scandic-web/utils/rewards.ts @@ -3,49 +3,74 @@ import { REWARD_IDS, REWARD_TYPES, } from "@/constants/rewards" +import { dt } from "@/lib/dt" + +import type { Dayjs } from "dayjs" import type { RestaurantRewardId, RewardId, RewardType, } from "@/types/components/myPages/rewards" -import type { RewardWithRedeem } from "@/server/routers/contentstack/reward/output" +import type { + Coupon, + RewardWithRedeem, +} from "@/server/routers/contentstack/reward/output" -export function isValidRewardId(id: string): id is RewardId { +export { + getEarliestExpirationDate, + getRewardType, + isOnSiteTierReward, + isRestaurantOnSiteTierReward, + isRestaurantReward, + isTierType, + isValidRewardId, + redeemLocationIsOnSite, +} + +function isValidRewardId(id: string): id is RewardId { return Object.values(REWARD_IDS).includes(id) } -export function isRestaurantReward( - rewardId: string -): rewardId is RestaurantRewardId { +function isRestaurantReward(rewardId: string): rewardId is RestaurantRewardId { return RESTAURANT_REWARD_IDS.some((id) => id === rewardId) } -export function redeemLocationIsOnSite( +function redeemLocationIsOnSite( location: RewardWithRedeem["redeemLocation"] ): location is "On-site" { return location === "On-site" } -export function isTierType( - type: RewardWithRedeem["rewardType"] -): type is "Tier" { +function isTierType(type: RewardWithRedeem["rewardType"]): type is "Tier" { return type === "Tier" } -export function isOnSiteTierReward(reward: RewardWithRedeem): boolean { +function isOnSiteTierReward(reward: RewardWithRedeem): boolean { return ( redeemLocationIsOnSite(reward.redeemLocation) && isTierType(reward.rewardType) ) } -export function isRestaurantOnSiteTierReward( - reward: RewardWithRedeem -): boolean { +function isRestaurantOnSiteTierReward(reward: RewardWithRedeem): boolean { return isOnSiteTierReward(reward) && isRestaurantReward(reward.reward_id) } -export function getRewardType(type?: string): RewardType | null { +function getRewardType(type?: string): RewardType | null { return REWARD_TYPES.find((t) => t === type) ?? null } + +function getEarliestExpirationDate(coupons: Coupon[]) { + return coupons + .map(({ expiresAt }) => expiresAt) + .filter((expiresAt): expiresAt is string => !!expiresAt) + .reduce((earliestDate: Dayjs | null, expiresAt) => { + const expiresAtDate = dt(expiresAt) + if (!earliestDate) { + return expiresAtDate + } + + return earliestDate.isBefore(expiresAtDate) ? earliestDate : expiresAtDate + }, null) +}