{heading}
-
+
{description}
diff --git a/apps/frontend/src/app/5_pages/LandingPage/components/QuickLaunch/QuickLaunch.tsx b/apps/frontend/src/app/5_pages/LandingPage/components/QuickLaunch/QuickLaunch.tsx
index 23ba149db..452cb46e0 100644
--- a/apps/frontend/src/app/5_pages/LandingPage/components/QuickLaunch/QuickLaunch.tsx
+++ b/apps/frontend/src/app/5_pages/LandingPage/components/QuickLaunch/QuickLaunch.tsx
@@ -4,8 +4,8 @@ import { t } from 'i18next';
import { useNavigate } from 'react-router-dom';
import { SupportedTokens } from '@sovryn/contracts';
-import { Button, ButtonStyle, Paragraph } from '@sovryn/ui';
+import { CTA } from '../../../../2_molecules/CTA/CTA';
import borrowBg from '../../../../../assets/images/QuickLaunch/borrow_bg.svg';
import earnBg from '../../../../../assets/images/QuickLaunch/earn_bg.svg';
import lendBg from '../../../../../assets/images/QuickLaunch/lend_bg.svg';
@@ -75,30 +75,14 @@ export const QuickLaunch: FC = () => {
return (
{options.map((option, index) => (
-
-
-
-
-
-
+
))}
);
diff --git a/apps/frontend/src/app/5_pages/LandingPage/components/TitleSection/TitleSection.tsx b/apps/frontend/src/app/5_pages/LandingPage/components/TitleSection/TitleSection.tsx
index eb82f72f3..d31000671 100644
--- a/apps/frontend/src/app/5_pages/LandingPage/components/TitleSection/TitleSection.tsx
+++ b/apps/frontend/src/app/5_pages/LandingPage/components/TitleSection/TitleSection.tsx
@@ -6,6 +6,7 @@ import { Trans } from 'react-i18next';
import { Button, ButtonStyle } from '@sovryn/ui';
import { translations } from '../../../../../locales/i18n';
+import { scrollToElement } from '../../../../../utils/helpers';
const pageTranslations = translations.landingPage.titleSection;
@@ -15,12 +16,7 @@ type TitleSectionProps = {
export const TitleSection: FC
= ({ ctaRef }) => {
const ctaClickHandler = useCallback(() => {
- if (ctaRef.current) {
- ctaRef.current.scrollIntoView({
- behavior: 'smooth',
- block: 'start',
- });
- }
+ scrollToElement(ctaRef);
}, [ctaRef]);
return (
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/LeaderboardPage.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/LeaderboardPage.tsx
new file mode 100644
index 000000000..0ed5f8999
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/LeaderboardPage.tsx
@@ -0,0 +1,27 @@
+import React, { FC, useRef, useState } from 'react';
+
+import { CTALinks } from './components/CTALinks/CTALinks';
+import { Intro } from './components/Intro/Intro';
+import { Leaderboard } from './components/Leaderboard/Leaderboard';
+import { PointsSection } from './components/PointsSection/PointsSection';
+
+const LeaderboardPage: FC = () => {
+ const tableRef = useRef(null);
+ const pointsSectionRef = useRef(null);
+ const [tabIndex, setTabIndex] = useState(0);
+
+ return (
+
+ );
+};
+
+export default LeaderboardPage;
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/CTALinks/CTALinks.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/CTALinks/CTALinks.tsx
new file mode 100644
index 000000000..3491a9541
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/CTALinks/CTALinks.tsx
@@ -0,0 +1,81 @@
+import React, {
+ Dispatch,
+ FC,
+ RefObject,
+ SetStateAction,
+ useCallback,
+ useMemo,
+} from 'react';
+
+import { t } from 'i18next';
+import { useNavigate } from 'react-router-dom';
+
+import { CTA } from '../../../../2_molecules/CTA/CTA';
+import socialBg from '../../../../../assets/images/Leaderboard/social.svg';
+import stakeBg from '../../../../../assets/images/Leaderboard/stake.svg';
+import tradeBg from '../../../../../assets/images/Leaderboard/trade.svg';
+import { translations } from '../../../../../locales/i18n';
+import { scrollToElement } from '../../../../../utils/helpers';
+
+const baseTranslation = translations.leaderboardPage.ctaLinksSection;
+
+type CTALinksProps = {
+ tableRef: RefObject;
+ setTabIndex: Dispatch>;
+};
+
+export const CTALinks: FC = ({ tableRef, setTabIndex }) => {
+ const navigate = useNavigate();
+
+ const socialClickHandler = useCallback(() => {
+ if (tableRef.current) {
+ scrollToElement(tableRef);
+ setTabIndex(2);
+ }
+ }, [setTabIndex, tableRef]);
+
+ const options = useMemo(
+ () => [
+ {
+ title: t(baseTranslation.stake.title),
+ description: t(baseTranslation.stake.description),
+ action: t(baseTranslation.stake.cta),
+ onClick: () => navigate('/earn/staking'),
+ backgroundImage: stakeBg,
+ },
+ {
+ title: t(baseTranslation.trade.title),
+ description: t(baseTranslation.trade.description),
+ action: t(baseTranslation.trade.cta),
+ onClick: () =>
+ window.open('https://app.babelfish.money/convert', '_blank'),
+ backgroundImage: tradeBg,
+ },
+ {
+ title: t(baseTranslation.social.title),
+ description: t(baseTranslation.social.description),
+ action: t(baseTranslation.social.cta),
+ onClick: socialClickHandler,
+ backgroundImage: socialBg,
+ },
+ ],
+ [navigate, socialClickHandler],
+ );
+
+ return (
+
+ {options.map(
+ ({ backgroundImage, title, description, action, onClick }, index) => (
+
+ ),
+ )}
+
+ );
+};
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Intro/Intro.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Intro/Intro.tsx
new file mode 100644
index 000000000..50662bd48
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Intro/Intro.tsx
@@ -0,0 +1,66 @@
+import React, { FC, RefObject, useCallback } from 'react';
+
+import { t } from 'i18next';
+import { Trans } from 'react-i18next';
+
+import { Button, ButtonStyle } from '@sovryn/ui';
+
+import { POWA_LINK } from '../../../../../constants/links';
+import { translations } from '../../../../../locales/i18n';
+import { scrollToElement } from '../../../../../utils/helpers';
+
+const baseTranslation = translations.leaderboardPage;
+
+type IntroProps = {
+ pointsSectionRef: RefObject;
+};
+
+export const Intro: FC = ({ pointsSectionRef }) => {
+ const primaryCtaClickHandler = useCallback(() => {
+ scrollToElement(pointsSectionRef);
+ }, [pointsSectionRef]);
+
+ return (
+
+
+ {t(baseTranslation.title)}
+
+
+ {t(baseTranslation.subtitle)}
+
+
+ Bitcoin OS]}
+ />
+
+
+ {t(baseTranslation.description2)}
+
+
+
+
+
+
+
+
+
+ {t(baseTranslation.ctaLinksSection.title)}
+
+
+ {t(baseTranslation.ctaLinksSection.subtitle)}
+
+
+
+ );
+};
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/Leaderboard.constants.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/Leaderboard.constants.tsx
new file mode 100644
index 000000000..7d433d555
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/Leaderboard.constants.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+
+import { t } from 'i18next';
+
+import { translations } from '../../../../../locales/i18n';
+import { SocialLeaderboard } from './components/SocialLeaderboard/SocialLeaderboard';
+import { StakingLeaderboard } from './components/StakingLeaderboard/StakingLeaderboard';
+import { TradingLeaderboard } from './components/TradingLeaderboard/TradingLeaderboard';
+
+const ACTIVE_CLASSNAME = 'border-t-primary-30';
+
+export const TAB_ITEMS = [
+ {
+ label: t(translations.leaderboardPage.tables.trading.tabTitle),
+ content: ,
+ activeClassName: ACTIVE_CLASSNAME,
+ dataAttribute: 'trading',
+ },
+ {
+ label: t(translations.leaderboardPage.tables.staking.tabTitle),
+ content: ,
+ activeClassName: ACTIVE_CLASSNAME,
+ dataAttribute: 'staking',
+ },
+ {
+ label: t(translations.leaderboardPage.tables.social.tabTitle),
+ content: ,
+ activeClassName: ACTIVE_CLASSNAME,
+ dataAttribute: 'social',
+ },
+];
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/Leaderboard.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/Leaderboard.tsx
new file mode 100644
index 000000000..340cd4094
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/Leaderboard.tsx
@@ -0,0 +1,25 @@
+import React, { Dispatch, FC, RefObject, SetStateAction } from 'react';
+
+import { Tabs } from '@sovryn/ui';
+
+import { TAB_ITEMS } from './Leaderboard.constants';
+
+type LeaderboardProps = {
+ index: number;
+ setIndex: Dispatch>;
+ tableRef: RefObject;
+};
+
+export const Leaderboard: FC = ({
+ index,
+ setIndex,
+ tableRef,
+}) => (
+
+);
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/Leaderboard.types.ts b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/Leaderboard.types.ts
new file mode 100644
index 000000000..826a8b0e7
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/Leaderboard.types.ts
@@ -0,0 +1,19 @@
+export enum UserBadges {
+ EARLY_USER = 'early_user',
+ GENESIS = 'genesis',
+ ORIGIN = 'origin',
+ NFT = 'nft',
+ TOP_IMPORTER_DAY = 'top_importer_day',
+ TOP_IMPORTER_WEEK = 'top_importer_week',
+ VOTER = 'voter',
+ TOP_STAKER_DAY = 'top_staker_day',
+ TOP_STAKER_WEEK = 'top_staker_week',
+ WALLET = 'wallet',
+}
+
+export type User = {
+ rank: string;
+ wallet: string;
+ points: string;
+ badges: UserBadges[];
+};
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/Badges/Badges.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/Badges/Badges.tsx
new file mode 100644
index 000000000..ec19e2a89
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/Badges/Badges.tsx
@@ -0,0 +1,22 @@
+import React, { FC } from 'react';
+
+import { Badge } from '@sovryn/ui';
+
+import { User } from '../../Leaderboard.types';
+import { getBadgeDetails } from './Badges.utils';
+
+type BadgesProps = {
+ user: User;
+};
+
+export const Badges: FC = ({ user }) => (
+ <>
+ {user.badges.map(badge => {
+ const { title, style } = getBadgeDetails(badge);
+
+ return (
+
+ );
+ })}
+ >
+);
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/Badges/Badges.utils.ts b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/Badges/Badges.utils.ts
new file mode 100644
index 000000000..9e4443a26
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/Badges/Badges.utils.ts
@@ -0,0 +1,63 @@
+import { t } from 'i18next';
+
+import { BadgeStyle } from '@sovryn/ui';
+
+import { translations } from '../../../../../../../locales/i18n';
+import { UserBadges } from '../../Leaderboard.types';
+
+const baseTranslation = translations.leaderboardPage.tables.baseTable.badges;
+
+export const getBadgeDetails = (badge: UserBadges) => {
+ switch (badge) {
+ case UserBadges.EARLY_USER:
+ return {
+ title: t(baseTranslation.earlyUser),
+ style: BadgeStyle.teal,
+ };
+ case UserBadges.GENESIS:
+ return {
+ title: t(baseTranslation.genesis),
+ style: BadgeStyle.teal,
+ };
+ case UserBadges.NFT:
+ return {
+ title: t(baseTranslation.nft),
+ style: BadgeStyle.teal,
+ };
+ case UserBadges.ORIGIN:
+ return {
+ title: t(baseTranslation.origin),
+ style: BadgeStyle.teal,
+ };
+ case UserBadges.TOP_IMPORTER_DAY:
+ return {
+ title: t(baseTranslation.topImporterDay),
+ style: BadgeStyle.grayLight,
+ };
+ case UserBadges.TOP_IMPORTER_WEEK:
+ return {
+ title: t(baseTranslation.topImporterWeek),
+ style: BadgeStyle.grayLight,
+ };
+ case UserBadges.VOTER:
+ return {
+ title: t(baseTranslation.voter),
+ style: BadgeStyle.teal,
+ };
+ case UserBadges.TOP_STAKER_DAY:
+ return {
+ title: t(baseTranslation.topStakerDay),
+ style: BadgeStyle.brown,
+ };
+ case UserBadges.TOP_STAKER_WEEK:
+ return {
+ title: t(baseTranslation.topStakerWeek),
+ style: BadgeStyle.brown,
+ };
+ case UserBadges.WALLET:
+ return {
+ title: t(baseTranslation.wallet),
+ style: BadgeStyle.gray,
+ };
+ }
+};
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/BaseTable.constants.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/BaseTable.constants.tsx
new file mode 100644
index 000000000..be74c18e6
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/BaseTable.constants.tsx
@@ -0,0 +1,51 @@
+import React from 'react';
+
+import { t } from 'i18next';
+
+import { prettyTx } from '@sovryn/ui';
+
+import { AmountRenderer } from '../../../../../../2_molecules/AmountRenderer/AmountRenderer';
+import { translations } from '../../../../../../../locales/i18n';
+import { User } from '../../Leaderboard.types';
+import { Badges } from '../Badges/Badges';
+
+export const TRADING_LEADERBOARD_URL =
+ 'https://redash.sovryn.app/api/queries/544/results.json?api_key=52Jy2PGF5HZVye97NCG9e8nNDDPZ1iFo65Hfo1sk';
+
+export const STAKING_LEADERBOARD_URL =
+ 'https://redash.sovryn.app/api/queries/545/results.json?api_key=sjTLMq48pU0yHJlDBWrFiQQS2x0jTtk7BChYTC8J';
+
+export const PAGE_SIZE = 50;
+
+export const MAXIMUM_USERS_TO_SHOW = 500;
+
+export const COLUMNS_CONFIG = (isSingleUser: boolean) => [
+ {
+ id: '',
+ title: isSingleUser
+ ? t(translations.leaderboardPage.tables.baseTable.yourPosition)
+ : '',
+ cellRenderer: (row: User) => row.rank,
+ },
+ {
+ id: isSingleUser ? '' : 'wallet',
+ title: isSingleUser
+ ? ''
+ : t(translations.leaderboardPage.tables.baseTable.participant),
+ cellRenderer: (row: User) => (
+
+
{prettyTx(row.wallet)}
+
+
+
+
+ ),
+ },
+ {
+ id: 'points',
+ title: t(translations.leaderboardPage.tables.baseTable.points),
+ cellRenderer: (row: User) => (
+
+ ),
+ },
+];
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/BaseTable.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/BaseTable.tsx
new file mode 100644
index 000000000..484bc588c
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/BaseTable.tsx
@@ -0,0 +1,84 @@
+import React, { FC, useMemo } from 'react';
+
+import { t } from 'i18next';
+
+import { Table, Pagination } from '@sovryn/ui';
+
+import { useAccount } from '../../../../../../../hooks/useAccount';
+import { useHandlePagination } from '../../../../../../../hooks/useHandlePagination';
+import { translations } from '../../../../../../../locales/i18n';
+import {
+ COLUMNS_CONFIG,
+ PAGE_SIZE,
+ STAKING_LEADERBOARD_URL,
+ TRADING_LEADERBOARD_URL,
+} from './BaseTable.constants';
+import { TableType } from './BaseTable.types';
+import { generateRowTitle } from './BaseTable.utils';
+import { ConnectWalletMessage } from './components/ConnectWalletMessage/ConnectWalletMessage';
+import { useGetData } from './hooks/useGetData';
+
+type BaseTableProps = {
+ type: TableType;
+ tableSubtitle?: string;
+};
+
+export const BaseTable: FC = ({ type, tableSubtitle }) => {
+ const { account } = useAccount();
+
+ const isStakingTable = useMemo(() => type === TableType.Staking, [type]);
+
+ const dataUrl = useMemo(
+ () => (isStakingTable ? STAKING_LEADERBOARD_URL : TRADING_LEADERBOARD_URL),
+ [isStakingTable],
+ );
+ const { loading, users, connectedWalletRow } = useGetData(dataUrl);
+
+ const { page, onPageChange, paginatedItems, isNextButtonDisabled } =
+ useHandlePagination(users, PAGE_SIZE);
+
+ return (
+
+
+
+ ) : (
+ t(translations.leaderboardPage.tables.participantNotFound)
+ )
+ }
+ isLoading={!!account && loading}
+ flatMode={true}
+ />
+
+ {tableSubtitle && (
+
{tableSubtitle}
+ )}
+
+
+ );
+};
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/BaseTable.types.ts b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/BaseTable.types.ts
new file mode 100644
index 000000000..372068ff0
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/BaseTable.types.ts
@@ -0,0 +1,4 @@
+export enum TableType {
+ Staking = 'staking',
+ Trading = 'trading',
+}
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/BaseTable.utils.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/BaseTable.utils.tsx
new file mode 100644
index 000000000..452f2cb8b
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/BaseTable.utils.tsx
@@ -0,0 +1,71 @@
+import React from 'react';
+
+import { prettyTx } from '@sovryn/ui';
+
+import { AmountRenderer } from '../../../../../../2_molecules/AmountRenderer/AmountRenderer';
+import { User, UserBadges } from '../../Leaderboard.types';
+import { Badges } from '../Badges/Badges';
+
+export const parseBadges = (data: any): UserBadges[] => {
+ let badges: UserBadges[] = [];
+
+ if (data.early_user === 1) {
+ badges.push(UserBadges.EARLY_USER);
+ }
+
+ if (data.genesis === 1) {
+ badges.push(UserBadges.GENESIS);
+ }
+
+ if (data.origin === 1) {
+ badges.push(UserBadges.ORIGIN);
+ }
+
+ if (data.nft === 1) {
+ badges.push(UserBadges.NFT);
+ }
+
+ if (data.top_importer_day === 1) {
+ badges.push(UserBadges.TOP_IMPORTER_DAY);
+ }
+
+ if (data.top_importer_week === 1) {
+ badges.push(UserBadges.TOP_IMPORTER_WEEK);
+ }
+
+ if (data.voter === 1) {
+ badges.push(UserBadges.VOTER);
+ }
+
+ if (data.top_staker_day === 1) {
+ badges.push(UserBadges.TOP_STAKER_DAY);
+ }
+
+ if (data.top_staker_week === 1) {
+ badges.push(UserBadges.TOP_STAKER_WEEK);
+ }
+
+ // Do not delete, may be used in future
+ // if (data.wallet === 1) {
+ // badges.push(UserBadges.WALLET);
+ // }
+
+ return badges;
+};
+
+export const generateRowTitle = (row: User) => (
+
+
{row.rank}
+
+
+
{prettyTx(row.wallet)}
+
+
+
+
+
+
+
+);
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/components/ConnectWalletMessage/ConnectWalletMessage.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/components/ConnectWalletMessage/ConnectWalletMessage.tsx
new file mode 100644
index 000000000..cc202e182
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/components/ConnectWalletMessage/ConnectWalletMessage.tsx
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import { t } from 'i18next';
+
+import { Button } from '@sovryn/ui';
+
+import { useWalletConnect } from '../../../../../../../../../hooks';
+import { translations } from '../../../../../../../../../locales/i18n';
+
+export const ConnectWalletMessage = () => {
+ const { connectWallet } = useWalletConnect();
+
+ return (
+
+
+ {t(translations.leaderboardPage.tables.connectWalletText)}
+
+
+
+ );
+};
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/hooks/useGetData.ts b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/hooks/useGetData.ts
new file mode 100644
index 000000000..92967fa39
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/BaseTable/hooks/useGetData.ts
@@ -0,0 +1,61 @@
+import { useCallback, useEffect, useMemo, useState } from 'react';
+
+import { useAccount } from '../../../../../../../../hooks/useAccount';
+import { User } from '../../../Leaderboard.types';
+import { MAXIMUM_USERS_TO_SHOW } from '../BaseTable.constants';
+import { parseBadges } from '../BaseTable.utils';
+
+export const useGetData = (url: string) => {
+ const { account } = useAccount();
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(false);
+
+ const fetchData = useCallback(async () => {
+ setLoading(true);
+ const response = await fetch(url);
+
+ if (response.ok) {
+ const data = await response.json();
+
+ if (data) {
+ const result = data?.query_result?.data?.rows;
+
+ if (result?.length > 0) {
+ setUsers(
+ result.map(item => ({
+ rank: item.rank,
+ wallet: item.user,
+ points: item.points,
+ badges: parseBadges(item),
+ })),
+ );
+ }
+ }
+ }
+ setLoading(false);
+ }, [url]);
+
+ const connectedWalletRow = useMemo(() => {
+ if (account && users.length > 0) {
+ const user = users.find(
+ item => item.wallet.toLowerCase() === account.toLowerCase(),
+ );
+
+ if (user) {
+ return [user];
+ }
+ return [];
+ }
+ return [];
+ }, [account, users]);
+
+ useEffect(() => {
+ fetchData();
+ }, [fetchData]);
+
+ return {
+ loading,
+ users: users.slice(0, MAXIMUM_USERS_TO_SHOW),
+ connectedWalletRow,
+ };
+};
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/SocialLeaderboard/SocialLeaderboard.constants.ts b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/SocialLeaderboard/SocialLeaderboard.constants.ts
new file mode 100644
index 000000000..2024e43ad
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/SocialLeaderboard/SocialLeaderboard.constants.ts
@@ -0,0 +1,2 @@
+export const COMPETITION_URL = 'https://gleam.io/EK36C/influence-powa';
+export const LEADERBOARD_URL = 'https://gleam.io/EK36C/leaderboard';
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/SocialLeaderboard/SocialLeaderboard.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/SocialLeaderboard/SocialLeaderboard.tsx
new file mode 100644
index 000000000..05aa6e4c1
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/SocialLeaderboard/SocialLeaderboard.tsx
@@ -0,0 +1,31 @@
+import React, { FC } from 'react';
+
+import {
+ COMPETITION_URL,
+ LEADERBOARD_URL,
+} from './SocialLeaderboard.constants';
+
+export const SocialLeaderboard: FC = () => {
+ return (
+
+ );
+};
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/StakingLeaderboard/StakingLeaderboard.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/StakingLeaderboard/StakingLeaderboard.tsx
new file mode 100644
index 000000000..faff2cb14
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/StakingLeaderboard/StakingLeaderboard.tsx
@@ -0,0 +1,14 @@
+import React, { FC } from 'react';
+
+import { t } from 'i18next';
+
+import { translations } from '../../../../../../../locales/i18n';
+import { BaseTable } from '../BaseTable/BaseTable';
+import { TableType } from '../BaseTable/BaseTable.types';
+
+export const StakingLeaderboard: FC = () => (
+
+);
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/TradingLeaderboard/TradingLeaderboard.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/TradingLeaderboard/TradingLeaderboard.tsx
new file mode 100644
index 000000000..b11dad78e
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/Leaderboard/components/TradingLeaderboard/TradingLeaderboard.tsx
@@ -0,0 +1,8 @@
+import React, { FC } from 'react';
+
+import { BaseTable } from '../BaseTable/BaseTable';
+import { TableType } from '../BaseTable/BaseTable.types';
+
+export const TradingLeaderboard: FC = () => (
+
+);
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/PointsSection/PointsSection.module.css b/apps/frontend/src/app/5_pages/LeaderboardPage/components/PointsSection/PointsSection.module.css
new file mode 100644
index 000000000..3558ac4c0
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/PointsSection/PointsSection.module.css
@@ -0,0 +1,3 @@
+.tableRow {
+ @apply flex justify-between;
+}
diff --git a/apps/frontend/src/app/5_pages/LeaderboardPage/components/PointsSection/PointsSection.tsx b/apps/frontend/src/app/5_pages/LeaderboardPage/components/PointsSection/PointsSection.tsx
new file mode 100644
index 000000000..6debcae66
--- /dev/null
+++ b/apps/frontend/src/app/5_pages/LeaderboardPage/components/PointsSection/PointsSection.tsx
@@ -0,0 +1,102 @@
+import React, { FC, RefObject } from 'react';
+
+import classNames from 'classnames';
+import { t } from 'i18next';
+
+import { SimpleTable, SimpleTableRow } from '@sovryn/ui';
+
+import { translations } from '../../../../../locales/i18n';
+import styles from './PointsSection.module.css';
+
+const baseTranslation = translations.leaderboardPage.pointsSection;
+const tablesTranslation = translations.leaderboardPage.pointsSection.tables;
+
+type PointsSectionProps = {
+ pointsSectionRef: RefObject;
+};
+
+export const PointsSection: FC = ({ pointsSectionRef }) => (
+ <>
+
+
+ {t(baseTranslation.title)}
+
+
+ {t(baseTranslation.description)}
+
+
+
+
+
+
+ {t(tablesTranslation.trading.title)}
+
+
+
+
+
+
+
+
+
+
+ {t(tablesTranslation.staking.title)}
+
+
+
+
+
+
+
+
+
+ {t(tablesTranslation.social.title)}
+
+
+
+
+ >
+);
diff --git a/apps/frontend/src/app/5_pages/ProposalPage/components/ProposalInfo/ProposalInfo.tsx b/apps/frontend/src/app/5_pages/ProposalPage/components/ProposalInfo/ProposalInfo.tsx
index 52b547639..9450c1e9e 100644
--- a/apps/frontend/src/app/5_pages/ProposalPage/components/ProposalInfo/ProposalInfo.tsx
+++ b/apps/frontend/src/app/5_pages/ProposalPage/components/ProposalInfo/ProposalInfo.tsx
@@ -47,7 +47,17 @@ export const ProposalInfo: FC = ({ link, description }) => (
{t(pageTranslations.proposalText)}
-
+ (
+
+ {props.children}
+
+ ),
+ }}
+ className={styles.markdown}
+ remarkPlugins={[remarkGfm]}
+ >
{description}
diff --git a/apps/frontend/src/assets/images/Leaderboard/social.svg b/apps/frontend/src/assets/images/Leaderboard/social.svg
new file mode 100644
index 000000000..02fab93de
--- /dev/null
+++ b/apps/frontend/src/assets/images/Leaderboard/social.svg
@@ -0,0 +1,31 @@
+
diff --git a/apps/frontend/src/assets/images/Leaderboard/stake.svg b/apps/frontend/src/assets/images/Leaderboard/stake.svg
new file mode 100644
index 000000000..e9d2736e9
--- /dev/null
+++ b/apps/frontend/src/assets/images/Leaderboard/stake.svg
@@ -0,0 +1,68 @@
+
diff --git a/apps/frontend/src/assets/images/Leaderboard/trade.svg b/apps/frontend/src/assets/images/Leaderboard/trade.svg
new file mode 100644
index 000000000..ba80b183f
--- /dev/null
+++ b/apps/frontend/src/assets/images/Leaderboard/trade.svg
@@ -0,0 +1,181 @@
+
diff --git a/apps/frontend/src/constants/links.ts b/apps/frontend/src/constants/links.ts
index dcf38e244..9e4a12178 100644
--- a/apps/frontend/src/constants/links.ts
+++ b/apps/frontend/src/constants/links.ts
@@ -54,3 +54,5 @@ export const WIKI_LINKS = {
};
export const HELPDESK_LINK = 'https://help.sovryn.app/';
+
+export const POWA_LINK = 'https://sovryn.com/powa';
diff --git a/apps/frontend/src/hooks/useHandlePagination.ts b/apps/frontend/src/hooks/useHandlePagination.ts
new file mode 100644
index 000000000..121451231
--- /dev/null
+++ b/apps/frontend/src/hooks/useHandlePagination.ts
@@ -0,0 +1,35 @@
+import { useCallback, useMemo, useState } from 'react';
+
+import { DEFAULT_PAGE_SIZE } from '../constants/general';
+
+export const useHandlePagination = (
+ items: T[],
+ pageSize = DEFAULT_PAGE_SIZE,
+) => {
+ const [page, setPage] = useState(0);
+
+ const onPageChange = useCallback(
+ (value: number) => {
+ if (items?.length < pageSize && value > page) {
+ return;
+ }
+
+ setPage(value);
+ },
+ [items?.length, pageSize, page],
+ );
+
+ const paginatedItems = useMemo(() => {
+ if (items) {
+ return items.slice(page * pageSize, (page + 1) * pageSize);
+ }
+ return [];
+ }, [items, page, pageSize]);
+
+ const isNextButtonDisabled = useMemo(
+ () => items?.length <= pageSize || paginatedItems?.length < pageSize,
+ [items?.length, pageSize, paginatedItems?.length],
+ );
+
+ return { page, onPageChange, paginatedItems, isNextButtonDisabled };
+};
diff --git a/apps/frontend/src/locales/en/translations.json b/apps/frontend/src/locales/en/translations.json
index 991f1acb1..6c73bc417 100644
--- a/apps/frontend/src/locales/en/translations.json
+++ b/apps/frontend/src/locales/en/translations.json
@@ -106,7 +106,8 @@
},
"convert": "Convert",
"stake": "Stake",
- "bitocracy": "Bitocracy"
+ "bitocracy": "Bitocracy",
+ "bob": "Go BoB"
}
},
"footer": {
@@ -479,6 +480,12 @@
"title": "BUY SOV - Shape The future",
"description": "Buy SOV - Sovryn’s governance token. Stake to earn rewards and vote in Bitocracy.",
"cta": "Buy SOV"
+ },
+ "competition": {
+ "title": "YOU’VE GOT THE POWA",
+ "description": "Anyone using Sovryn already has points that will convert to POWA - a new bitcoin-token that allows an early use of what’s to come with the BitcoinOS launch. See how many points you’ve got:",
+ "cta": "Leaderboard",
+ "secondaryCta": "More about POWA"
}
},
"titleSection": {
@@ -852,6 +859,93 @@
}
},
+ "leaderboardPage": {
+ "headerLink": "POWA Points",
+ "headerDescription": "Earn Points, Earn POWA",
+ "title": "You’ve got the POWA!",
+ "subtitle": "Earn Points - get POWA",
+ "description1": "POWA is the most powaful Bitcoin token! Minted on an entirely new Bitcoin technology, POWA tokens allow early use of what's to come with the <0>BitcoinOS0> launch.",
+ "description2": "Stake, Trade, or use your Influence to earn Points and POWA. Connect your wallet to see how many points you already have!",
+ "primaryCta": "How to earn points",
+ "secondaryCta": "More about POWA",
+ "ctaLinksSection": {
+ "title": "1 Trillion POWA Prize Pool!",
+ "subtitle": "The more points you’ll have - the more POWA you’ll get!",
+ "stake": {
+ "title": "Stake",
+ "description": "The longer you stake, the more POWA you’ll get! Existing stakes earn 1 point for each Voting Power. New and re-stakes earn 10 points for each VP.",
+ "cta": "Stake SOV"
+ },
+ "trade": {
+ "title": "Trade",
+ "description": "Get 25 points per $1 value you bridge into Sovryn, 10 points per $1 deposit into the SOV or DLLR AMMs, and 1 point per $1 trade.",
+ "cta": "Deposit stablecoins"
+ },
+ "social": {
+ "title": "Influence",
+ "description": "Earn points for your social influence! Like, Share, and Tweet to be given the POWA!",
+ "cta": "Twitter quests"
+ }
+ },
+ "tables": {
+ "connectWalletText": "Connect your wallet to see how many points you’ve got.",
+ "participantNotFound": "Take any action on the dapp to start earning points",
+ "connectWalletCta": "Connect wallet",
+ "baseTable": {
+ "participant": "Participant",
+ "points": "Points",
+ "yourPosition": "Your position",
+ "badges": {
+ "earlyUser": "+5% Early user",
+ "genesis": "+5% Genesis",
+ "nft": "+5% NFT holder",
+ "origin": "+5% Origin",
+ "topImporterDay": "+10% Daily top importer",
+ "topImporterWeek": "+10% Weekly top importer",
+ "voter": "+5% Voter",
+ "topStakerDay": "+10% Daily top staker",
+ "topStakerWeek": "+10% Weekly top trader",
+ "wallet": "+100 points"
+ }
+ },
+ "trading": {
+ "tabTitle": "Trading"
+ },
+ "staking": {
+ "tabTitle": "Staking",
+ "tableSubtitle": "Please note that the system is currently undercounting restaking. This will be addressed in a coming update."
+ },
+ "social": {
+ "tabTitle": "Influence"
+ }
+ },
+ "pointsSection": {
+ "title": "Gain POWA!",
+ "description": "Actions on the Sovryn dApp and on Twitter earn you points. Points convert to POWA - the next Bitcoin token class.",
+ "tables": {
+ "trading": {
+ "title": "Trade",
+ "item1": "Provide liquidity for SOV or DLLR AMMs, per $1 value",
+ "item2": "Any AMM trade, per $1 value",
+ "item3": "Import stablecoins, per $1 value",
+ "item4": "Weekly top importer",
+ "item5": "Daily top importer"
+ },
+ "staking": {
+ "title": "Stake",
+ "item1": "New and re-stakes, 1VP",
+ "item2": "Existing stakes, 1 VP",
+ "item3": "Weekly top staker",
+ "item4": "Daily top staker"
+ },
+ "social": {
+ "title": "Influence",
+ "item1": "See Influence leaderboard for current quests points breakdown."
+ }
+ }
+ }
+ },
+
"fastBtc": {
"mainScreen": {
"title": "Funding",
diff --git a/apps/frontend/src/router.tsx b/apps/frontend/src/router.tsx
index 5efaab84d..1449edd65 100644
--- a/apps/frontend/src/router.tsx
+++ b/apps/frontend/src/router.tsx
@@ -61,6 +61,10 @@ const ProtocolDataPage = loadable(
() => import('./app/5_pages/ProtocolDataPage/ProtocolDataPage'),
);
+const LeaderboardPage = loadable(
+ () => import('./app/5_pages/LeaderboardPage/LeaderboardPage'),
+);
+
const routes = [
{
path: '/',
@@ -136,6 +140,10 @@ const routes = [
path: '/earn/staking',
element: ,
},
+ {
+ path: '/powa',
+ element: ,
+ },
{
path: '/stats',
element: ,
diff --git a/apps/frontend/src/utils/helpers.ts b/apps/frontend/src/utils/helpers.ts
index 7dcea7637..54f9988a0 100644
--- a/apps/frontend/src/utils/helpers.ts
+++ b/apps/frontend/src/utils/helpers.ts
@@ -1,3 +1,5 @@
+import { RefObject } from 'react';
+
import dayjs from 'dayjs';
import { BigNumber, providers } from 'ethers';
import resolveConfig from 'tailwindcss/resolveConfig';
@@ -207,3 +209,12 @@ export const renderTokenSymbol = (token: string) =>
export const generateNonce = () =>
BigNumber.from(Math.floor(Date.now() + Math.random() * 100));
+
+export const scrollToElement = (ref: RefObject) => {
+ if (ref.current) {
+ ref.current.scrollIntoView({
+ behavior: 'smooth',
+ block: 'start',
+ });
+ }
+};
diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md
index 337ca784d..e0fe382ed 100644
--- a/packages/ui/CHANGELOG.md
+++ b/packages/ui/CHANGELOG.md
@@ -1,5 +1,11 @@
# @sovryn/ui
+## 1.0.23
+
+### Patch Changes
+
+- 3d6870a1: SOV-3801: Add new icon, extend properties on Table component, prevent onClick events from propagating from Tooltip clicks
+
## 1.0.22
### Patch Changes
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 5d3e0c901..b1b64c10e 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@sovryn/ui",
- "version": "1.0.22",
+ "version": "1.0.23",
"main": "./dist/index.js",
"module": "./dist/index.esm.js",
"types": "./dist/typings.d.ts",
diff --git a/packages/ui/src/1_atoms/Accordion/Accordion.tsx b/packages/ui/src/1_atoms/Accordion/Accordion.tsx
index 8cca0e95d..0405aa3e3 100644
--- a/packages/ui/src/1_atoms/Accordion/Accordion.tsx
+++ b/packages/ui/src/1_atoms/Accordion/Accordion.tsx
@@ -23,6 +23,7 @@ export interface IAccordionProps {
onClick?: (toOpen: boolean) => void;
dataAttribute?: string;
style?: AccordionStyle;
+ flatMode?: boolean;
}
export const Accordion: FC = ({
@@ -35,10 +36,11 @@ export const Accordion: FC = ({
dataAttribute,
labelClassName,
style = AccordionStyle.primary,
+ flatMode,
}) => {
const onClickCallback = useCallback(
- () => !disabled && onClick?.(!open),
- [disabled, open, onClick],
+ () => !disabled && !flatMode && onClick?.(!open),
+ [disabled, flatMode, onClick, open],
);
return (
@@ -61,15 +63,17 @@ export const Accordion: FC = ({
label
)}
>
-
-
-
+ {!flatMode && (
+
+
+
+ )}
{open && (
= {
hideHeader?: boolean;
subtitleRenderer?: (row: RowType) => ReactNode;
expandedIndex?: number;
+ flatMode?: boolean;
};
export enum OrderDirection {
diff --git a/packages/ui/src/2_molecules/Table/components/TableMobile/TableMobile.tsx b/packages/ui/src/2_molecules/Table/components/TableMobile/TableMobile.tsx
index ae92bc882..38abb992a 100644
--- a/packages/ui/src/2_molecules/Table/components/TableMobile/TableMobile.tsx
+++ b/packages/ui/src/2_molecules/Table/components/TableMobile/TableMobile.tsx
@@ -20,6 +20,7 @@ export const TableMobile =
({
expandedContent,
mobileRenderer,
subtitleRenderer,
+ flatMode,
}: TableProps) => (
{rows &&
@@ -36,6 +37,7 @@ export const TableMobile =
({
expandedContent={expandedContent}
renderer={mobileRenderer}
subtitleRenderer={subtitleRenderer}
+ flatMode={flatMode}
/>
))}
diff --git a/packages/ui/src/2_molecules/Table/components/TableMobile/components/TableMobileRow.tsx b/packages/ui/src/2_molecules/Table/components/TableMobile/components/TableMobileRow.tsx
index 4787acc13..5858116f1 100644
--- a/packages/ui/src/2_molecules/Table/components/TableMobile/components/TableMobileRow.tsx
+++ b/packages/ui/src/2_molecules/Table/components/TableMobile/components/TableMobileRow.tsx
@@ -17,6 +17,7 @@ type TableMobileRowProps = {
expandedContent?: (row: RowType) => ReactNode;
renderer?: (row: RowType) => ReactNode;
subtitleRenderer?: (row: RowType) => ReactNode;
+ flatMode?: boolean;
index: number;
};
@@ -29,13 +30,16 @@ export const TableMobileRow = ({
expandedContent,
renderer,
subtitleRenderer,
+ flatMode,
index,
}: TableMobileRowProps) => {
const [open, setOpen] = useState(false);
const onClick = useCallback(() => {
- onRowClick?.(row);
- }, [onRowClick, row]);
+ if (!flatMode) {
+ onRowClick?.(row);
+ }
+ }, [flatMode, onRowClick, row]);
const title = useMemo(
() => <>{titleRenderer?.(row, open) || index}>,
@@ -50,6 +54,7 @@ export const TableMobileRow = ({
labelClassName={styles.accordion}
dataAttribute={dataAttribute}
style={AccordionStyle.secondary}
+ flatMode={flatMode}
>
{!renderer &&
diff --git a/packages/ui/src/2_molecules/Tabs/Tabs.tsx b/packages/ui/src/2_molecules/Tabs/Tabs.tsx
index 2943e46a0..a4fe9f7cc 100644
--- a/packages/ui/src/2_molecules/Tabs/Tabs.tsx
+++ b/packages/ui/src/2_molecules/Tabs/Tabs.tsx
@@ -1,4 +1,4 @@
-import React, { useMemo, useCallback } from 'react';
+import React, { useMemo, useCallback, RefObject } from 'react';
import classNames from 'classnames';
@@ -22,6 +22,7 @@ type TabsProps = {
onChange?: (index: number) => void;
type?: TabType;
size?: TabSize;
+ wrapperRef?: RefObject
;
};
export const Tabs: React.FC = ({
@@ -32,6 +33,7 @@ export const Tabs: React.FC = ({
contentClassName,
type = TabType.primary,
size = TabSize.normal,
+ wrapperRef,
}) => {
const selectTab = useCallback(
(item: ITabItem, index: number) => {
@@ -45,7 +47,7 @@ export const Tabs: React.FC = ({
const content = useMemo(() => items[index]?.content, [index, items]);
return (
-
+
{items.map((item, i) => (