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