fix(SW-696): add unwrap to surprises

add animations to sliding cards

various minor fixes
This commit is contained in:
Christian Andolf
2024-11-07 11:51:21 +01:00
parent 01638f4dd7
commit 0824f7ce26
14 changed files with 264 additions and 128 deletions

View File

@@ -1,5 +1,6 @@
"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"
@@ -21,7 +22,29 @@ import useLang from "@/hooks/useLang"
import styles from "./surprises.module.css"
import type { SurprisesProps } from "@/types/components/blocks/surprises"
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,
@@ -30,9 +53,29 @@ export default function SurprisesNotification({
const lang = useLang()
const pathname = usePathname()
const [open, setOpen] = useState(true)
const [selectedSurprise, setSelectedSurprise] = useState(0)
const [[selectedSurprise, direction], setSelectedSurprise] = useState([0, 0])
const [showSurprises, setShowSurprises] = useState(false)
const update = trpc.contentstack.rewards.update.useMutation()
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) {
@@ -41,36 +84,48 @@ export default function SurprisesNotification({
const surprise = surprises[selectedSurprise]
function showSurprise(n: number) {
setSelectedSurprise((surprise) => surprise + n)
function showSurprise(newDirection: number) {
setSelectedSurprise(([currentIndex]) => [
currentIndex + newDirection,
newDirection,
])
}
function viewRewards() {
if (surprise.reward_id) {
update.mutate({ id: surprise.reward_id })
}
}
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
)
function closeModal(close: VoidFunction) {
viewRewards()
close()
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>
</>
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}
@@ -96,7 +151,10 @@ export default function SurprisesNotification({
</Caption>
)}
<button
onClick={() => closeModal(close)}
onClick={() => {
viewRewards()
close()
}}
type="button"
className={styles.close}
>
@@ -105,23 +163,46 @@ export default function SurprisesNotification({
</div>
{showSurprises ? (
<>
<div className={styles.content}>
<Surprise title={surprise.label}>
<Body textAlign="center">{surprise.description}</Body>
<div className={styles.badge}>
<Caption>
{intl.formatMessage({ id: "Valid through" })}{" "}
{dt(surprise.endsAt)
.locale(lang)
.format("DD MMM YYYY")}
</Caption>
<Caption>
{intl.formatMessage({ id: "Membership ID" })}{" "}
{membershipNumber}
</Caption>
</div>
</Surprise>
</div>
<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}>
@@ -154,10 +235,10 @@ export default function SurprisesNotification({
)}
</>
) : (
<div className={styles.content}>
{surprises.length > 1 ? (
<Surprise title={intl.formatMessage({ id: "Surprise!" })}>
<Body textAlign="center">
<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!",
@@ -171,32 +252,22 @@ export default function SurprisesNotification({
{intl.formatMessage({
id: "Hurry up and use them before they expire!",
})}
</Body>
<Caption>
{intl.formatMessage({
id: "You'll find all your gifts in 'My benefits'",
})}
</Caption>
</Surprise>
) : (
<Surprise title={intl.formatMessage({ id: "Surprise!" })}>
<Body textAlign="center">
{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>
</Surprise>
)}
</>
) : (
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={() => {
viewRewards()
setShowSurprises(true)
}}
size="medium"
@@ -211,7 +282,7 @@ export default function SurprisesNotification({
{ amount: surprises.length }
)}
</Button>
</div>
</Surprise>
)}
</>
)
@@ -230,18 +301,20 @@ function Surprise({
children?: React.ReactNode
}) {
return (
<>
<div className={styles.content}>
<Image
src="/_static/img/loyalty-award.png"
width={113}
height={125}
alt="Gift"
/>
<Title textAlign="center" level="h4">
{title}
</Title>
<header>
<Title textAlign="center" level="h4">
{title}
</Title>
</header>
{children}
</>
</div>
)
}

View File

@@ -1,9 +1,14 @@
import { env } from "@/env/server"
import { getProfile } from "@/lib/trpc/memoizedRequests"
import { serverClient } from "@/lib/trpc/server"
import SurprisesNotification from "./SurprisesNotification"
export default async function Surprises() {
if (env.HIDE_FOR_NEXT_RELEASE) {
return null
}
const user = await getProfile()
if (!user || "error" in user) {

View File

@@ -28,7 +28,7 @@
z-index: 100;
&[data-entering] {
animation: modal-fade 200ms;
animation: modal-fade 200ms ease-in;
}
&[data-exiting] {
animation: modal-fade 200ms reverse ease-in;
@@ -40,6 +40,17 @@
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;
}
}
}
@@ -65,6 +76,10 @@
flex-direction: column;
gap: var(--Spacing-x2);
padding-bottom: var(--Spacing-x2);
/* to hide sliding cards */
position: relative;
overflow: hidden;
}
@media screen and (min-width: 768px) {
@@ -90,8 +105,10 @@
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0 var(--Spacing-x3);
gap: var(--Spacing-x2);
min-height: 350px;
}
.nav {
@@ -103,6 +120,8 @@
}
.nav button {
user-select: none;
&:nth-child(1) {
padding-left: 0;
}