diff --git a/components/MyPages/Surprises/Card.tsx b/components/MyPages/Surprises/Card.tsx
new file mode 100644
index 000000000..932f0ecc9
--- /dev/null
+++ b/components/MyPages/Surprises/Card.tsx
@@ -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 (
+
+
+
+
+ {children}
+
+ )
+}
diff --git a/components/MyPages/Surprises/Client.tsx b/components/MyPages/Surprises/Client.tsx
new file mode 100644
index 000000000..2e4202103
--- /dev/null
+++ b/components/MyPages/Surprises/Client.tsx
@@ -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 }
+ )}
+
+
+ {intl.formatMessage({ id: "Go to My Benefits" })}
+
+ >
+ )
+ }
+ },
+ 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 (
+
+
+
+ {open && (
+
+
+
+ )}
+
+
+ )
+}
+
+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,
+ }
+ },
+}
diff --git a/components/MyPages/Surprises/Header.tsx b/components/MyPages/Surprises/Header.tsx
new file mode 100644
index 000000000..3a25400a9
--- /dev/null
+++ b/components/MyPages/Surprises/Header.tsx
@@ -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 (
+
+ {children}
+
+
+ )
+}
diff --git a/components/MyPages/Surprises/Initial.tsx b/components/MyPages/Surprises/Initial.tsx
new file mode 100644
index 000000000..8e3a60b9c
--- /dev/null
+++ b/components/MyPages/Surprises/Initial.tsx
@@ -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 (
+
+
+ {totalSurprises > 1 ? (
+ <>
+ {intl.formatMessage(
+ {
+ id: "You have # gifts waiting for you!",
+ },
+ {
+ amount: totalSurprises,
+ b: (str) => {str},
+ }
+ )}
+
+ {intl.formatMessage({
+ id: "Hurry up and use them before they expire!",
+ })}
+ >
+ ) : (
+ intl.formatMessage({
+ id: "We have a special gift waiting for you!",
+ })
+ )}
+
+
+ {intl.formatMessage({
+ id: "You'll find all your gifts in 'My benefits'",
+ })}
+
+
+
+
+ )
+}
diff --git a/components/MyPages/Surprises/Navigation.tsx b/components/MyPages/Surprises/Navigation.tsx
new file mode 100644
index 000000000..2a1296f01
--- /dev/null
+++ b/components/MyPages/Surprises/Navigation.tsx
@@ -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 (
+
+ )
+}
diff --git a/components/MyPages/Surprises/Slide.tsx b/components/MyPages/Surprises/Slide.tsx
new file mode 100644
index 000000000..2a4173fca
--- /dev/null
+++ b/components/MyPages/Surprises/Slide.tsx
@@ -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 (
+
+ {surprise.description}
+
+
+ {intl.formatMessage(
+ { id: "Expires at the earliest" },
+ {
+ date: dt(earliestExpirationDate)
+ .locale(lang)
+ .format("D MMM YYYY"),
+ }
+ )}
+
+
+ {intl.formatMessage({
+ id: "Membership ID",
+ })}{" "}
+ {membershipNumber}
+
+
+
+ )
+}
diff --git a/components/MyPages/Surprises/SurprisesNotification.tsx b/components/MyPages/Surprises/SurprisesNotification.tsx
deleted file mode 100644
index f69269c8c..000000000
--- a/components/MyPages/Surprises/SurprisesNotification.tsx
+++ /dev/null
@@ -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 }
- )}
-
-
- {intl.formatMessage({ id: "Go to My Benefits" })}
-
- >
- )
- }
- },
- 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 (
-
-
-
-
-
- )
-}
-
-function Surprise({
- title,
- children,
-}: {
- title?: string
- children?: React.ReactNode
-}) {
- return (
-
-
-
-
- {children}
-
- )
-}
diff --git a/components/MyPages/Surprises/confetti.ts b/components/MyPages/Surprises/confetti.ts
new file mode 100644
index 000000000..2990fade4
--- /dev/null
+++ b/components/MyPages/Surprises/confetti.ts
@@ -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"],
+ })
+}
diff --git a/components/MyPages/Surprises/index.tsx b/components/MyPages/Surprises/index.tsx
index 4a0eae273..2ae414a3a 100644
--- a/components/MyPages/Surprises/index.tsx
+++ b/components/MyPages/Surprises/index.tsx
@@ -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 (
-
diff --git a/components/MyPages/Surprises/surprises.module.css b/components/MyPages/Surprises/surprises.module.css
index c752fece3..9a80761f4 100644
--- a/components/MyPages/Surprises/surprises.module.css
+++ b/components/MyPages/Surprises/surprises.module.css
@@ -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;
+}
diff --git a/package-lock.json b/package-lock.json
index e611379b4..d1a2b5f1f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,7 @@
"@trpc/client": "^11.0.0-rc.467",
"@trpc/react-query": "^11.0.0-rc.467",
"@trpc/server": "^11.0.0-rc.467",
+ "@tsparticles/confetti": "^3.5.0",
"@vercel/otel": "^1.9.1",
"@vis.gl/react-google-maps": "^1.2.0",
"class-variance-authority": "^0.7.0",
@@ -6402,6 +6403,279 @@
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true
},
+ "node_modules/@tsparticles/basic": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/basic/-/basic-3.5.0.tgz",
+ "integrity": "sha512-oty33TxM2aHWrzcwWRic1bQ04KBCdpnvzv8JXEkx5Uyp70vgVegUbtKmwGki3shqKZIt3v2qE4I8NsK6onhLrA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/matteobruni"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/tsparticles"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://www.buymeacoffee.com/matteobruni"
+ }
+ ],
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0",
+ "@tsparticles/move-base": "^3.5.0",
+ "@tsparticles/shape-circle": "^3.5.0",
+ "@tsparticles/updater-color": "^3.5.0",
+ "@tsparticles/updater-opacity": "^3.5.0",
+ "@tsparticles/updater-out-modes": "^3.5.0",
+ "@tsparticles/updater-size": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/confetti": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/confetti/-/confetti-3.5.0.tgz",
+ "integrity": "sha512-wS3nqtanbCvAbNlyAffKJq6lgIPzHFljEOO3JSCDgRD6rG5X/jvidhw2vR3kLrjBTV40c+Xv6MpJgSgTRWkogg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/matteobruni"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/tsparticles"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://www.buymeacoffee.com/matteobruni"
+ }
+ ],
+ "dependencies": {
+ "@tsparticles/basic": "^3.5.0",
+ "@tsparticles/engine": "^3.5.0",
+ "@tsparticles/plugin-emitters": "^3.5.0",
+ "@tsparticles/plugin-motion": "^3.5.0",
+ "@tsparticles/shape-cards": "^3.5.0",
+ "@tsparticles/shape-emoji": "^3.5.0",
+ "@tsparticles/shape-heart": "^3.5.0",
+ "@tsparticles/shape-image": "^3.5.0",
+ "@tsparticles/shape-polygon": "^3.5.0",
+ "@tsparticles/shape-square": "^3.5.0",
+ "@tsparticles/shape-star": "^3.5.0",
+ "@tsparticles/updater-life": "^3.5.0",
+ "@tsparticles/updater-roll": "^3.5.0",
+ "@tsparticles/updater-rotate": "^3.5.0",
+ "@tsparticles/updater-tilt": "^3.5.0",
+ "@tsparticles/updater-wobble": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/engine": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/engine/-/engine-3.5.0.tgz",
+ "integrity": "sha512-RCwrJ2SvSYdhXJ24oUCjSUKEZQ9lXwObOWMvfMC9vS6/bk+Qo0N7Xx8AfumqzP/LebB1YJdlCvuoJMauAon0Pg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/matteobruni"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/tsparticles"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://www.buymeacoffee.com/matteobruni"
+ }
+ ],
+ "hasInstallScript": true
+ },
+ "node_modules/@tsparticles/move-base": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/move-base/-/move-base-3.5.0.tgz",
+ "integrity": "sha512-9oDk7zTxyhUCstj3lHTNTiWAgqIBzWa2o1tVQFK63Qwq+/WxzJCSwZOocC9PAHGM1IP6nA4zYJSfjbMBTrUocA==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/plugin-emitters": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/plugin-emitters/-/plugin-emitters-3.5.0.tgz",
+ "integrity": "sha512-8Vg6wAPS75ibkukqtTM7yoC+8NnfXBl8xVUUbTaoeQCE0WDWwztboMf5L4pUgWe9WA52ZgFkWtT/mFH5wk5T9g==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/plugin-motion": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/plugin-motion/-/plugin-motion-3.5.0.tgz",
+ "integrity": "sha512-juP8f9ABjlhQmg4SO+tTofLYJwvwLPfKWJYvG8c6HU2rlJxJ/6eeWe9kDpv/T8nun3kXYHtrLhcJAmvWg/b5qA==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/shape-cards": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/shape-cards/-/shape-cards-3.5.0.tgz",
+ "integrity": "sha512-rU7rp1Yn1leHpCNA/7vrfY6tcLjvrG6A6sOT11dSanIj2J8zgLNXnbVtRJPtU13x+masft9Ta1tpw3dFRdtHcA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/matteobruni"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/tsparticles"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://www.buymeacoffee.com/matteobruni"
+ }
+ ],
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/shape-circle": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/shape-circle/-/shape-circle-3.5.0.tgz",
+ "integrity": "sha512-59TmXkeeI6Jzv5vt/D3TkclglabaoEXQi2kbDjSCBK68SXRHzlQu29mSAL41Y5S0Ft5ZJKkAQHX1IqEnm8Hyjg==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/shape-emoji": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/shape-emoji/-/shape-emoji-3.5.0.tgz",
+ "integrity": "sha512-cxWHxQxnG5vLDltkoxdo7KS87uKPwQgf4SDWy/WCxW4Psm1TEeeSGYMJPVed+wWPspOKmLb7u8OaEexgE2pHHQ==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/shape-heart": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/shape-heart/-/shape-heart-3.5.0.tgz",
+ "integrity": "sha512-MvOxW6X7w1jHH+KRJShvHMDhRZ+bpei2mAqQOFR5HY+2D6KFzaDVgtfGFwoiaX8Pm6oP6OQssQ3QnDtrywLRFw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/matteobruni"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/tsparticles"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://www.buymeacoffee.com/matteobruni"
+ }
+ ],
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/shape-image": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/shape-image/-/shape-image-3.5.0.tgz",
+ "integrity": "sha512-lWYg7DTv74dSOnXy+4dr7t1/OSuUmxDpIo12Lbxgx/QBN7A5I/HoqbKcs13TSA0RS1hcuMgtti07BcDTEYW3Dw==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/shape-polygon": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/shape-polygon/-/shape-polygon-3.5.0.tgz",
+ "integrity": "sha512-sqYL+YXpnq3nSWcOEGZaJ4Z7Cb7x8M0iORSLpPdNEIvwDKdPczYyQM95D8ep19Pv1CV5L0uRthV36wg7UpnJ9Q==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/shape-square": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/shape-square/-/shape-square-3.5.0.tgz",
+ "integrity": "sha512-rPHpA4Pzm1W5DIIow+lQS+VS7D2thSBQQbV9eHxb933Wh0/QC3me3w4vovuq7hdtVANhsUVO04n44Gc/2TgHkw==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/shape-star": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/shape-star/-/shape-star-3.5.0.tgz",
+ "integrity": "sha512-EDEJc4MYv3UbOeA3wrZjuJVtZ08PdCzzBij3T/7Tp3HUCf/p9XnfHBd/CPR5Mo6X0xpGfrein8UQN9CjGLHwUA==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/updater-color": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/updater-color/-/updater-color-3.5.0.tgz",
+ "integrity": "sha512-TGGgiLixIg37sst2Fj9IV4XbdMwkT6PYanM7qEqyfmv4hJ/RHMQlCznEe6b7OhChQVBg5ov5EMl/BT4/fIWEYw==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/updater-life": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/updater-life/-/updater-life-3.5.0.tgz",
+ "integrity": "sha512-jlMEq16dwN+rZmW/UmLdqaCe4W0NFrVdmXkZV8QWYgu06a+Ucslz337nHYaP89/9rZWpNua/uq1JDjDzaVD5Jg==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/updater-opacity": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/updater-opacity/-/updater-opacity-3.5.0.tgz",
+ "integrity": "sha512-T2YfqdIFV/f5VOg1JQsXu6/owdi9g9K2wrJlBfgteo+IboVp6Lcuo4PGAfilWVkWrTdp1Nz4mz39NrLHfOce2g==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/updater-out-modes": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/updater-out-modes/-/updater-out-modes-3.5.0.tgz",
+ "integrity": "sha512-y6NZe2OSk5SrYdaLwUIQnHICsNEDIdPPJHQ2nAWSvAuPJphlSKjUknc7OaGiFwle6l0OkhWoZZe1rV1ktbw/lA==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/updater-roll": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/updater-roll/-/updater-roll-3.5.0.tgz",
+ "integrity": "sha512-K3NfBGqVIu2zyJv72oNPlYLMDQKmUXTaCvnxUjzBEJJCYRdx7KhZPQVjAsfVYLHd7m7D7/+wKlkXmdYYAd67bg==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/updater-rotate": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/updater-rotate/-/updater-rotate-3.5.0.tgz",
+ "integrity": "sha512-j4qPHQd1eUmDoGnIJOsVswHLqtTof1je+b2GTOLB3WIoKmlyUpzQYjVc7PNfLMuCEUubwpZCfcd/vC80VZeWkg==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/updater-size": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/updater-size/-/updater-size-3.5.0.tgz",
+ "integrity": "sha512-TnWlOChBsVZffT2uO0S4ALGSzxT6UAMIVlhGMGFgSeIlktKMqM+dxDGAPrYa1n8IS2dkVGisiXzsV0Ss6Ceu1A==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/updater-tilt": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/updater-tilt/-/updater-tilt-3.5.0.tgz",
+ "integrity": "sha512-ovK6jH4fAmTav1kCC5Z1FW/pPjKxtK+X+w9BZJEddpS5cyBEdWD4FgvNgLnmZYpK0xad/nb+xxqeDkpSu/O51Q==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
+ "node_modules/@tsparticles/updater-wobble": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/@tsparticles/updater-wobble/-/updater-wobble-3.5.0.tgz",
+ "integrity": "sha512-fpN0XPvAf3dJ5UU++C+ETVDLurpnkzje02w865Ar4ubPBgGpMhowr6AbtFUe37Zl8rFUTYntBOSEoxqNYJAUgQ==",
+ "dependencies": {
+ "@tsparticles/engine": "^3.5.0"
+ }
+ },
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
diff --git a/package.json b/package.json
index 5871b48b4..7baa98fe1 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"@trpc/client": "^11.0.0-rc.467",
"@trpc/react-query": "^11.0.0-rc.467",
"@trpc/server": "^11.0.0-rc.467",
+ "@tsparticles/confetti": "^3.5.0",
"@vercel/otel": "^1.9.1",
"@vis.gl/react-google-maps": "^1.2.0",
"class-variance-authority": "^0.7.0",
diff --git a/types/components/blocks/surprises.ts b/types/components/blocks/surprises.ts
index da4a8730c..674c52d39 100644
--- a/types/components/blocks/surprises.ts
+++ b/types/components/blocks/surprises.ts
@@ -9,3 +9,27 @@ export interface SurprisesProps {
surprises: Surprise[]
membershipNumber?: string
}
+
+export interface NavigationProps {
+ selectedSurprise: number
+ totalSurprises: number
+ showSurprise: (direction: number) => void
+}
+
+export interface CardProps extends React.PropsWithChildren {
+ title?: string
+}
+
+export interface InitialProps {
+ totalSurprises: number
+ onOpen: VoidFunction
+}
+
+export interface SlideProps {
+ surprise: Surprise
+ membershipNumber?: string
+}
+
+export interface HeaderProps extends React.PropsWithChildren {
+ onClose: VoidFunction
+}