Merged in feat/LOY-63-redeem-campaign (pull request #1122)
feat(LOY-63): redeem campaign Approved-by: Chuma Mcphoy (We Ahead)
This commit is contained in:
@@ -7,7 +7,7 @@ import Pagination from "@/components/MyPages/Pagination"
|
|||||||
import Grids from "@/components/TempDesignSystem/Grids"
|
import Grids from "@/components/TempDesignSystem/Grids"
|
||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
|
||||||
import Redeem from "./Redeem"
|
import Redeem from "../Redeem"
|
||||||
|
|
||||||
import styles from "./current.module.css"
|
import styles from "./current.module.css"
|
||||||
|
|
||||||
|
|||||||
@@ -1,262 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { motion } from "framer-motion"
|
|
||||||
import { useState } from "react"
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogTrigger,
|
|
||||||
Modal,
|
|
||||||
ModalOverlay,
|
|
||||||
} from "react-aria-components"
|
|
||||||
import { useIntl } from "react-intl"
|
|
||||||
|
|
||||||
import { trpc } from "@/lib/trpc/client"
|
|
||||||
|
|
||||||
import Countdown from "@/components/Countdown"
|
|
||||||
import { CheckCircleIcon, CloseLargeIcon } from "@/components/Icons"
|
|
||||||
import Button from "@/components/TempDesignSystem/Button"
|
|
||||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
|
||||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
|
||||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
|
||||||
import { isRestaurantOnSiteTierReward } from "@/utils/rewards"
|
|
||||||
|
|
||||||
import { RewardIcon } from "../RewardIcon"
|
|
||||||
|
|
||||||
import styles from "./current.module.css"
|
|
||||||
|
|
||||||
import type {
|
|
||||||
RedeemModalState,
|
|
||||||
RedeemProps,
|
|
||||||
RedeemStep,
|
|
||||||
} from "@/types/components/myPages/myPage/accountPage"
|
|
||||||
import type { Reward } from "@/server/routers/contentstack/reward/output"
|
|
||||||
|
|
||||||
const MotionOverlay = motion(ModalOverlay)
|
|
||||||
const MotionModal = motion(Modal)
|
|
||||||
|
|
||||||
export default function Redeem({ reward, membershipNumber }: RedeemProps) {
|
|
||||||
const [animation, setAnimation] = useState<RedeemModalState>("unmounted")
|
|
||||||
const intl = useIntl()
|
|
||||||
const update = trpc.contentstack.rewards.redeem.useMutation()
|
|
||||||
const [redeemStep, setRedeemStep] = useState<RedeemStep>("initial")
|
|
||||||
|
|
||||||
function onProceed() {
|
|
||||||
if (reward.id) {
|
|
||||||
update.mutate(
|
|
||||||
{ rewardId: reward.id },
|
|
||||||
{
|
|
||||||
onSuccess() {
|
|
||||||
setRedeemStep("redeemed")
|
|
||||||
},
|
|
||||||
onError(error) {
|
|
||||||
console.error("Failed to redeem", error)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function modalStateHandler(newAnimationState: RedeemModalState) {
|
|
||||||
setAnimation((currentAnimationState) =>
|
|
||||||
newAnimationState === "hidden" && currentAnimationState === "hidden"
|
|
||||||
? "unmounted"
|
|
||||||
: currentAnimationState
|
|
||||||
)
|
|
||||||
if (newAnimationState === "unmounted") {
|
|
||||||
setRedeemStep("initial")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogTrigger
|
|
||||||
onOpenChange={(isOpen) => setAnimation(isOpen ? "visible" : "hidden")}
|
|
||||||
>
|
|
||||||
<Button intent="primary" fullWidth>
|
|
||||||
{intl.formatMessage({ id: "Open" })}
|
|
||||||
</Button>
|
|
||||||
<MotionOverlay
|
|
||||||
className={styles.overlay}
|
|
||||||
isExiting={animation === "hidden"}
|
|
||||||
onAnimationComplete={modalStateHandler}
|
|
||||||
variants={variants.fade}
|
|
||||||
initial="hidden"
|
|
||||||
animate={animation}
|
|
||||||
>
|
|
||||||
<MotionModal
|
|
||||||
className={styles.modal}
|
|
||||||
variants={variants.slideInOut}
|
|
||||||
initial="hidden"
|
|
||||||
animate={animation}
|
|
||||||
>
|
|
||||||
<Dialog className={styles.dialog} aria-label={reward.label}>
|
|
||||||
{({ close }) => (
|
|
||||||
<>
|
|
||||||
<header className={styles.modalHeader}>
|
|
||||||
<button
|
|
||||||
onClick={close}
|
|
||||||
type="button"
|
|
||||||
className={styles.modalClose}
|
|
||||||
>
|
|
||||||
<CloseLargeIcon />
|
|
||||||
</button>
|
|
||||||
</header>
|
|
||||||
<div className={styles.modalContent}>
|
|
||||||
{redeemStep === "redeemed" && (
|
|
||||||
<ConfirmationBadge reward={reward} />
|
|
||||||
)}
|
|
||||||
<RewardIcon rewardId={reward.reward_id} />
|
|
||||||
<Title level="h3" textAlign="center" textTransform="regular">
|
|
||||||
{reward.label}
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
{redeemStep === "initial" && (
|
|
||||||
<Body textAlign="center">{reward.description}</Body>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{redeemStep === "confirmation" &&
|
|
||||||
"redeem_description" in reward && (
|
|
||||||
<Body textAlign="center">
|
|
||||||
{reward.redeem_description}
|
|
||||||
</Body>
|
|
||||||
)}
|
|
||||||
{redeemStep === "redeemed" &&
|
|
||||||
isRestaurantOnSiteTierReward(reward) &&
|
|
||||||
membershipNumber && (
|
|
||||||
<MembershipNumberBadge
|
|
||||||
membershipNumber={membershipNumber}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{redeemStep === "initial" && (
|
|
||||||
<footer className={styles.modalFooter}>
|
|
||||||
<Button
|
|
||||||
onClick={() => setRedeemStep("confirmation")}
|
|
||||||
intent="primary"
|
|
||||||
theme="base"
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "Redeem benefit" })}
|
|
||||||
</Button>
|
|
||||||
</footer>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{redeemStep === "confirmation" && (
|
|
||||||
<footer className={styles.modalFooter}>
|
|
||||||
<Button
|
|
||||||
onClick={onProceed}
|
|
||||||
disabled={update.isPending}
|
|
||||||
intent="primary"
|
|
||||||
theme="base"
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "Yes, redeem" })}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={close} intent="secondary" theme="base">
|
|
||||||
{intl.formatMessage({ id: "Go back" })}
|
|
||||||
</Button>
|
|
||||||
</footer>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Dialog>
|
|
||||||
</MotionModal>
|
|
||||||
</MotionOverlay>
|
|
||||||
</DialogTrigger>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const variants = {
|
|
||||||
fade: {
|
|
||||||
hidden: {
|
|
||||||
opacity: 0,
|
|
||||||
transition: { duration: 0.4, ease: "easeInOut" },
|
|
||||||
},
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
transition: { duration: 0.4, ease: "easeInOut" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
slideInOut: {
|
|
||||||
hidden: {
|
|
||||||
opacity: 0,
|
|
||||||
y: 32,
|
|
||||||
transition: { duration: 0.4, ease: "easeInOut" },
|
|
||||||
},
|
|
||||||
visible: {
|
|
||||||
opacity: 1,
|
|
||||||
y: 0,
|
|
||||||
transition: { duration: 0.4, ease: "easeInOut" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConfirmationBadge({ reward }: { reward: Reward }) {
|
|
||||||
return (
|
|
||||||
<div className={styles.badge}>
|
|
||||||
{isRestaurantOnSiteTierReward(reward) ? (
|
|
||||||
<ActiveRedeemedBadge />
|
|
||||||
) : (
|
|
||||||
<TimedRedeemedBadge />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ActiveRedeemedBadge() {
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.redeemed}>
|
|
||||||
<motion.div
|
|
||||||
animate={{
|
|
||||||
opacity: [1, 0.4, 1],
|
|
||||||
}}
|
|
||||||
transition={{
|
|
||||||
duration: 2,
|
|
||||||
repeat: Infinity,
|
|
||||||
ease: "easeInOut",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckCircleIcon color="uiSemanticSuccess" />
|
|
||||||
</motion.div>
|
|
||||||
<Caption>{intl.formatMessage({ id: "Active" })}</Caption>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function TimedRedeemedBadge() {
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={styles.redeemed}>
|
|
||||||
<CheckCircleIcon color="uiSemanticSuccess" />
|
|
||||||
<Caption>
|
|
||||||
{intl.formatMessage({
|
|
||||||
id: "Redeemed & valid through:",
|
|
||||||
})}
|
|
||||||
</Caption>
|
|
||||||
</div>
|
|
||||||
<Countdown />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function MembershipNumberBadge({
|
|
||||||
membershipNumber,
|
|
||||||
}: {
|
|
||||||
membershipNumber: string
|
|
||||||
}) {
|
|
||||||
const intl = useIntl()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.membershipNumberBadge}>
|
|
||||||
<Caption
|
|
||||||
textTransform="uppercase"
|
|
||||||
textAlign="center"
|
|
||||||
color="uiTextHighContrast"
|
|
||||||
>
|
|
||||||
{intl.formatMessage({ id: "Membership ID:" })} {membershipNumber}
|
|
||||||
</Caption>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -27,119 +27,3 @@
|
|||||||
.btnContainer {
|
.btnContainer {
|
||||||
padding: 0 var(--Spacing-x3) var(--Spacing-x3);
|
padding: 0 var(--Spacing-x3) var(--Spacing-x3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
|
||||||
border-radius: var(--Small, 4px);
|
|
||||||
border: 1px solid var(--Base-Border-Subtle);
|
|
||||||
display: flex;
|
|
||||||
padding: var(--Spacing-x1) var(--Spacing-x2);
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.redeemed {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--Spacing-x-half);
|
|
||||||
align-self: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overlay {
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
height: var(--visual-viewport-height);
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100vw;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
.overlay {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal {
|
|
||||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
|
||||||
border-radius: var(--Corner-radius-Medium);
|
|
||||||
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
|
||||||
width: 100%;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 101;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (min-width: 768px) {
|
|
||||||
.modal {
|
|
||||||
left: auto;
|
|
||||||
bottom: auto;
|
|
||||||
width: 400px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-bottom: var(--Spacing-x3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modalHeader {
|
|
||||||
--button-height: 32px;
|
|
||||||
box-sizing: content-box;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
height: var(--button-height);
|
|
||||||
position: relative;
|
|
||||||
justify-content: center;
|
|
||||||
padding: var(--Spacing-x3) var(--Spacing-x2) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modalContent {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--Spacing-x2);
|
|
||||||
padding: 0 var(--Spacing-x3) var(--Spacing-x3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modalFooter {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 0 var(--Spacing-x3) var(--Spacing-x1);
|
|
||||||
gap: var(--Spacing-x-one-and-half);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modalFooter > button {
|
|
||||||
flex: 1 0 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modalClose {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
position: absolute;
|
|
||||||
right: var(--Spacing-x2);
|
|
||||||
width: 32px;
|
|
||||||
height: var(--button-height);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--Spacing-x-half);
|
|
||||||
color: var(--UI-Semantic-Success);
|
|
||||||
}
|
|
||||||
|
|
||||||
.membershipNumberBadge {
|
|
||||||
border-radius: var(--Small);
|
|
||||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
|
||||||
background: var(--Base-Surface-Secondary-light-Normal);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { CheckCircleIcon } from "@/components/Icons"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
|
import styles from "./redeem.module.css"
|
||||||
|
|
||||||
|
export default function ActiveRedeemedBadge() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.redeemed}>
|
||||||
|
<motion.div
|
||||||
|
animate={{
|
||||||
|
opacity: [1, 0.4, 1],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckCircleIcon color="uiSemanticSuccess" />
|
||||||
|
</motion.div>
|
||||||
|
<Caption>{intl.formatMessage({ id: "Active" })}</Caption>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import CopyIcon from "@/components/Icons/Copy"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||||
|
|
||||||
|
import { RewardIcon } from "../../RewardIcon"
|
||||||
|
|
||||||
|
import styles from "../redeem.module.css"
|
||||||
|
|
||||||
|
import type { RewardWithRedeem } from "@/server/routers/contentstack/reward/output"
|
||||||
|
|
||||||
|
export default function Campaign({ reward }: { reward: RewardWithRedeem }) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
function handleCopy() {
|
||||||
|
navigator.clipboard.writeText(reward.operaRewardId)
|
||||||
|
toast.success(intl.formatMessage({ id: "Copied to clipboard" }))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.modalContent}>
|
||||||
|
<RewardIcon rewardId={reward.reward_id} />
|
||||||
|
<Title level="h3" textAlign="center" textTransform="regular">
|
||||||
|
{reward.label}
|
||||||
|
</Title>
|
||||||
|
<Body textAlign="center">{reward.description}</Body>
|
||||||
|
<div className={styles.rewardBadge}>
|
||||||
|
<Caption textAlign="center" color="uiTextHighContrast" type="bold">
|
||||||
|
{intl.formatMessage({ id: "Promo code" })}
|
||||||
|
</Caption>
|
||||||
|
<Caption textAlign="center" color="uiTextHighContrast">
|
||||||
|
{reward.operaRewardId}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer className={styles.modalFooter}>
|
||||||
|
<Button
|
||||||
|
onClick={handleCopy}
|
||||||
|
type="button"
|
||||||
|
variant="icon"
|
||||||
|
size="small"
|
||||||
|
theme="base"
|
||||||
|
intent="primary"
|
||||||
|
>
|
||||||
|
{reward.operaRewardId}
|
||||||
|
<CopyIcon color="pale" />
|
||||||
|
</Button>
|
||||||
|
</footer>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||||
|
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||||
|
import { isRestaurantOnSiteTierReward } from "@/utils/rewards"
|
||||||
|
|
||||||
|
import { RewardIcon } from "../../RewardIcon"
|
||||||
|
import ActiveRedeemedBadge from "../ActiveRedeemedBadge"
|
||||||
|
import MembershipNumberBadge from "../MembershipNumberBadge"
|
||||||
|
import TimedRedeemedBadge from "../TimedRedeemedBadge"
|
||||||
|
import useRedeemFlow from "../useRedeemFlow"
|
||||||
|
|
||||||
|
import styles from "../redeem.module.css"
|
||||||
|
|
||||||
|
import type { RewardWithRedeem } from "@/server/routers/contentstack/reward/output"
|
||||||
|
|
||||||
|
export default function Tier({
|
||||||
|
reward,
|
||||||
|
membershipNumber,
|
||||||
|
}: {
|
||||||
|
reward: RewardWithRedeem
|
||||||
|
membershipNumber: string
|
||||||
|
}) {
|
||||||
|
const { onRedeem, redeemStep, setRedeemStep, isRedeeming } =
|
||||||
|
useRedeemFlow(reward)
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.modalContent}>
|
||||||
|
{redeemStep === "redeemed" && (
|
||||||
|
<div className={styles.badge}>
|
||||||
|
{isRestaurantOnSiteTierReward(reward) ? (
|
||||||
|
<ActiveRedeemedBadge />
|
||||||
|
) : (
|
||||||
|
<TimedRedeemedBadge />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<RewardIcon rewardId={reward.reward_id} />
|
||||||
|
<Title level="h3" textAlign="center" textTransform="regular">
|
||||||
|
{reward.label}
|
||||||
|
</Title>
|
||||||
|
|
||||||
|
{redeemStep === "initial" && (
|
||||||
|
<Body textAlign="center">{reward.description}</Body>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{redeemStep === "confirmation" && (
|
||||||
|
<Body textAlign="center">{reward.redeem_description}</Body>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{redeemStep === "redeemed" &&
|
||||||
|
isRestaurantOnSiteTierReward(reward) &&
|
||||||
|
membershipNumber && (
|
||||||
|
<MembershipNumberBadge membershipNumber={membershipNumber} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{redeemStep === "initial" && (
|
||||||
|
<footer className={styles.modalFooter}>
|
||||||
|
<Button
|
||||||
|
onClick={() => setRedeemStep("confirmation")}
|
||||||
|
intent="primary"
|
||||||
|
theme="base"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Redeem benefit" })}
|
||||||
|
</Button>
|
||||||
|
</footer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{redeemStep === "confirmation" && (
|
||||||
|
<footer className={styles.modalFooter}>
|
||||||
|
<Button
|
||||||
|
onClick={onRedeem}
|
||||||
|
disabled={isRedeeming}
|
||||||
|
intent="primary"
|
||||||
|
theme="base"
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: "Yes, redeem" })}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={close} intent="secondary" theme="base">
|
||||||
|
{intl.formatMessage({ id: "Go back" })}
|
||||||
|
</Button>
|
||||||
|
</footer>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
|
import styles from "./redeem.module.css"
|
||||||
|
|
||||||
|
export default function MembershipNumberBadge({
|
||||||
|
membershipNumber,
|
||||||
|
}: {
|
||||||
|
membershipNumber: string
|
||||||
|
}) {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.rewardBadge}>
|
||||||
|
<Caption textAlign="center" color="uiTextHighContrast">
|
||||||
|
{intl.formatMessage({ id: "Membership ID:" })} {membershipNumber}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import Countdown from "@/components/Countdown"
|
||||||
|
import { CheckCircleIcon } from "@/components/Icons"
|
||||||
|
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||||
|
|
||||||
|
import styles from "./redeem.module.css"
|
||||||
|
|
||||||
|
export default function TimedRedeemedBadge() {
|
||||||
|
const intl = useIntl()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={styles.redeemed}>
|
||||||
|
<CheckCircleIcon color="uiSemanticSuccess" />
|
||||||
|
<Caption>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: "Redeemed & valid through:",
|
||||||
|
})}
|
||||||
|
</Caption>
|
||||||
|
</div>
|
||||||
|
<Countdown />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
131
components/Blocks/DynamicContent/Rewards/Redeem/index.tsx
Normal file
131
components/Blocks/DynamicContent/Rewards/Redeem/index.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
import { useState } from "react"
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
Modal,
|
||||||
|
ModalOverlay,
|
||||||
|
} from "react-aria-components"
|
||||||
|
import { useIntl } from "react-intl"
|
||||||
|
|
||||||
|
import { CloseLargeIcon } from "@/components/Icons"
|
||||||
|
import Button from "@/components/TempDesignSystem/Button"
|
||||||
|
|
||||||
|
import Campaign from "./Flows/Campaign"
|
||||||
|
import Tier from "./Flows/Tier"
|
||||||
|
import { RedeemContext } from "./useRedeemFlow"
|
||||||
|
|
||||||
|
import styles from "./redeem.module.css"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
RedeemModalState,
|
||||||
|
RedeemProps,
|
||||||
|
RedeemStep,
|
||||||
|
} from "@/types/components/myPages/myPage/accountPage"
|
||||||
|
import type { RewardWithRedeem } from "@/server/routers/contentstack/reward/output"
|
||||||
|
|
||||||
|
const MotionOverlay = motion(ModalOverlay)
|
||||||
|
const MotionModal = motion(Modal)
|
||||||
|
|
||||||
|
export default function Redeem({ reward, membershipNumber }: RedeemProps) {
|
||||||
|
const [animation, setAnimation] = useState<RedeemModalState>("unmounted")
|
||||||
|
const intl = useIntl()
|
||||||
|
const [redeemStep, setRedeemStep] = useState<RedeemStep>("initial")
|
||||||
|
|
||||||
|
function modalStateHandler(newAnimationState: RedeemModalState) {
|
||||||
|
setAnimation((currentAnimationState) =>
|
||||||
|
newAnimationState === "hidden" && currentAnimationState === "hidden"
|
||||||
|
? "unmounted"
|
||||||
|
: currentAnimationState
|
||||||
|
)
|
||||||
|
if (newAnimationState === "unmounted") {
|
||||||
|
setRedeemStep("initial")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RedeemContext.Provider value={{ redeemStep, setRedeemStep }}>
|
||||||
|
<DialogTrigger
|
||||||
|
onOpenChange={(isOpen) => setAnimation(isOpen ? "visible" : "hidden")}
|
||||||
|
>
|
||||||
|
<Button intent="primary" fullWidth>
|
||||||
|
{intl.formatMessage({ id: "Open" })}
|
||||||
|
</Button>
|
||||||
|
<MotionOverlay
|
||||||
|
className={styles.overlay}
|
||||||
|
isExiting={animation === "hidden"}
|
||||||
|
onAnimationComplete={modalStateHandler}
|
||||||
|
variants={variants.fade}
|
||||||
|
initial="hidden"
|
||||||
|
animate={animation}
|
||||||
|
>
|
||||||
|
<MotionModal
|
||||||
|
className={styles.modal}
|
||||||
|
variants={variants.slideInOut}
|
||||||
|
initial="hidden"
|
||||||
|
animate={animation}
|
||||||
|
>
|
||||||
|
<Dialog className={styles.dialog} aria-label={reward.label}>
|
||||||
|
{({ close }) => (
|
||||||
|
<>
|
||||||
|
<header className={styles.modalHeader}>
|
||||||
|
<button
|
||||||
|
onClick={close}
|
||||||
|
type="button"
|
||||||
|
className={styles.modalClose}
|
||||||
|
>
|
||||||
|
<CloseLargeIcon />
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{getRedeemFlow(reward, membershipNumber || "")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</MotionModal>
|
||||||
|
</MotionOverlay>
|
||||||
|
</DialogTrigger>
|
||||||
|
</RedeemContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const variants = {
|
||||||
|
fade: {
|
||||||
|
hidden: {
|
||||||
|
opacity: 0,
|
||||||
|
transition: { duration: 0.4, ease: "easeInOut" },
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: { duration: 0.4, ease: "easeInOut" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
slideInOut: {
|
||||||
|
hidden: {
|
||||||
|
opacity: 0,
|
||||||
|
y: 32,
|
||||||
|
transition: { duration: 0.4, ease: "easeInOut" },
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: { duration: 0.4, ease: "easeInOut" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRedeemFlow(reward: RewardWithRedeem, membershipNumber: string) {
|
||||||
|
switch (reward.rewardType) {
|
||||||
|
case "Campaign":
|
||||||
|
return <Campaign reward={reward} />
|
||||||
|
case "Surprise":
|
||||||
|
case "Tier":
|
||||||
|
return <Tier reward={reward} membershipNumber={membershipNumber} />
|
||||||
|
default:
|
||||||
|
console.warn("Unsupported reward type for redeem:", reward.rewardType)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
.badge {
|
||||||
|
border-radius: var(--Small, 4px);
|
||||||
|
border: 1px solid var(--Base-Border-Subtle);
|
||||||
|
display: flex;
|
||||||
|
padding: var(--Spacing-x1) var(--Spacing-x2);
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.redeemed {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
align-self: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
height: var(--visual-viewport-height);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.overlay {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||||
|
border-radius: var(--Corner-radius-Medium);
|
||||||
|
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 101;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 768px) {
|
||||||
|
.modal {
|
||||||
|
left: auto;
|
||||||
|
bottom: auto;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-bottom: var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalHeader {
|
||||||
|
--button-height: 32px;
|
||||||
|
box-sizing: content-box;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: var(--button-height);
|
||||||
|
position: relative;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--Spacing-x3) var(--Spacing-x2) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalContent {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Spacing-x2);
|
||||||
|
padding: 0 var(--Spacing-x3) var(--Spacing-x3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalFooter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 var(--Spacing-x3) var(--Spacing-x1);
|
||||||
|
gap: var(--Spacing-x-one-and-half);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalFooter > button {
|
||||||
|
flex: 1 0 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modalClose {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
right: var(--Spacing-x2);
|
||||||
|
width: 32px;
|
||||||
|
height: var(--button-height);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
color: var(--UI-Semantic-Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rewardBadge {
|
||||||
|
border-radius: var(--Corner-radius-Small);
|
||||||
|
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
||||||
|
background: var(--Base-Surface-Secondary-light-Normal);
|
||||||
|
display: grid;
|
||||||
|
gap: var(--Spacing-x-half);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { createContext, useCallback, useContext, useState } from "react"
|
||||||
|
|
||||||
|
import { trpc } from "@/lib/trpc/client"
|
||||||
|
|
||||||
|
import type { RedeemFlowContext } from "@/types/components/myPages/myPage/accountPage"
|
||||||
|
import type { RewardWithRedeem } from "@/server/routers/contentstack/reward/output"
|
||||||
|
|
||||||
|
export const RedeemContext = createContext<RedeemFlowContext>({
|
||||||
|
redeemStep: "initial",
|
||||||
|
setRedeemStep: () => undefined,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function useRedeemFlow(reward: RewardWithRedeem) {
|
||||||
|
const { redeemStep, setRedeemStep } = useContext(RedeemContext)
|
||||||
|
|
||||||
|
const update = trpc.contentstack.rewards.redeem.useMutation<{
|
||||||
|
rewards: RewardWithRedeem[]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const onRedeem = useCallback(() => {
|
||||||
|
if (reward?.id) {
|
||||||
|
update.mutate(
|
||||||
|
{ rewardId: reward.id },
|
||||||
|
{
|
||||||
|
onSuccess() {
|
||||||
|
setRedeemStep("redeemed")
|
||||||
|
},
|
||||||
|
onError(error) {
|
||||||
|
console.error("Failed to redeem", error)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}, [reward, update, setRedeemStep])
|
||||||
|
|
||||||
|
return {
|
||||||
|
onRedeem,
|
||||||
|
redeemStep,
|
||||||
|
setRedeemStep,
|
||||||
|
isRedeeming: update.isPending,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -103,6 +103,7 @@
|
|||||||
"Contact information": "Contact information",
|
"Contact information": "Contact information",
|
||||||
"Contact us": "Contact us",
|
"Contact us": "Contact us",
|
||||||
"Continue": "Continue",
|
"Continue": "Continue",
|
||||||
|
"Copied to clipboard": "Copied to clipboard",
|
||||||
"Copyright all rights reserved": "Scandic AB All rights reserved",
|
"Copyright all rights reserved": "Scandic AB All rights reserved",
|
||||||
"Could not find requested resource": "Could not find requested resource",
|
"Could not find requested resource": "Could not find requested resource",
|
||||||
"Country": "Country",
|
"Country": "Country",
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const SurpriseReward = z.object({
|
|||||||
rewardType: z.string().optional(),
|
rewardType: z.string().optional(),
|
||||||
endsAt: z.string().datetime({ offset: true }).optional(),
|
endsAt: z.string().datetime({ offset: true }).optional(),
|
||||||
coupons: z.array(Coupon).optional(),
|
coupons: z.array(Coupon).optional(),
|
||||||
|
operaRewardId: z.string().default(""),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const validateApiRewardSchema = z
|
export const validateApiRewardSchema = z
|
||||||
@@ -53,6 +54,7 @@ export const validateApiRewardSchema = z
|
|||||||
autoApplyReward: z.boolean().default(false),
|
autoApplyReward: z.boolean().default(false),
|
||||||
rewardType: z.string().optional(),
|
rewardType: z.string().optional(),
|
||||||
rewardTierLevel: z.string().optional(),
|
rewardTierLevel: z.string().optional(),
|
||||||
|
operaRewardId: z.string().default(""),
|
||||||
}),
|
}),
|
||||||
SurpriseReward,
|
SurpriseReward,
|
||||||
])
|
])
|
||||||
@@ -87,6 +89,7 @@ export const validateApiTierRewardsSchema = z.record(
|
|||||||
autoApplyReward: z.boolean().default(false),
|
autoApplyReward: z.boolean().default(false),
|
||||||
rewardType: z.string().optional(),
|
rewardType: z.string().optional(),
|
||||||
rewardTierLevel: z.string().optional(),
|
rewardTierLevel: z.string().optional(),
|
||||||
|
operaRewardId: z.string().default(""),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -99,7 +102,7 @@ export const validateCmsRewardsSchema = z
|
|||||||
z.object({
|
z.object({
|
||||||
taxonomies: z.array(
|
taxonomies: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
term_uid: z.string().optional(),
|
term_uid: z.string().optional().default(""),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
label: z.string().optional(),
|
label: z.string().optional(),
|
||||||
@@ -123,7 +126,7 @@ export const validateCmsRewardsWithRedeemSchema = z
|
|||||||
z.object({
|
z.object({
|
||||||
taxonomies: z.array(
|
taxonomies: z.array(
|
||||||
z.object({
|
z.object({
|
||||||
term_uid: z.string().optional(),
|
term_uid: z.string().optional().default(""),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
label: z.string().optional(),
|
label: z.string().optional(),
|
||||||
@@ -163,12 +166,14 @@ export type Reward = CMSReward & {
|
|||||||
id: string | undefined
|
id: string | undefined
|
||||||
rewardType: string | undefined
|
rewardType: string | undefined
|
||||||
redeemLocation: string | undefined
|
redeemLocation: string | undefined
|
||||||
|
operaRewardId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RewardWithRedeem = CMSRewardWithRedeem & {
|
export type RewardWithRedeem = CMSRewardWithRedeem & {
|
||||||
id: string | undefined
|
id: string | undefined
|
||||||
rewardType: string | undefined
|
rewardType: string | undefined
|
||||||
redeemLocation: string | undefined
|
redeemLocation: string | undefined
|
||||||
|
operaRewardId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// New endpoint related types and schemas.
|
// New endpoint related types and schemas.
|
||||||
@@ -178,16 +183,15 @@ const BenefitReward = z.object({
|
|||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
redeemLocation: z.string().optional(),
|
redeemLocation: z.string().optional(),
|
||||||
rewardId: z.string().optional(),
|
rewardId: z.string().optional(),
|
||||||
rewardType: z.string().optional(),
|
rewardType: z.string().optional(), // TODO: Should be "Tier" but can't because of backwards compatibility
|
||||||
rewardTierLevel: z.string().optional(),
|
rewardTierLevel: z.string().optional(),
|
||||||
status: z.string().optional(),
|
status: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const CouponState = z.enum(["claimed", "redeemed", "viewed"])
|
|
||||||
const CouponData = z.object({
|
const CouponData = z.object({
|
||||||
couponCode: z.string().optional(),
|
couponCode: z.string().optional(),
|
||||||
unwrapped: z.boolean().default(false),
|
unwrapped: z.boolean().default(false),
|
||||||
state: CouponState,
|
state: z.enum(["claimed", "redeemed", "viewed"]),
|
||||||
expiresAt: z.string().datetime({ offset: true }).optional(),
|
expiresAt: z.string().datetime({ offset: true }).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -195,8 +199,9 @@ const CouponReward = z.object({
|
|||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
rewardId: z.string().optional(),
|
rewardId: z.string().optional(),
|
||||||
rewardType: z.string().optional(),
|
rewardType: z.enum(["Surprise", "Campaign"]),
|
||||||
redeemLocation: z.string().optional(),
|
redeemLocation: z.string().optional(),
|
||||||
|
operaRewardId: z.string().default(""),
|
||||||
status: z.string().optional(),
|
status: z.string().optional(),
|
||||||
coupon: z.array(CouponData).optional(),
|
coupon: z.array(CouponData).optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ export const rewardQueryRouter = router({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await apiResponse.json()
|
const data = await apiResponse.json()
|
||||||
|
|
||||||
const validatedApiRewards = isNewEndpoint
|
const validatedApiRewards = isNewEndpoint
|
||||||
? validateCategorizedRewardsSchema.safeParse(data)
|
? validateCategorizedRewardsSchema.safeParse(data)
|
||||||
: validateApiRewardSchema.safeParse(data)
|
: validateApiRewardSchema.safeParse(data)
|
||||||
@@ -256,6 +257,10 @@ export const rewardQueryRouter = router({
|
|||||||
id: apiReward?.id,
|
id: apiReward?.id,
|
||||||
rewardType: apiReward?.rewardType,
|
rewardType: apiReward?.rewardType,
|
||||||
redeemLocation: apiReward?.redeemLocation,
|
redeemLocation: apiReward?.redeemLocation,
|
||||||
|
operaRewardId:
|
||||||
|
apiReward && "operaRewardId" in apiReward
|
||||||
|
? apiReward.operaRewardId
|
||||||
|
: "",
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { Reward } from "@/server/routers/contentstack/reward/output"
|
import type { Reward } from "@/server/routers/contentstack/reward/output"
|
||||||
|
|
||||||
export interface Surprise extends Reward {
|
export interface Surprise extends Omit<Reward, "operaRewardId"> {
|
||||||
coupons: { couponCode?: string; expiresAt?: string }[]
|
coupons: { couponCode?: string; expiresAt?: string }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { Dispatch, ReactNode, SetStateAction } from "react"
|
||||||
import type { z } from "zod"
|
import type { z } from "zod"
|
||||||
|
|
||||||
import type { DynamicContent } from "@/types/trpc/routers/contentstack/blocks"
|
import type { DynamicContent } from "@/types/trpc/routers/contentstack/blocks"
|
||||||
@@ -37,3 +38,8 @@ export interface RedeemProps {
|
|||||||
export type RedeemModalState = "unmounted" | "hidden" | "visible"
|
export type RedeemModalState = "unmounted" | "hidden" | "visible"
|
||||||
|
|
||||||
export type RedeemStep = "initial" | "confirmation" | "redeemed"
|
export type RedeemStep = "initial" | "confirmation" | "redeemed"
|
||||||
|
|
||||||
|
export type RedeemFlowContext = {
|
||||||
|
redeemStep: RedeemStep
|
||||||
|
setRedeemStep: Dispatch<SetStateAction<RedeemStep>>
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type {
|
|||||||
RestaurantRewardId,
|
RestaurantRewardId,
|
||||||
RewardId,
|
RewardId,
|
||||||
} from "@/types/components/myPages/rewards"
|
} from "@/types/components/myPages/rewards"
|
||||||
import type { Reward } from "@/server/routers/contentstack/reward/output"
|
import type { RewardWithRedeem } from "@/server/routers/contentstack/reward/output"
|
||||||
|
|
||||||
export function isValidRewardId(id: string): id is RewardId {
|
export function isValidRewardId(id: string): id is RewardId {
|
||||||
return Object.values<string>(REWARD_IDS).includes(id)
|
return Object.values<string>(REWARD_IDS).includes(id)
|
||||||
@@ -17,22 +17,26 @@ export function isRestaurantReward(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function redeemLocationIsOnSite(
|
export function redeemLocationIsOnSite(
|
||||||
location: Reward["redeemLocation"]
|
location: RewardWithRedeem["redeemLocation"]
|
||||||
): location is "On-site" {
|
): location is "On-site" {
|
||||||
return location === "On-site"
|
return location === "On-site"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isTierType(type: Reward["rewardType"]): type is "Tier" {
|
export function isTierType(
|
||||||
|
type: RewardWithRedeem["rewardType"]
|
||||||
|
): type is "Tier" {
|
||||||
return type === "Tier"
|
return type === "Tier"
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isOnSiteTierReward(reward: Reward): boolean {
|
export function isOnSiteTierReward(reward: RewardWithRedeem): boolean {
|
||||||
return (
|
return (
|
||||||
redeemLocationIsOnSite(reward.redeemLocation) &&
|
redeemLocationIsOnSite(reward.redeemLocation) &&
|
||||||
isTierType(reward.rewardType)
|
isTierType(reward.rewardType)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRestaurantOnSiteTierReward(reward: Reward): boolean {
|
export function isRestaurantOnSiteTierReward(
|
||||||
|
reward: RewardWithRedeem
|
||||||
|
): boolean {
|
||||||
return isOnSiteTierReward(reward) && isRestaurantReward(reward.reward_id)
|
return isOnSiteTierReward(reward) && isRestaurantReward(reward.reward_id)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user