From 8d175c8212e881d27172be6f3208f71f52c4a988 Mon Sep 17 00:00:00 2001 From: Manuel Gellfart Date: Thu, 19 Sep 2024 16:44:57 +0200 Subject: [PATCH] feat: dynamic campaign promotions (#202) --- src/components/Claim/index.tsx | 10 -- .../DashboardWidgets/ClaimingWidget.tsx | 27 +-- src/components/Points/CampaignPromo.tsx | 159 +++++++++--------- src/components/Points/styles.module.css | 12 ++ src/components/Sep5DeadlineChip/index.tsx | 16 -- src/components/Sep5InfoBox/index.tsx | 44 ----- src/hooks/useCampaigns.ts | 6 + src/utils/airdrop.ts | 13 -- 8 files changed, 100 insertions(+), 187 deletions(-) delete mode 100644 src/components/Sep5DeadlineChip/index.tsx delete mode 100644 src/components/Sep5InfoBox/index.tsx diff --git a/src/components/Claim/index.tsx b/src/components/Claim/index.tsx index 077ccc59..e7b3d65d 100644 --- a/src/components/Claim/index.tsx +++ b/src/components/Claim/index.tsx @@ -30,8 +30,6 @@ import SafeToken from '@/public/images/token.svg' import css from './styles.module.css' import { useRouter } from 'next/router' import { AppRoutes } from '@/config/routes' -import { canRedeemSep5Airdrop } from '@/utils/airdrop' -import { Sep5InfoBox } from '../Sep5InfoBox' import { formatAmount } from '@/utils/formatters' import { ClaimCard } from '../ClaimCard' import { InfoAlert } from '../InfoAlert' @@ -78,8 +76,6 @@ const ClaimOverview = (): ReactElement => { // Allocation, vesting and voting power const { data: allocation } = useSafeTokenAllocation() - const canRedeemSep5 = canRedeemSep5Airdrop(allocation) - const { ecosystemVesting, investorVesting } = getVestingTypes(allocation?.vestingData ?? []) const { sep5, user, ecosystem, investor, total } = useTaggedAllocations() @@ -213,12 +209,6 @@ const ClaimOverview = (): ReactElement => { - {canRedeemSep5 && ( - - - - )} - { const { safe } = useSafeAppsSDK() const delegate = useDelegate() const { data: allocation } = useSafeTokenAllocation() - const canRedeemSep5 = canRedeemSep5Airdrop(allocation) const totalClaimed = allocation?.vestingData.reduce((acc, { amountClaimed }) => { return acc.add(amountClaimed) @@ -96,23 +92,6 @@ const VotingPowerWidget = (): ReactElement => { return ( - {canRedeemSep5 && ( - - - New allocation - - - - - - - - - )} Your voting power @@ -164,7 +143,6 @@ const VotingPowerWidget = (): ReactElement => { export const ClaimingWidget = (): ReactElement => { const { data: allocation, isLoading } = useSafeTokenAllocation() - const canRedeemSep5 = canRedeemSep5Airdrop(allocation) if (isLoading) { return ( @@ -186,7 +164,6 @@ export const ClaimingWidget = (): ReactElement => { sx={{ minWidth: WIDGET_WIDTH, maxWidth: WIDGET_WIDTH, - border: canRedeemSep5 ? ({ palette }) => `1px solid ${palette.primary.main}` : undefined, }} > <>{allocation?.votingPower.eq(0) ? : } diff --git a/src/components/Points/CampaignPromo.tsx b/src/components/Points/CampaignPromo.tsx index c390b676..f29e76da 100644 --- a/src/components/Points/CampaignPromo.tsx +++ b/src/components/Points/CampaignPromo.tsx @@ -1,40 +1,45 @@ -import { Box, LinearProgress, Stack, SvgIcon, Typography } from '@mui/material' +import { Box, LinearProgress, Stack, Typography } from '@mui/material' import PaperContainer from '../PaperContainer' -import MorhoIcon from '@/public/images/morpho.svg' -import SpotlightIcon from '@/public/images/spotlight.svg' import css from './styles.module.css' -import { MORPHO_CAMPAIGN_IDS } from '@/config/constants' import { useChainId } from '@/hooks/useChainId' -import { useCampaignInfo } from '@/hooks/useCampaigns' +import { useCampaignsPaginated } from '@/hooks/useCampaigns' import { getRelativeTime } from '@/utils/date' import { getSafeAppUrl } from '@/utils/safe-apps' import { useAddress } from '@/hooks/useAddress' import { useMemo } from 'react' import { ExternalLink } from '../ExternalLink' +import { formatAmount } from '@/utils/formatters' export const CampaignPromo = () => { const chainId = useChainId() const address = useAddress() - const morphoCampaignId = MORPHO_CAMPAIGN_IDS[chainId] + const { data: campaignsPage } = useCampaignsPaginated() - const morphoCampaign = useCampaignInfo(morphoCampaignId) + // We only check the first page as it contains the most up-to-date campaigns + const promotedCampaign = useMemo( + () => campaignsPage?.find((campaign) => campaign.isPromoted && new Date(campaign.endDate).getTime() > Date.now()), + [campaignsPage], + ) const safeAppUrl = useMemo( - () => (address ? getSafeAppUrl(chainId, address, 'https://safe-app.morpho.org/') : ''), - [chainId, address], + () => + address && promotedCampaign?.safeAppUrl + ? getSafeAppUrl(chainId, address, promotedCampaign?.safeAppUrl ?? '') + : undefined, + [address, chainId, promotedCampaign?.safeAppUrl], ) - if (!morphoCampaign) { + if (!promotedCampaign) { return null } - const hasStarted = new Date(morphoCampaign.startDate).getTime() <= Date.now() - const hasEnded = new Date(morphoCampaign.endDate).getTime() <= Date.now() + const hasStarted = new Date(promotedCampaign.startDate).getTime() <= Date.now() + const hasEnded = new Date(promotedCampaign.endDate).getTime() <= Date.now() const progress = - Math.max(Date.now() - new Date(morphoCampaign.startDate).getTime(), 0) / - (new Date(morphoCampaign.endDate).getTime() - new Date(morphoCampaign.startDate).getTime()) + Math.max(Date.now() - new Date(promotedCampaign.startDate).getTime(), 0) / + (new Date(promotedCampaign.endDate).getTime() - new Date(promotedCampaign.startDate).getTime()) if (hasEnded) { return null @@ -45,7 +50,7 @@ export const CampaignPromo = () => { { /> - {morphoCampaign + {promotedCampaign ? !hasStarted - ? `Starts ${getRelativeTime(new Date(morphoCampaign.startDate))}` - : `Ends ${getRelativeTime(new Date(morphoCampaign.endDate))}` + ? `Starts ${getRelativeTime(new Date(promotedCampaign.startDate))}` + : `Ends ${getRelativeTime(new Date(promotedCampaign.endDate))}` : null} - - - - + {promotedCampaign.iconUrl ? ( + + {promotedCampaign.iconUrl && ( + {'Campaign + )} + + ) : ( + + {promotedCampaign.name} + + )} + + + Prize pool + + {promotedCampaign.rewardValue ? ( + + {formatAmount(Number(promotedCampaign.rewardValue), 0)}{' '} + {promotedCampaign.rewardText} + + ) : ( + + {promotedCampaign.rewardText} + + )} + + & Safe points - - Prize pool - - - 600,000 MORPHO - - & Safe points - - Learn more - {' '} - and participate in campaign: + Participate in campaign: - - Safe App - - or - `rgba(36, 112, 255, ${palette.action.hoverOpacity})`, - }, - }} - > - Morpho - + {safeAppUrl && ( + <> + + Safe App + + or + + )} + + {promotedCampaign.partnerUrl && ( + `rgba(255, 255, 255, ${palette.action.hoverOpacity})`, + }, + }} + > + {promotedCampaign.name} + + )} diff --git a/src/components/Points/styles.module.css b/src/components/Points/styles.module.css index 2c368085..ec45f802 100644 --- a/src/components/Points/styles.module.css +++ b/src/components/Points/styles.module.css @@ -51,3 +51,15 @@ .progressBar { background: none; } + +.prizePoolBox { + border-radius: 6px; + border: 1px solid var(--mui-palette-border-light); + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + margin-top: 32px; + padding: 16px 0px; +} diff --git a/src/components/Sep5DeadlineChip/index.tsx b/src/components/Sep5DeadlineChip/index.tsx deleted file mode 100644 index 85679998..00000000 --- a/src/components/Sep5DeadlineChip/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { SvgIcon } from '@mui/material' -import type { TypographyProps } from '@mui/material' -import type { ReactElement } from 'react' - -import ClockIcon from '@/public/images/clock.svg' -import { TypographyChip } from '../TypographyChip' -import { SEP5_EXPIRATION_DATE } from '@/config/constants' - -export const Sep5DeadlineChip = (props: TypographyProps): ReactElement => { - return ( - - - Until {SEP5_EXPIRATION_DATE} - - ) -} diff --git a/src/components/Sep5InfoBox/index.tsx b/src/components/Sep5InfoBox/index.tsx deleted file mode 100644 index 9859e245..00000000 --- a/src/components/Sep5InfoBox/index.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { Tooltip, Typography } from '@mui/material' -import { formatEther } from 'ethers/lib/utils' -import type { ReactElement } from 'react' - -import { formatAmount } from '@/utils/formatters' -import { ExternalLink } from '@/components/ExternalLink' -import { InfoBox } from '@/components/InfoBox' -import { Sep5DeadlineChip } from '@/components/Sep5DeadlineChip' -import { useSafeTokenAllocation } from '@/hooks/useSafeTokenAllocation' -import { getVestingTypes } from '@/utils/vesting' -import { SEP5_EXPIRATION_DATE, SEP5_PROPOSAL_URL } from '@/config/constants' - -export const Sep5InfoBox = (): ReactElement | null => { - const { data: allocation } = useSafeTokenAllocation() - const { sep5Vesting } = getVestingTypes(allocation?.vestingData || []) - - if (!sep5Vesting) { - return null - } - - return ( - -
- - As a result of{' '} - - SEP #5 - - , you qualify for - - {formatAmount(formatEther(sep5Vesting.amount), 2)} SAFE -
- - - - - -
- ) -} diff --git a/src/hooks/useCampaigns.ts b/src/hooks/useCampaigns.ts index 7bb0a470..db3bd9a3 100644 --- a/src/hooks/useCampaigns.ts +++ b/src/hooks/useCampaigns.ts @@ -18,6 +18,12 @@ export type Campaign = { description: string maxPoints: string }[] + rewardValue: string | null + rewardText: string | null + iconUrl: string | null + safeAppUrl: string | null + partnerUrl: string | null + isPromoted: boolean } const PAGE_SIZE = 5 diff --git a/src/utils/airdrop.ts b/src/utils/airdrop.ts index 869380c6..2742044a 100644 --- a/src/utils/airdrop.ts +++ b/src/utils/airdrop.ts @@ -1,9 +1,6 @@ import { BigNumber } from 'ethers' import { parseEther } from 'ethers/lib/utils' -import { AIRDROP_TAGS } from '@/config/constants' -import { useSafeTokenAllocation } from '@/hooks/useSafeTokenAllocation' - const MAX_UINT128 = BigNumber.from('0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF') /** @@ -64,13 +61,3 @@ export const splitAirdropAmounts = ({ amountInWei.sub(userAndSep5AndInvestor).toString(), ] } - -export const canRedeemSep5Airdrop = (allocation: ReturnType['data']): boolean => { - const sep5Allocation = allocation?.vestingData.find(({ tag }) => tag === AIRDROP_TAGS.SEP5) - - if (!sep5Allocation) { - return false - } - - return !sep5Allocation.isRedeemed && !sep5Allocation.isExpired -}