Skip to content

Commit

Permalink
dApp: Acre points UI (#704)
Browse files Browse the repository at this point in the history
Ref: #722

### Overview:
- `AcrePointsCard` component
- `AcrePointsClaimModal` component
- Data refetching
- Feature flag 

### Screenshots:
<img width="577" alt="image"
src="https://github.com/user-attachments/assets/fd35802b-0c89-4465-b644-6185f9cb58f0">

<img width="577" alt="image"
src="https://github.com/user-attachments/assets/475a6781-ebda-441f-893b-b7f449cb4f00">

<img width="577" alt="image"
src="https://github.com/user-attachments/assets/a0b06a70-8102-4c4a-9354-d3b7d75b3358">


https://github.com/user-attachments/assets/01f04ccc-2a1f-42a3-b260-869691178a07
  • Loading branch information
kkosiorowska authored Oct 18, 2024
2 parents fb2fbac + 2be0e39 commit b901071
Show file tree
Hide file tree
Showing 36 changed files with 12,309 additions and 14,365 deletions.
3 changes: 2 additions & 1 deletion dapp/.env
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ VITE_SENTRY_DSN=""
VITE_ETH_HOSTNAME_HTTP="https://sepolia.infura.io/v3/c80e8ccdcc4c4a809bce4fc165310617"
VITE_REFERRAL=0

# TODO: Set this env variable in CI.
# ENDPOINTS
VITE_TBTC_API_ENDPOINT=""
VITE_ACRE_API_ENDPOINT="http://localhost:8788/api/v1/"

Expand All @@ -26,5 +26,6 @@ VITE_FEATURE_FLAG_WITHDRAWALS_ENABLED="false"
VITE_FEATURE_FLAG_OKX_WALLET_ENABLED="false"
VITE_FEATURE_FLAG_XVERSE_WALLET_ENABLED="false"
VITE_FEATURE_FLAG_BEEHIVE_COMPONENT_ENABLED="false"
VITE_FEATURE_FLAG_ACRE_POINTS_ENABLED="true"
VITE_FEATURE_FLAG_TVL_ENABLED="false"

1 change: 1 addition & 0 deletions dapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"formik": "^2.4.5",
"framer-motion": "^10.16.5",
"react": "^18.2.0",
"react-confetti-explosion": "^2.1.2",
"react-dom": "^18.2.0",
"react-number-format": "^5.3.1",
"react-redux": "^9.1.0",
Expand Down
214 changes: 214 additions & 0 deletions dapp/src/components/AcrePointsClaimModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import React, { ReactNode, useEffect, useMemo, useState } from "react"
import { useTimeout } from "#/hooks"
import { Box, Button, ModalBody, VStack } from "@chakra-ui/react"
import {
AnimationSequence,
motion,
Transition,
useAnimate,
} from "framer-motion"
import { logPromiseFailure, numberToLocaleString } from "#/utils"
import { ONE_SEC_IN_MILLISECONDS } from "#/constants"
import ConfettiExplosion from "react-confetti-explosion"
import { BaseModalProps } from "#/types"
import { AnimatedNumber } from "./shared/AnimatedNumber"
import { TextXl } from "./shared/Typography"
import withBaseModal from "./ModalRoot/withBaseModal"

const MotionBox = motion(Box)

const INITIAL_CONTAINER_HEIGHT = 214
const CONTAINER_HEIGHT = 288
const VALUE_SCALE = 0.375
const STEP_HEIGHT = CONTAINER_HEIGHT * (1 - VALUE_SCALE)
const STEP_SPACING = 32
const TRANSITION: Transition = {
type: "spring",
damping: 14,
stiffness: 86,
delay: 4, // step duration
}
const AUTOCLOSE_DELAY = 12 * ONE_SEC_IN_MILLISECONDS
const CONFETTI_DURATION = 4 * ONE_SEC_IN_MILLISECONDS

const getStepOffsets = (
stepCount: number,
stepHeight: number,
spacing: number,
) =>
Array(stepCount - 1)
.fill(0)
.map((_, index) =>
index === 0
? -stepHeight
: (index + 1) * -stepHeight - spacing * 2 ** index,
)

type AcrePointsClaimModalBaseProps = {
claimedAmount: number
totalAmount: number
} & BaseModalProps

function AcrePointsClaimModalBase({
claimedAmount,
totalAmount,
closeModal,
}: AcrePointsClaimModalBaseProps) {
const formattedClaimedAmount = numberToLocaleString(claimedAmount)
const formattedTotalAmount = numberToLocaleString(totalAmount)

const steps = useMemo<[string, ReactNode][]>(
() => [
[
"You earned",
<AnimatedNumber
value={formattedClaimedAmount}
prefix="+"
suffix=" PTS"
animateMode="whileInView"
color="brand.400"
/>,
],
[
"Updating points balance...",
<AnimatedNumber
value={formattedTotalAmount}
suffix=" PTS"
animateMode="whileInView"
indicationColor="brand.400"
/>,
],
// TODO: Uncomment when the leaderboard feature is ready
// [
// "Calculating rank...",
// <AnimatedNumber
// value={rankPositionDifference}
// prefix={rankPositionDifference > 0 ? "+" : "-"}
// animateMode="whileInView"
// color={rankPositionDifference > 0 ? "green.500" : "red.500"}
// />,
// ],
// [
// "Updating rank...",
// <AnimatedNumber
// value={estimatedRankPosition}
// prefix="#"
// animateMode="whileInView"
// indicationColor="brand.400"
// />,
// ],
],
[formattedClaimedAmount, formattedTotalAmount],
)

const [scope, animate] = useAnimate()

useEffect(() => {
const offsets = getStepOffsets(steps.length, STEP_HEIGHT, STEP_SPACING)
const valueElements = [
...(scope.current as HTMLElement).querySelectorAll("[data-step-value]"),
].slice(0, -1)

const sequence = [
["[data-steps-list]", { y: offsets[0] }, TRANSITION],
[
"[data-container]",
{
clipPath: `polygon(0 0, 100% 0, 100% ${CONTAINER_HEIGHT}px, 0 ${CONTAINER_HEIGHT}px)`,
},
{ at: "<", ...TRANSITION },
],

[valueElements[0], { scale: VALUE_SCALE }, { at: "<", ...TRANSITION }],
["[data-close-button]", { opacity: 1 }, { at: "<", ...TRANSITION }],

// TODO: Uncomment when the leaderboard feature is ready

// ["[data-steps-list]", { y: offsets[1] }, TRANSITION],
// [valueElements[1], { scale: VALUE_SCALE }, { at: "<", ...TRANSITION }],

// ["[data-steps-list]", { y: offsets[2] }, TRANSITION],
// [valueElements[2], { scale: VALUE_SCALE }, { at: "<", ...TRANSITION }],
] as AnimationSequence

const handleAnimation = async () => {
await animate(sequence)
}

logPromiseFailure(handleAnimation())
}, [scope, animate, steps])

const [isCofettiExploded, setIsCofettiExploded] = useState(false)

useTimeout(closeModal, AUTOCLOSE_DELAY)

return (
<ModalBody gap={0} p={0} pos="relative" ref={scope}>
<MotionBox
data-container
clipPath={`polygon(0 0, 100% 0, 100% ${INITIAL_CONTAINER_HEIGHT}px, 0 ${INITIAL_CONTAINER_HEIGHT}px)`}
overflow="hidden"
>
<VStack data-steps-list spacing={8}>
{steps.map(([currentStepLabel, currentStepValue]) => (
<Box key={currentStepLabel}>
<TextXl
fontWeight="semibold"
mb="5.25rem" // 84px
>
{currentStepLabel}
</TextXl>

<Box
data-step-value
transformOrigin="bottom"
fontSize="8xl"
lineHeight="6.25rem" // 100px
fontWeight="bold"
color="grey.700"
>
{currentStepValue}
</Box>
</Box>
))}
</VStack>
</MotionBox>

<Button
opacity={0}
onClick={closeModal}
data-close-button
variant="outline"
>
Yay!
</Button>

{!isCofettiExploded && (
<Box
pos="absolute"
top={0}
left="50%"
translateX="-50%"
transform="auto"
>
<ConfettiExplosion
zIndex={1410} // Chakra's modal has a z-index of 1400
width={768}
height="100vh"
duration={CONFETTI_DURATION}
force={0.2}
onComplete={() => setIsCofettiExploded(true)}
/>
</Box>
)}
</ModalBody>
)
}

const AcrePointsClaimModal = withBaseModal(AcrePointsClaimModalBase, {
returnFocusOnClose: false,
variant: "unstyled",
size: "full",
})

export default AcrePointsClaimModal
4 changes: 1 addition & 3 deletions dapp/src/components/MezoBeehiveModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ function MezoBeehiveModalBase() {
<VStack spacing={1}>
{data && (
<HStack>
<H6 fontWeight="bold">
{numberToLocaleString(data.totalMats, 0)}
</H6>
<H6 fontWeight="bold">{numberToLocaleString(data.totalMats)}</H6>
<TextLg fontWeight="bold">MATS</TextLg>
</HStack>
)}
Expand Down
2 changes: 2 additions & 0 deletions dapp/src/components/ModalRoot/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import WelcomeModal from "../WelcomeModal"
import MezoBeehiveModal from "../MezoBeehiveModal"
import ConnectWalletModal from "../ConnectWalletModal"
import UnexpectedErrorModal from "../UnexpectedErrorModal"
import AcrePointsClaimModal from "../AcrePointsClaimModal"

const MODALS: Record<ModalType, ElementType> = {
STAKE: TransactionModal,
Expand All @@ -14,6 +15,7 @@ const MODALS: Record<ModalType, ElementType> = {
MEZO_BEEHIVE: MezoBeehiveModal,
CONNECT_WALLET: ConnectWalletModal,
UNEXPECTED_ERROR: UnexpectedErrorModal,
ACRE_POINTS_CLAIM: AcrePointsClaimModal,
} as const

export default function ModalRoot() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import React, { useMemo, useState } from "react"
import {
HStack,
Icon,
Menu,
MenuButton,
MenuItem,
MenuList,
StackProps,
VStack,
} from "@chakra-ui/react"
import { H4, TextMd } from "#/components/shared/Typography"
import { numberToLocaleString } from "#/utils"
import { IconChevronDown } from "@tabler/icons-react"
import { useTokenAmountField } from "#/components/shared/TokenAmountForm/TokenAmountFormBase"
import {
ONE_MONTH_IN_DAYS,
ONE_WEEK_IN_DAYS,
ONE_YEAR_IN_DAYS,
} from "#/constants"

const ACRE_POINTS_DATA = {
week: {
label: "Per week",
multipler: ONE_WEEK_IN_DAYS,
},
month: {
label: "Per month",
multipler: ONE_MONTH_IN_DAYS,
},
year: {
label: "Per year",
multipler: ONE_YEAR_IN_DAYS,
},
}

function AcrePointsRewardEstimation(props: StackProps) {
const [selectedTierItem, setSelectedTierItem] = useState(
ACRE_POINTS_DATA.week,
)

const tierItems = [
selectedTierItem,
...Object.values(ACRE_POINTS_DATA).filter(
({ label, multipler }) =>
label !== selectedTierItem.label &&
multipler !== selectedTierItem.multipler,
),
]

const { value = 0n } = useTokenAmountField()
const baseReward = Number(value)
const pointsRate = 10000

const estimatedReward = useMemo(
() => (selectedTierItem.multipler * baseReward) / pointsRate,
[baseReward, selectedTierItem],
)

return (
<VStack spacing={2} {...props}>
<HStack>
<TextMd fontWeight="semibold">Acre points you&apos;ll earn</TextMd>

<Menu gutter={0} matchWidth offset={[0, -32]}>
{({ isOpen }) => (
<>
<MenuButton
type="button"
h="auto"
px={3}
py={1}
rounded="2xl"
bg="gold.300"
_hover={{ bg: "gold.200" }}
>
<HStack spacing={1}>
<TextMd>{selectedTierItem.label}</TextMd>
<Icon
as={IconChevronDown}
boxSize={4}
color="brand.400"
zIndex={2}
rotate={isOpen ? 180 : 0}
transform="auto"
/>
</HStack>
</MenuButton>

<MenuList
p={0}
minW={0}
rounded="2xl"
shadow="none"
bg="gold.300"
border="none"
overflow="hidden"
motionProps={{
variants: {},
}}
>
{tierItems.map((tierItem) => (
<MenuItem
type="button"
px={3}
py={1}
rounded="2xl"
bg="gold.300"
_active={{ bg: "gold.200" }}
_hover={{ bg: "gold.200" }}
key={tierItem.label}
onClick={() => setSelectedTierItem(tierItem)}
fontWeight="semibold"
>
{tierItem.label}
</MenuItem>
))}
</MenuList>
</>
)}
</Menu>
</HStack>

<H4>+{numberToLocaleString(estimatedReward)} PTS</H4>
</VStack>
)
}

export default AcrePointsRewardEstimation
Loading

0 comments on commit b901071

Please sign in to comment.