321 lines
10 KiB
TypeScript
321 lines
10 KiB
TypeScript
"use client"
|
|
|
|
import { AnimatePresence, motion } from "framer-motion"
|
|
import { usePathname } from "next/navigation"
|
|
import React, { useState } from "react"
|
|
import { Dialog, Modal, ModalOverlay } from "react-aria-components"
|
|
import { useIntl } from "react-intl"
|
|
|
|
import { benefits } from "@/constants/routes/myPages"
|
|
import { dt } from "@/lib/dt"
|
|
import { trpc } from "@/lib/trpc/client"
|
|
|
|
import { ChevronRightSmallIcon, CloseLargeIcon } from "@/components/Icons"
|
|
import Image from "@/components/Image"
|
|
import Button from "@/components/TempDesignSystem/Button"
|
|
import Link from "@/components/TempDesignSystem/Link"
|
|
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 useLang from "@/hooks/useLang"
|
|
|
|
import styles from "./surprises.module.css"
|
|
|
|
import type {
|
|
Surprise,
|
|
SurprisesProps,
|
|
} from "@/types/components/blocks/surprises"
|
|
|
|
const variants = {
|
|
enter: (direction: number) => {
|
|
return {
|
|
x: direction > 0 ? 1000 : -1000,
|
|
opacity: 0,
|
|
}
|
|
},
|
|
center: {
|
|
x: 0,
|
|
opacity: 1,
|
|
},
|
|
exit: (direction: number) => {
|
|
return {
|
|
x: direction < 0 ? 1000 : -1000,
|
|
opacity: 0,
|
|
}
|
|
},
|
|
}
|
|
|
|
export default function SurprisesNotification({
|
|
surprises,
|
|
membershipNumber,
|
|
}: SurprisesProps) {
|
|
const lang = useLang()
|
|
const pathname = usePathname()
|
|
const [open, setOpen] = useState(true)
|
|
const [[selectedSurprise, direction], setSelectedSurprise] = useState([0, 0])
|
|
const [showSurprises, setShowSurprises] = useState(false)
|
|
const unwrap = trpc.contentstack.rewards.unwrap.useMutation({
|
|
onSuccess: () => {
|
|
if (pathname.indexOf(benefits[lang]) !== 0) {
|
|
toast.success(
|
|
<>
|
|
{intl.formatMessage(
|
|
{ id: "Gift(s) added to your benefits" },
|
|
{ amount: surprises.length }
|
|
)}
|
|
<br />
|
|
<Link href={benefits[lang]} variant="underscored" color="burgundy">
|
|
{intl.formatMessage({ id: "Go to My Benefits" })}
|
|
</Link>
|
|
</>
|
|
)
|
|
}
|
|
},
|
|
onError: (error) => {
|
|
console.error("Error", error)
|
|
},
|
|
})
|
|
const intl = useIntl()
|
|
|
|
if (!surprises.length) {
|
|
return null
|
|
}
|
|
|
|
const surprise = surprises[selectedSurprise]
|
|
|
|
function showSurprise(newDirection: number) {
|
|
setSelectedSurprise(([currentIndex]) => [
|
|
currentIndex + newDirection,
|
|
newDirection,
|
|
])
|
|
}
|
|
|
|
async function viewRewards() {
|
|
const updates = surprises
|
|
.map((surprise) => {
|
|
const coupons = surprise.coupons
|
|
?.map((coupon) => {
|
|
if (coupon?.couponCode) {
|
|
return {
|
|
rewardId: surprise.id,
|
|
couponCode: coupon.couponCode,
|
|
}
|
|
}
|
|
})
|
|
.filter(
|
|
(coupon): coupon is { rewardId: string; couponCode: string } =>
|
|
!!coupon
|
|
)
|
|
|
|
return coupons
|
|
})
|
|
.flat()
|
|
.filter(
|
|
(coupon): coupon is { rewardId: string; couponCode: string } => !!coupon
|
|
)
|
|
|
|
unwrap.mutate(updates)
|
|
}
|
|
|
|
const earliestExpirationDate = surprise.coupons?.reduce(
|
|
(earliestDate, coupon) => {
|
|
const expiresAt = dt(coupon.expiresAt)
|
|
return earliestDate.isBefore(expiresAt) ? earliestDate : expiresAt
|
|
},
|
|
dt()
|
|
)
|
|
|
|
return (
|
|
<ModalOverlay
|
|
className={styles.overlay}
|
|
isOpen={open}
|
|
onOpenChange={setOpen}
|
|
isKeyboardDismissDisabled
|
|
>
|
|
<Modal className={styles.modal}>
|
|
<Dialog aria-label="Surprises" className={styles.dialog}>
|
|
{({ close }) => {
|
|
return (
|
|
<>
|
|
<div className={styles.top}>
|
|
{surprises.length > 1 && showSurprises && (
|
|
<Caption type="label" uppercase>
|
|
{intl.formatMessage(
|
|
{ id: "{amount} out of {total}" },
|
|
{
|
|
amount: selectedSurprise + 1,
|
|
total: surprises.length,
|
|
}
|
|
)}
|
|
</Caption>
|
|
)}
|
|
<button
|
|
onClick={() => {
|
|
viewRewards()
|
|
close()
|
|
}}
|
|
type="button"
|
|
className={styles.close}
|
|
>
|
|
<CloseLargeIcon />
|
|
</button>
|
|
</div>
|
|
{showSurprises ? (
|
|
<>
|
|
<AnimatePresence
|
|
mode="popLayout"
|
|
initial={false}
|
|
custom={direction}
|
|
>
|
|
<motion.div
|
|
key={selectedSurprise}
|
|
custom={direction}
|
|
variants={variants}
|
|
initial="enter"
|
|
animate="center"
|
|
exit="exit"
|
|
transition={{
|
|
x: { type: "ease", duration: 0.5 },
|
|
opacity: { duration: 0.2 },
|
|
}}
|
|
>
|
|
<Surprise title={surprise.label}>
|
|
<Body textAlign="center">{surprise.description}</Body>
|
|
<div className={styles.badge}>
|
|
<Caption>
|
|
{intl.formatMessage(
|
|
{ id: "Expires at the earliest" },
|
|
{
|
|
date: dt(earliestExpirationDate)
|
|
.locale(lang)
|
|
.format("D MMM YYYY"),
|
|
}
|
|
)}
|
|
</Caption>
|
|
<Caption>
|
|
{intl.formatMessage({
|
|
id: "Membership ID",
|
|
})}{" "}
|
|
{membershipNumber}
|
|
</Caption>
|
|
</div>
|
|
</Surprise>
|
|
</motion.div>
|
|
</AnimatePresence>
|
|
{surprises.length > 1 && (
|
|
<>
|
|
<nav className={styles.nav}>
|
|
<Button
|
|
variant="icon"
|
|
intent="tertiary"
|
|
disabled={selectedSurprise === 0}
|
|
onPress={() => showSurprise(-1)}
|
|
size="small"
|
|
>
|
|
<ChevronRightSmallIcon
|
|
className={styles.chevron}
|
|
width={20}
|
|
height={20}
|
|
/>
|
|
{intl.formatMessage({ id: "Previous" })}
|
|
</Button>
|
|
<Button
|
|
variant="icon"
|
|
intent="tertiary"
|
|
disabled={selectedSurprise === surprises.length - 1}
|
|
onPress={() => showSurprise(1)}
|
|
size="small"
|
|
>
|
|
{intl.formatMessage({ id: "Next" })}
|
|
<ChevronRightSmallIcon width={20} height={20} />
|
|
</Button>
|
|
</nav>
|
|
</>
|
|
)}
|
|
</>
|
|
) : (
|
|
<Surprise title={intl.formatMessage({ id: "Surprise!" })}>
|
|
<Body textAlign="center">
|
|
{surprises.length > 1 ? (
|
|
<>
|
|
{intl.formatMessage<React.ReactNode>(
|
|
{
|
|
id: "You have <b>#</b> gifts waiting for you!",
|
|
},
|
|
{
|
|
amount: surprises.length,
|
|
b: (str) => <b>{str}</b>,
|
|
}
|
|
)}
|
|
<br />
|
|
{intl.formatMessage({
|
|
id: "Hurry up and use them before they expire!",
|
|
})}
|
|
</>
|
|
) : (
|
|
intl.formatMessage({
|
|
id: "We have a special gift waiting for you!",
|
|
})
|
|
)}
|
|
</Body>
|
|
<Caption>
|
|
{intl.formatMessage({
|
|
id: "You'll find all your gifts in 'My benefits'",
|
|
})}
|
|
</Caption>
|
|
|
|
<Button
|
|
intent="primary"
|
|
onPress={() => {
|
|
setShowSurprises(true)
|
|
}}
|
|
size="medium"
|
|
theme="base"
|
|
fullWidth
|
|
autoFocus
|
|
>
|
|
{intl.formatMessage(
|
|
{
|
|
id: "Open gift(s)",
|
|
},
|
|
{ amount: surprises.length }
|
|
)}
|
|
</Button>
|
|
</Surprise>
|
|
)}
|
|
</>
|
|
)
|
|
}}
|
|
</Dialog>
|
|
</Modal>
|
|
</ModalOverlay>
|
|
)
|
|
}
|
|
|
|
function Surprise({
|
|
title,
|
|
children,
|
|
}: {
|
|
title?: string
|
|
children?: React.ReactNode
|
|
}) {
|
|
return (
|
|
<div className={styles.content}>
|
|
<Image
|
|
src="/_static/img/loyalty-award.png"
|
|
width={113}
|
|
height={125}
|
|
alt="Gift"
|
|
/>
|
|
<header>
|
|
<Title textAlign="center" level="h4">
|
|
{title}
|
|
</Title>
|
|
</header>
|
|
|
|
{children}
|
|
</div>
|
|
)
|
|
}
|