feat(SW-556): add surprise notification component
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { trpc } from "@/lib/trpc/client"
|
||||
import { Reward } from "@/server/routers/contentstack/reward/output"
|
||||
import { ApiReward, Reward } from "@/server/routers/contentstack/reward/output"
|
||||
|
||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||
import Grids from "@/components/TempDesignSystem/Grids"
|
||||
@@ -9,10 +9,16 @@ import ShowMoreButton from "@/components/TempDesignSystem/ShowMoreButton"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import Surprises from "../Surprises"
|
||||
|
||||
import styles from "./current.module.css"
|
||||
|
||||
type CurrentRewardsClientProps = {
|
||||
initialCurrentRewards: { rewards: Reward[]; nextCursor: number | undefined }
|
||||
initialCurrentRewards: {
|
||||
rewards: Reward[]
|
||||
apiRewards: ApiReward[]
|
||||
nextCursor: number | undefined
|
||||
}
|
||||
}
|
||||
export default function ClientCurrentRewards({
|
||||
initialCurrentRewards,
|
||||
@@ -32,25 +38,34 @@ export default function ClientCurrentRewards({
|
||||
},
|
||||
}
|
||||
)
|
||||
function loadMoreData() {
|
||||
if (hasNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
}
|
||||
const filteredRewards =
|
||||
data?.pages.filter((page) => page && page.rewards) ?? []
|
||||
const rewards = filteredRewards.flatMap((page) => page?.rewards) as Reward[]
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
|
||||
if (!rewards.length) {
|
||||
const rewards =
|
||||
data?.pages
|
||||
.flatMap((page) => page?.rewards)
|
||||
.filter((reward): reward is Reward => !!reward) ?? []
|
||||
|
||||
const surprises =
|
||||
data?.pages
|
||||
.flatMap((page) => page?.apiRewards)
|
||||
.filter((reward): reward is ApiReward => reward?.type === "surprise") ??
|
||||
[]
|
||||
|
||||
if (!rewards.length && !surprises.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
function loadMoreData() {
|
||||
if (hasNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<Grids.Stackable>
|
||||
{rewards.map((reward, idx) => (
|
||||
<article className={styles.card} key={`${reward.reward_id}-${idx}`}>
|
||||
@@ -71,6 +86,7 @@ export default function ClientCurrentRewards({
|
||||
) : (
|
||||
<ShowMoreButton loadMoreData={loadMoreData} />
|
||||
))}
|
||||
</div>
|
||||
<Surprises surprises={surprises} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
231
components/Blocks/DynamicContent/Rewards/Surprises/index.tsx
Normal file
231
components/Blocks/DynamicContent/Rewards/Surprises/index.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
"use client"
|
||||
|
||||
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 { ApiReward } from "@/server/routers/contentstack/reward/output"
|
||||
|
||||
import { ChevronRightSmallIcon, CloseLargeIcon } from "@/components/Icons"
|
||||
import Image from "@/components/Image"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import Body from "@/components/TempDesignSystem/Text/Body"
|
||||
import Caption from "@/components/TempDesignSystem/Text/Caption"
|
||||
import CaptionLabel from "@/components/TempDesignSystem/Text/CaptionLabel"
|
||||
import Title from "@/components/TempDesignSystem/Text/Title"
|
||||
import { toast } from "@/components/TempDesignSystem/Toasts"
|
||||
import useLang from "@/hooks/useLang"
|
||||
|
||||
import styles from "./surprises.module.css"
|
||||
|
||||
interface SurprisesProps {
|
||||
surprises: ApiReward[]
|
||||
}
|
||||
|
||||
export default function Surprises({ surprises }: SurprisesProps) {
|
||||
const lang = useLang()
|
||||
const [open, setOpen] = useState(true)
|
||||
const [selectedSurprise, setSelectedSurprise] = useState(0)
|
||||
const [showSurprises, setShowSurprises] = useState(false)
|
||||
const update = trpc.contentstack.rewards.update.useMutation()
|
||||
const intl = useIntl()
|
||||
|
||||
if (!surprises.length) return null
|
||||
|
||||
function showSurprise(n: number) {
|
||||
setSelectedSurprise((surprise) => surprise + n)
|
||||
}
|
||||
|
||||
function viewRewards(id?: string) {
|
||||
if (!id) return
|
||||
update.mutate({ id })
|
||||
}
|
||||
|
||||
const surprise = surprises[selectedSurprise]
|
||||
|
||||
return (
|
||||
<ModalOverlay
|
||||
className={styles.overlay}
|
||||
isOpen={open}
|
||||
onOpenChange={setOpen}
|
||||
>
|
||||
<Modal className={styles.modal}>
|
||||
<Dialog aria-label="Surprises" className={styles.dialog}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.top}>
|
||||
{surprises.length > 1 && showSurprises && (
|
||||
<CaptionLabel uppercase>
|
||||
{intl.formatMessage(
|
||||
{ id: "{amount} out of {total}" },
|
||||
{
|
||||
amount: selectedSurprise + 1,
|
||||
total: surprises.length,
|
||||
}
|
||||
)}
|
||||
</CaptionLabel>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
viewRewards()
|
||||
toast.success(
|
||||
<>
|
||||
{intl.formatMessage(
|
||||
{ id: "Gift(s) added to your benefits" },
|
||||
{ amount: surprises.length }
|
||||
)}
|
||||
<br />
|
||||
<a href={benefits[lang]}>
|
||||
{intl.formatMessage({ id: "Go to My Benefits" })}
|
||||
</a>
|
||||
</>
|
||||
)
|
||||
close()
|
||||
}}
|
||||
type="button"
|
||||
className={styles.close}
|
||||
>
|
||||
<CloseLargeIcon />
|
||||
</button>
|
||||
</div>
|
||||
{showSurprises ? (
|
||||
<>
|
||||
<div className={styles.content}>
|
||||
<Surprise title={surprise.title}>
|
||||
<Body textAlign="center">
|
||||
This is just some dummy text describing the gift and
|
||||
should be replaced.
|
||||
</Body>
|
||||
<div className={styles.badge}>
|
||||
<Caption>Valid through DD M YYYY</Caption>
|
||||
<Caption>Member ID 000000</Caption>
|
||||
</div>
|
||||
</Surprise>
|
||||
</div>
|
||||
{surprises.length > 1 && (
|
||||
<>
|
||||
<nav className={styles.nav}>
|
||||
<Button
|
||||
wrapping
|
||||
variant="icon"
|
||||
intent="tertiary"
|
||||
disabled={selectedSurprise === 0}
|
||||
onClick={() => showSurprise(-1)}
|
||||
size="small"
|
||||
>
|
||||
<ChevronRightSmallIcon
|
||||
className={styles.chevron}
|
||||
width={20}
|
||||
height={20}
|
||||
/>
|
||||
{intl.formatMessage({ id: "Previous" })}
|
||||
</Button>
|
||||
<Button
|
||||
wrapping
|
||||
variant="icon"
|
||||
intent="tertiary"
|
||||
disabled={selectedSurprise === surprises.length - 1}
|
||||
onClick={() => showSurprise(1)}
|
||||
size="small"
|
||||
>
|
||||
{intl.formatMessage({ id: "Next" })}
|
||||
<ChevronRightSmallIcon width={20} height={20} />
|
||||
</Button>
|
||||
</nav>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className={styles.content}>
|
||||
{surprises.length > 1 ? (
|
||||
<Surprise title={intl.formatMessage({ id: "Surprise!" })}>
|
||||
<Body textAlign="center">
|
||||
{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!",
|
||||
})}
|
||||
</Body>
|
||||
<Caption>
|
||||
{intl.formatMessage({
|
||||
id: "You'll find all your gifts in 'My benefits'",
|
||||
})}
|
||||
</Caption>
|
||||
</Surprise>
|
||||
) : (
|
||||
<Surprise title={surprise.title}>
|
||||
<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>
|
||||
)}
|
||||
|
||||
<Button
|
||||
intent="primary"
|
||||
onPress={() => {
|
||||
viewRewards(surprise.id)
|
||||
setShowSurprises(true)
|
||||
}}
|
||||
size="medium"
|
||||
theme="base"
|
||||
fullWidth
|
||||
>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "Open gift(s)",
|
||||
},
|
||||
{ amount: surprises.length }
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</ModalOverlay>
|
||||
)
|
||||
}
|
||||
|
||||
function Surprise({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title?: string
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src="/_static/img/loyalty-award.png"
|
||||
width={113}
|
||||
height={125}
|
||||
alt="Gift"
|
||||
/>
|
||||
<Title textAlign="center" level="h4">
|
||||
{title}
|
||||
</Title>
|
||||
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
.icon {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@keyframes modal-fade {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-up {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
height: var(--visual-viewport-height);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
z-index: 100;
|
||||
|
||||
&[data-entering] {
|
||||
animation: modal-fade 200ms;
|
||||
}
|
||||
&[data-exiting] {
|
||||
animation: modal-fade 200ms reverse ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.overlay {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: var(--Base-Surface-Primary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Medium);
|
||||
box-shadow: 0px 4px 24px 0px rgba(38, 32, 30, 0.08);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
transition: height 200ms ease-in-out;
|
||||
|
||||
&[data-entering] {
|
||||
animation: slide-up 200ms;
|
||||
}
|
||||
&[data-exiting] {
|
||||
animation: slide-up 200ms reverse ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--Spacing-x2);
|
||||
padding-bottom: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.modal {
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.top {
|
||||
--button-height: 32px;
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--button-height);
|
||||
position: relative;
|
||||
justify-content: center;
|
||||
padding: var(--Spacing-x2) var(--Spacing-x2) 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0 var(--Spacing-x3);
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.nav {
|
||||
border-top: 1px solid var(--Base-Border-Subtle);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 var(--Spacing-x2);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav button {
|
||||
&[disabled] {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: var(--Spacing-x1) var(--Spacing-x-one-and-half);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--Spacing-x-half);
|
||||
background-color: var(--Base-Surface-Secondary-light-Normal);
|
||||
border-radius: var(--Corner-radius-Small);
|
||||
}
|
||||
|
||||
.close {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: var(--Spacing-x2);
|
||||
width: 32px;
|
||||
height: var(--button-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
Reference in New Issue
Block a user