diff --git a/package.json b/package.json index 27ccf07..f5af2cf 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "safe-dao-governance-app", "homepage": "https://github.com/safe-global/safe-dao-governance-app", "license": "GPL-3.0", - "version": "2.1.0", + "version": "2.2.0", "scripts": { "build": "next build && next export", "lint": "tsc && next lint", diff --git a/src/components/Points/ActivityPointsFeed.tsx b/src/components/Points/ActivityPointsFeed.tsx index 54f7959..c28f3e5 100644 --- a/src/components/Points/ActivityPointsFeed.tsx +++ b/src/components/Points/ActivityPointsFeed.tsx @@ -2,44 +2,83 @@ import { GLOBAL_CAMPAIGN_IDS } from '@/config/constants' import { Campaign } from '@/hooks/useCampaigns' import { useChainId } from '@/hooks/useChainId' import { useOwnCampaignRank } from '@/hooks/useLeaderboard' -import { formatDatetime } from '@/utils/date' +import { formatDate } from '@/utils/date' import { Divider, Skeleton, Stack, Typography } from '@mui/material' import Box from '@mui/material/Box' -import { useEffect, useMemo, useState } from 'react' +import { ReactNode, useEffect, useMemo, useState } from 'react' import PointsCounter from '../PointsCounter' import Barcode from '@/public/images/horizontal_barcode.svg' import css from './styles.module.css' +import { useLatestCampaignUpdate } from '@/hooks/useLatestCampaignUpdate' + +const BorderedBox = ({ children }: { children: ReactNode }) => { + return ( + `1px solid ${palette.border.light}`, + borderRadius: '6px', + p: '24px 32px', + display: 'flex', + flexDirection: 'column', + gap: 2, + position: 'relative', + }} + > + {children} + + ) +} + const HiddenValue = () => ( ) +const DataWrapper = ({ children }: { children: ReactNode }) => { + return ( + + {children} + + ) +} + export const ActivityPointsFeed = ({ campaign }: { campaign?: Campaign }) => { const { data: ownEntry, isLoading } = useOwnCampaignRank(campaign?.resourceId) + const { data: latestUpdate, isLoading: isLatestUpdateLoading } = useLatestCampaignUpdate(campaign?.resourceId) const data = useMemo(() => { return { - activityPoints: ownEntry?.totalPoints ?? 0, - boostedPoints: ownEntry ? ownEntry.totalBoostedPoints - ownEntry.totalPoints : 0, - totalPoints: ownEntry?.totalBoostedPoints ?? 0, + activityPoints: latestUpdate?.totalPoints ?? 0, + boostedPoints: latestUpdate ? latestUpdate.totalBoostedPoints - latestUpdate.totalPoints : 0, + totalPoints: latestUpdate?.totalBoostedPoints ?? 0, + overallPoints: ownEntry?.totalBoostedPoints ?? 0, } - }, [ownEntry]) + }, [latestUpdate, ownEntry]) const [showBoostPoints, setShowBoostPoints] = useState(false) const [showTotalPoints, setShowTotalPoints] = useState(false) + const [showOverallPoints, setShowOverallPoints] = useState(false) useEffect(() => { - if (ownEntry !== undefined || !isLoading) { - const showBoostPointsTimeout = setTimeout(() => setShowBoostPoints(true), 1000) - const showTotalPointsTimeout = setTimeout(() => setShowTotalPoints(true), 2000) + if (isLoading || isLatestUpdateLoading) { + return + } + const showBoostPointsTimeout = setTimeout(() => setShowBoostPoints(true), 1000) + const showTotalPointsTimeout = setTimeout(() => setShowTotalPoints(true), 2000) + const showOverallPointsTimeout = setTimeout(() => setShowOverallPoints(true), 3000) - return () => { - clearTimeout(showBoostPointsTimeout) - clearTimeout(showTotalPointsTimeout) - } + return () => { + clearTimeout(showBoostPointsTimeout) + clearTimeout(showTotalPointsTimeout) + clearTimeout(showOverallPointsTimeout) } - }, [ownEntry, isLoading]) + }, [ownEntry, isLoading, isLatestUpdateLoading]) const chainId = useChainId() @@ -53,81 +92,120 @@ export const ActivityPointsFeed = ({ campaign }: { campaign?: Campaign }) => { if (isLoading) { return ( - `1px solid ${palette.border.light}`, - borderRadius: '6px', - p: '24px 32px', - display: 'flex', - flexDirection: 'column', - gap: 2, - position: 'relative', - }} - > - - Activity points - - - - Boost points - - - - - {!isGlobal && 'Campaign'} Total - - - - - + <> + + + Last drop + + + + + Activity points + + + + Boost points + + + + + {!isGlobal && 'Campaign'} Week total + + + + + {!isGlobal && 'Campaign'} Overall + + + + Your points are updated weekly. + + + + {!isGlobal && ( + + + Campaign total + + + + )} + ) } return ( - `1px solid ${palette.border.light}`, - borderRadius: '6px', - p: '24px 32px', - display: 'flex', - flexDirection: 'column', - gap: 2, - position: 'relative', - }} - > - - Activity points - - points - - - - Boost points - {showBoostPoints ? ( - - points - - ) : ( - - )} - - - - {!isGlobal && 'Campaign'} Total - {showTotalPoints ? ( - + <> + + + Your last drop + {latestUpdate && ( + + {formatDate(new Date(latestUpdate.startDate))} - {formatDate(new Date(latestUpdate.endDate))} + + )} + + + + Activity points + points - ) : ( - - )} - - - Last updated {formatDatetime(new Date(campaign.lastUpdated))} - - - + + + Boost points + {showBoostPoints ? ( + + points + + ) : ( + + )} + + + + {!isGlobal && 'Campaign'} Week total + {showTotalPoints ? ( + + points + + ) : ( + + )} + + + Points are updated weekly. + + + Last update: {formatDate(new Date(campaign.lastUpdated))} + + + + + {!isGlobal && ( + + + Campaign total + {showOverallPoints ? ( + + points + + ) : ( + + )} + + + )} + ) } diff --git a/src/components/Points/CampaignTabs.tsx b/src/components/Points/CampaignTabs.tsx index 480bda6..6229c34 100644 --- a/src/components/Points/CampaignTabs.tsx +++ b/src/components/Points/CampaignTabs.tsx @@ -1,18 +1,44 @@ -import * as React from 'react' import Tabs from '@mui/material/Tabs' import Tab from '@mui/material/Tab' -import { Box, Chip, Typography, useMediaQuery } from '@mui/material' +import { Box, Chip, Tooltip, Typography } from '@mui/material' import { useTheme } from '@mui/material/styles' +import css from './styles.module.css' const CAMPAIGN_TABS = [ { - label: 'Global', + label: ( + + + palette.primary.main, + minWidth: '6px', + minHeight: '6px', + flexShrink: 0, + marginRight: 1, + }} + /> + + Global + + ), disabled: false, }, { label: ( - - Campaigns + + palette.text.disabled, + minWidth: '6px', + minHeight: '6px', + flexShrink: 0, + marginRight: 1, + }} + /> + Campaigns ), disabled: true, @@ -21,19 +47,22 @@ const CAMPAIGN_TABS = [ const CampaignTabs = ({ onChange, selectedTabIdx }: { onChange: (tab: number) => void; selectedTabIdx: number }) => { const theme = useTheme() - const isSmallScreen = useMediaQuery(theme.breakpoints.down('lg')) return ( onChange(value)} > {CAMPAIGN_TABS.map((tab, tabIdx) => ( - + ))} diff --git a/src/components/Points/TotalPoints.tsx b/src/components/Points/TotalPoints.tsx new file mode 100644 index 0000000..57fe11b --- /dev/null +++ b/src/components/Points/TotalPoints.tsx @@ -0,0 +1,37 @@ +import { useGlobalCampaignId } from '@/hooks/useGlobalCampaignId' +import { useOwnCampaignRank } from '@/hooks/useLeaderboard' +import { InfoOutlined } from '@mui/icons-material' +import { Box, Skeleton, Stack, Tooltip, Typography } from '@mui/material' +import { useMemo } from 'react' +import PaperContainer from '../PaperContainer' +import PointsCounter from '../PointsCounter' + +export const TotalPoints = () => { + const globalCampaignId = useGlobalCampaignId() + const ownRank = useOwnCampaignRank(globalCampaignId) + + const totalPoints = useMemo( + () => (ownRank.isLoading ? undefined : ownRank.data?.totalBoostedPoints ?? 0), + [ownRank.data?.totalBoostedPoints, ownRank.isLoading], + ) + + return ( + + + {totalPoints !== undefined ? ( + + ) : ( + + + + )} + + Your total points + + palette.text.secondary }} /> + + + + + ) +} diff --git a/src/components/Points/index.tsx b/src/components/Points/index.tsx index 69d75ea..7d9cafb 100644 --- a/src/components/Points/index.tsx +++ b/src/components/Points/index.tsx @@ -1,8 +1,8 @@ import { GLOBAL_CAMPAIGN_IDS } from '@/config/constants' import { useCampaignInfo, useCampaignPage } from '@/hooks/useCampaigns' import { useChainId } from '@/hooks/useChainId' -import { Grid, Typography, Stack, Box } from '@mui/material' -import { useEffect, useState } from 'react' +import { Grid, Typography, Stack, Box, SvgIcon, Divider, useMediaQuery } from '@mui/material' +import { useEffect, useMemo, useState } from 'react' import { ExternalLink } from '../ExternalLink' import PaperContainer from '../PaperContainer' import SafePassDisclaimer from '../SafePassDisclaimer' @@ -11,7 +11,10 @@ import { CampaignInfo } from './CampaignInfo' import { CampaignLeaderboard } from './CampaignLeaderboard' import { CampaignSelector } from './CampaignSelector' import CampaignTabs from './CampaignTabs' +import StarIcon from '@/public/images/leaderboard-title-star.svg' import css from './styles.module.css' +import { TotalPoints } from './TotalPoints' +import { useTheme } from '@mui/material/styles' const Points = () => { const campaignPage = useCampaignPage(20) @@ -41,6 +44,13 @@ const Points = () => { } } + const isGlobalCampaign = useMemo( + () => globalCampaign?.resourceId === selectedCampaignId, + [globalCampaign?.resourceId, selectedCampaignId], + ) + const theme = useTheme() + const isSmallScreen = useMediaQuery(theme.breakpoints.down('lg')) + return ( @@ -52,8 +62,26 @@ const Points = () => { + {isSmallScreen && } - + + {!isSmallScreen && } + + + Points activity feed + + + {!isSmallScreen && ( + + Earn points by engaging with Safe and our campaign partners. Depending on the campaign, you'll + be rewarded for various activities suggested by our partners. You can also earn points for regular + Safe activities. + + )} + + + + { minHeight="60px" > - - Points activity feed + + {isGlobalCampaign ? 'Global' : campaign?.name} - Your points are updated weekly. {selectedTab > 0 && ( { + {!isSmallScreen && } diff --git a/src/components/Points/styles.module.css b/src/components/Points/styles.module.css index 23cfa8c..d977f6a 100644 --- a/src/components/Points/styles.module.css +++ b/src/components/Points/styles.module.css @@ -18,3 +18,12 @@ transform: translate(-50%); pointer-events: none; } + +.comingSoon { + background-color: #303033; + border-radius: 4px; + padding: 4px 8px; + margin-left: 8px; + border: none; + font-weight: 400; +} diff --git a/src/components/TokenLocking/ActivityRewardsInfo.tsx b/src/components/TokenLocking/ActivityRewardsInfo.tsx index 51352c9..9c3ecd5 100644 --- a/src/components/TokenLocking/ActivityRewardsInfo.tsx +++ b/src/components/TokenLocking/ActivityRewardsInfo.tsx @@ -75,7 +75,7 @@ export const ActivityRewardsInfo = () => { description="Lock your tokens early to increase your earning power. The earlier and more you lock, the bigger your points multiplier. Geographic & other limitations apply (see disclaimer below)." /> diff --git a/src/hooks/useLatestCampaignUpdate.ts b/src/hooks/useLatestCampaignUpdate.ts new file mode 100644 index 0000000..582233d --- /dev/null +++ b/src/hooks/useLatestCampaignUpdate.ts @@ -0,0 +1,51 @@ +import { useAddress } from './useAddress' +import { PaginatedResult, useGatewayBaseUrl } from './useGatewayBaseUrl' +import useSWR from 'swr' +import { useMemo } from 'react' + +type CampaignUpdate = { + startDate: string + endDate: string + holder: string + boost: number + totalPoints: number + totalBoostedPoints: number +} + +export const useLatestCampaignUpdate = (resourceId: string | undefined) => { + const address = useAddress() + const gatewayBaseUrl = useGatewayBaseUrl() + + const getKey = (resourceId: string | undefined) => { + if (!resourceId || !address) { + return null + } + // Load newest page only + return `${gatewayBaseUrl}/v1/community/campaigns/${resourceId}/activities/?holder=${address}` + } + + const { data, isLoading } = useSWR( + getKey(resourceId), + async (url: string) => { + return await fetch(url).then((resp) => { + if (resp.ok) { + return resp.json() as Promise> + } else { + throw new Error('Error fetching campaign update.') + } + }) + }, + { + errorRetryCount: 1, + }, + ) + + // Only return newest entry + const newestUpdate = useMemo(() => { + if (data && data.results.length > 0) { + return data.results[0] + } + }, [data]) + + return { data: newestUpdate, isLoading } +} diff --git a/src/hooks/useLeaderboard.ts b/src/hooks/useLeaderboard.ts index 50b0628..22c1805 100644 --- a/src/hooks/useLeaderboard.ts +++ b/src/hooks/useLeaderboard.ts @@ -72,7 +72,7 @@ export const useOwnCampaignRank = (resourceId: string | undefined) => { const gatewayBaseUrl = useGatewayBaseUrl() const getKey = (resourceId: string | undefined) => { - if (!resourceId) { + if (!resourceId || !address) { return null } // Load next page