Files
web/apps/scandic-web/components/Blocks/DynamicContent/Rewards/Redeem/index.tsx
Rasmus Langvad c65091b36a Merged in feat/SW-3644-storybook-v10 (pull request #3240)
feat(SW-3644): Storybook v10

* Auto update to Storybook v10

* Add scandic theme and logo

* Update yarn.lock

* Update formatting of package.json

* Update vitest config and playwright plugin

* Remove vitest 4 update

* Re-added comment

* Update the Typography component to explicitly return React.ReactNode

* Add an explicit type assertion to the export

* Add an explicit type assertion to the export for Checkbox

* Explicit return type assertion

* Add an explicit type assertion to the export

* Update @types/react and fix ts warnings

* Updated typings


Approved-by: Linus Flood
Approved-by: Matilda Landström
2025-11-28 08:05:40 +00:00

194 lines
5.7 KiB
TypeScript

"use client"
import { motion } from "motion/react"
import { useState } from "react"
import {
Dialog,
DialogTrigger,
Modal,
ModalOverlay,
} from "react-aria-components"
import { useIntl } from "react-intl"
import { logger } from "@scandic-hotels/common/logger"
import { Button } from "@scandic-hotels/design-system/Button"
import { MaterialIcon } from "@scandic-hotels/design-system/Icons/MaterialIcon"
import { trpc } from "@scandic-hotels/trpc/client"
import useLang from "@/hooks/useLang"
import { isRestaurantOnSiteTierReward } from "@/utils/rewards"
import RedeemCampaign from "./Flows/Campaign"
import RedeemTier from "./Flows/Tier"
import { ConfirmClose } from "./ConfirmClose"
import { RedeemContext } from "./useRedeemFlow"
import styles from "./redeem.module.css"
import type { Reward } from "@scandic-hotels/trpc/types/rewards"
import type {
RedeemModalState,
RedeemProps,
RedeemStep,
} from "@/types/components/myPages/myPage/accountPage"
const MotionOverlay = motion.create(ModalOverlay)
const MotionModal = motion.create(Modal)
const thirtyMinutesInMs = 1000 * 60 * 30
export default function Redeem({ reward, membershipNumber }: RedeemProps) {
const [animation, setAnimation] = useState<RedeemModalState>("unmounted")
const intl = useIntl()
const lang = useLang()
const utils = trpc.useUtils()
const [redeemStep, setRedeemStep] = useState<RedeemStep>("initial")
const [timeRemaining, setTimeRemaining] = useState(thirtyMinutesInMs)
function modalStateHandler(newAnimationState: RedeemModalState) {
setAnimation((currentAnimationState) =>
newAnimationState === "hidden" && currentAnimationState === "hidden"
? "unmounted"
: currentAnimationState
)
if (newAnimationState === "unmounted") {
setRedeemStep("initial")
}
}
return (
<RedeemContext.Provider
value={{
redeemStep,
setRedeemStep,
defaultTimeRemaining: thirtyMinutesInMs,
timeRemaining,
setTimeRemaining,
}}
>
<DialogTrigger
onOpenChange={(isOpen) => setAnimation(isOpen ? "visible" : "hidden")}
>
<Button
variant="Tertiary"
size="Large"
typography="Body/Paragraph/mdBold"
className={styles.redeemButton}
>
{reward.redeemLocation === "Non-redeemable"
? intl.formatMessage({
id: "rewards.howToUse",
defaultMessage: "How to use",
})
: intl.formatMessage({
id: "common.open",
defaultMessage: "Open",
})}
</Button>
{animation !== "unmounted" && (
<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 }) => {
function closeModal() {
if (
redeemStep === "redeemed" ||
redeemStep === "confirm-close"
) {
utils.contentstack.rewards.current.invalidate({
lang,
})
}
close()
}
return (
<>
<header className={styles.modalHeader}>
<button
onClick={() => {
if (
redeemStep === "redeemed" &&
!isRestaurantOnSiteTierReward(reward)
) {
setRedeemStep("confirm-close")
} else {
closeModal()
}
}}
type="button"
className={styles.modalClose}
>
<MaterialIcon icon="close" />
</button>
</header>
{redeemStep === "confirm-close" ? (
<ConfirmClose close={closeModal} />
) : (
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" },
},
} as const,
slideInOut: {
hidden: {
opacity: 0,
y: 32,
transition: { duration: 0.4, ease: "easeInOut" },
},
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: "easeInOut" },
},
},
} as const
function getRedeemFlow(reward: Reward, membershipNumber: string) {
const { rewardType } = reward
switch (rewardType) {
case "Campaign":
return <RedeemCampaign reward={reward} />
case "Surprise":
case "Tier":
return <RedeemTier reward={reward} membershipNumber={membershipNumber} />
default:
logger.warn("Unsupported reward type for redeem:", rewardType)
return null
}
}