fix(SW-696): split up surprises into more components for readability
add tsparticles for confetti
This commit is contained in:
26
components/MyPages/Surprises/Card.tsx
Normal file
26
components/MyPages/Surprises/Card.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import Image from "@/components/Image"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
|
||||
import styles from "./surprises.module.css"
|
||||
|
||||
import type { CardProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
export default function Card({ title, children }: CardProps) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
217
components/MyPages/Surprises/Client.tsx
Normal file
217
components/MyPages/Surprises/Client.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
"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 { trpc } from "@/lib/trpc/client"
|
||||
|
||||
import Link from "@/components/TempDesignSystem/Link"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import confetti from "./confetti"
|
||||
import Header from "./Header"
|
||||
import Initial from "./Initial"
|
||||
import Navigation from "./Navigation"
|
||||
import Slide from "./Slide"
|
||||
|
||||
import styles from "./surprises.module.css"
|
||||
|
||||
import type { SurprisesProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
const MotionModal = motion(Modal)
|
||||
|
||||
export default function SurprisesNotification({
|
||||
surprises,
|
||||
membershipNumber,
|
||||
}: SurprisesProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
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("Failed to unwrap surprise", error)
|
||||
},
|
||||
})
|
||||
|
||||
const totalSurprises = surprises.length
|
||||
if (!totalSurprises) {
|
||||
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)
|
||||
}
|
||||
|
||||
return (
|
||||
<ModalOverlay
|
||||
className={styles.overlay}
|
||||
isOpen={open}
|
||||
onOpenChange={setOpen}
|
||||
isKeyboardDismissDisabled
|
||||
>
|
||||
<canvas id="surprise-confetti" className={styles.confetti} />
|
||||
<AnimatePresence mode="wait">
|
||||
{open && (
|
||||
<MotionModal
|
||||
className={styles.modal}
|
||||
initial={{ y: 32, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 32, opacity: 0 }}
|
||||
transition={{
|
||||
y: { duration: 0.4, ease: "easeInOut" },
|
||||
opacity: { duration: 0.4, ease: "easeInOut" },
|
||||
}}
|
||||
onAnimationComplete={confetti}
|
||||
>
|
||||
<Dialog aria-label="Surprises" className={styles.dialog}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
onClose={() => {
|
||||
viewRewards()
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{showSurprises && totalSurprises > 1 && (
|
||||
<Caption type="label" uppercase>
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} out of {total}" },
|
||||
{
|
||||
amount: selectedSurprise + 1,
|
||||
total: totalSurprises,
|
||||
}
|
||||
)}
|
||||
</Caption>
|
||||
)}
|
||||
</Header>
|
||||
|
||||
{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 },
|
||||
}}
|
||||
layout
|
||||
>
|
||||
<Slide
|
||||
surprise={surprise}
|
||||
membershipNumber={membershipNumber}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{totalSurprises > 1 && (
|
||||
<Navigation
|
||||
selectedSurprise={selectedSurprise}
|
||||
totalSurprises={totalSurprises}
|
||||
showSurprise={showSurprise}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Initial
|
||||
totalSurprises={totalSurprises}
|
||||
onOpen={() => {
|
||||
setShowSurprises(true)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Dialog>
|
||||
</MotionModal>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
},
|
||||
}
|
||||
16
components/MyPages/Surprises/Header.tsx
Normal file
16
components/MyPages/Surprises/Header.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { CloseLargeIcon } from "@/components/Icons"
|
||||
|
||||
import styles from "./surprises.module.css"
|
||||
|
||||
import type { HeaderProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
export default function Header({ onClose, children }: HeaderProps) {
|
||||
return (
|
||||
<div className={styles.top}>
|
||||
{children}
|
||||
<button onClick={onClose} type="button" className={styles.close}>
|
||||
<CloseLargeIcon />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
62
components/MyPages/Surprises/Initial.tsx
Normal file
62
components/MyPages/Surprises/Initial.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
|
||||
import Card from "./Card"
|
||||
|
||||
import type { InitialProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
export default function Initial({ totalSurprises, onOpen }: InitialProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<Card title={intl.formatMessage({ id: "Surprise!" })}>
|
||||
<Body textAlign="center">
|
||||
{totalSurprises > 1 ? (
|
||||
<>
|
||||
{intl.formatMessage<React.ReactNode>(
|
||||
{
|
||||
id: "You have <b>#</b> gifts waiting for you!",
|
||||
},
|
||||
{
|
||||
amount: totalSurprises,
|
||||
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={onOpen}
|
||||
size="medium"
|
||||
theme="base"
|
||||
fullWidth
|
||||
autoFocus
|
||||
>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Open gift(s)",
|
||||
},
|
||||
{ amount: totalSurprises }
|
||||
)}
|
||||
</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
45
components/MyPages/Surprises/Navigation.tsx
Normal file
45
components/MyPages/Surprises/Navigation.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { ChevronRightSmallIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
|
||||
import styles from "./surprises.module.css"
|
||||
|
||||
import type { NavigationProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
export default function Navigation({
|
||||
selectedSurprise,
|
||||
totalSurprises,
|
||||
showSurprise,
|
||||
}: NavigationProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<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 === totalSurprises - 1}
|
||||
onPress={() => showSurprise(1)}
|
||||
size="small"
|
||||
>
|
||||
{intl.formatMessage({ id: "Next" })}
|
||||
<ChevronRightSmallIcon width={20} height={20} />
|
||||
</Button>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
49
components/MyPages/Surprises/Slide.tsx
Normal file
49
components/MyPages/Surprises/Slide.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { dt } from "@/lib/dt"
|
||||
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import Card from "./Card"
|
||||
|
||||
import styles from "./surprises.module.css"
|
||||
|
||||
import type { SlideProps } from "@/types/components/blocks/surprises"
|
||||
|
||||
export default function Slide({ surprise, membershipNumber }: SlideProps) {
|
||||
const lang = useLang()
|
||||
const intl = useIntl()
|
||||
|
||||
const earliestExpirationDate = surprise.coupons?.reduce(
|
||||
(earliestDate, coupon) => {
|
||||
const expiresAt = dt(coupon.expiresAt)
|
||||
return earliestDate.isBefore(expiresAt) ? earliestDate : expiresAt
|
||||
},
|
||||
dt()
|
||||
)
|
||||
return (
|
||||
<Card 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>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
13
components/MyPages/Surprises/confetti.ts
Normal file
13
components/MyPages/Surprises/confetti.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { confetti as particlesConfetti } from "@tsparticles/confetti"
|
||||
|
||||
export default function confetti() {
|
||||
particlesConfetti("surprise-confetti", {
|
||||
count: 300,
|
||||
spread: 150,
|
||||
position: {
|
||||
y: 60,
|
||||
},
|
||||
colors: ["#cd0921", "#4d001b", "#fff"],
|
||||
shapes: ["star", "square", "circle", "polygon"],
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { env } from "@/env/server"
|
||||
import { getProfile } from "@/lib/trpc/memoizedRequests"
|
||||
import { serverClient } from "@/lib/trpc/server"
|
||||
|
||||
import SurprisesNotification from "./SurprisesNotification"
|
||||
import SurprisesClient from "./Client"
|
||||
|
||||
export default async function Surprises() {
|
||||
if (env.HIDE_FOR_NEXT_RELEASE) {
|
||||
@@ -22,7 +22,7 @@ export default async function Surprises() {
|
||||
}
|
||||
|
||||
return (
|
||||
<SurprisesNotification
|
||||
<SurprisesClient
|
||||
surprises={surprises}
|
||||
membershipNumber={user.membership?.membershipNumber}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@keyframes modal-fade {
|
||||
@keyframes fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
@@ -8,16 +8,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
height: var(--visual-viewport-height);
|
||||
@@ -28,10 +18,10 @@
|
||||
z-index: 100;
|
||||
|
||||
&[data-entering] {
|
||||
animation: modal-fade 200ms ease-in;
|
||||
animation: fade 400ms ease-in;
|
||||
}
|
||||
&[data-exiting] {
|
||||
animation: modal-fade 200ms reverse ease-in;
|
||||
animation: fade 400ms reverse ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,17 +30,19 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
&:before {
|
||||
background-image: url("/_static/img/confetti.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center 40%;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: modal-fade 200ms ease-in;
|
||||
display: block;
|
||||
}
|
||||
@media screen and (min-width: 768px) and (prefers-reduced-motion) {
|
||||
.overlay:before {
|
||||
background-image: url("/_static/img/confetti.svg");
|
||||
background-repeat: no-repeat;
|
||||
background-position: center 40%;
|
||||
content: "";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: fade 400ms ease-in;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,12 +54,14 @@
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
z-index: 102;
|
||||
}
|
||||
|
||||
&[data-entering] {
|
||||
animation: slide-up 200ms;
|
||||
}
|
||||
&[data-exiting] {
|
||||
animation: slide-up 200ms reverse ease-in-out;
|
||||
@media screen and (min-width: 768px) {
|
||||
.modal {
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,14 +76,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.modal {
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
--button-height: 32px;
|
||||
box-sizing: content-box;
|
||||
@@ -160,3 +146,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/*
|
||||
* temporary fix until next version of tsparticles is released
|
||||
* https://github.com/tsparticles/tsparticles/issues/5375
|
||||
*/
|
||||
.confetti {
|
||||
position: relative;
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user