Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ede909dacc | ||
|
|
7d3bdd2283 | ||
|
|
83c7e0b98f | ||
|
|
6fc25290ab | ||
|
|
f201a522d5 | ||
|
|
cf042492b4 |
@@ -0,0 +1,140 @@
|
||||
import { useEffect } from 'react';
|
||||
import { titlefy } from '@vegaprotocol/utils';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { ErrorBoundary } from '@sentry/react';
|
||||
import { CompetitionsHeader } from '../../components/competitions/competitions-header';
|
||||
import { Intent, Loader, TradingButton } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
import { useGames } from './hooks/use-games';
|
||||
import { useCurrentEpochInfoQuery } from '../referrals/hooks/__generated__/Epoch';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { Links } from '../../lib/links';
|
||||
import {
|
||||
CompetitionsAction,
|
||||
CompetitionsActionsContainer,
|
||||
} from '../../components/competitions/competitions-cta';
|
||||
import { GamesContainer } from '../../components/competitions/games-container';
|
||||
import { CompetitionsLeaderboard } from '../../components/competitions/competitions-leaderboard';
|
||||
import { useTeams } from './hooks/use-teams';
|
||||
import take from 'lodash/take';
|
||||
|
||||
export const CompetitionsHome = () => {
|
||||
const t = useT();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: epochData } = useCurrentEpochInfoQuery();
|
||||
const currentEpoch = Number(epochData?.epoch.id);
|
||||
|
||||
const { data: gamesData, loading: gamesLoading } = useGames({
|
||||
onlyActive: true,
|
||||
currentEpoch,
|
||||
});
|
||||
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams({
|
||||
sortByField: ['totalQuantumRewards'],
|
||||
order: 'desc',
|
||||
});
|
||||
|
||||
const { updateTitle } = usePageTitleStore((store) => ({
|
||||
updateTitle: store.updateTitle,
|
||||
}));
|
||||
useEffect(() => {
|
||||
updateTitle(titlefy([t('Competitions')]));
|
||||
}, [updateTitle, t]);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<CompetitionsHeader title={t('Competitions')}>
|
||||
<p className="text-lg mb-1">
|
||||
{t(
|
||||
'Be a team player! Participate in games and work together to rake in as much profit to win.'
|
||||
)}
|
||||
</p>
|
||||
</CompetitionsHeader>
|
||||
|
||||
{/** Get started */}
|
||||
<h2 className="text-2xl mb-6">{t('Get started')}</h2>
|
||||
|
||||
<CompetitionsActionsContainer>
|
||||
<CompetitionsAction
|
||||
variant="A"
|
||||
title={t('Create a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM());
|
||||
}}
|
||||
>
|
||||
{t('Create a public team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="B"
|
||||
title={t('Solo team / lone wolf')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_CREATE_TEAM());
|
||||
}}
|
||||
>
|
||||
{t('Create a private team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
<CompetitionsAction
|
||||
variant="C"
|
||||
title={t('Join a team')}
|
||||
description={t(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipisicing elit placeat ipsum minus nemo error dicta.'
|
||||
)}
|
||||
actionElement={
|
||||
<TradingButton
|
||||
intent={Intent.Primary}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(Links.COMPETITIONS_TEAMS());
|
||||
}}
|
||||
>
|
||||
{t('Choose a team')}
|
||||
</TradingButton>
|
||||
}
|
||||
/>
|
||||
</CompetitionsActionsContainer>
|
||||
|
||||
{/** List of available games */}
|
||||
<h2 className="text-2xl mb-6">{t('Games')}</h2>
|
||||
|
||||
{gamesLoading ? (
|
||||
<Loader size="small" />
|
||||
) : (
|
||||
<GamesContainer data={gamesData} currentEpoch={currentEpoch} />
|
||||
)}
|
||||
|
||||
{/** The teams ranking */}
|
||||
<div className="mb-6 flex flex-row items-baseline justify-between">
|
||||
<h2 className="text-2xl">{t('Leaderboard')}</h2>
|
||||
<Link to={Links.COMPETITIONS_TEAMS()} className="text-sm underline">
|
||||
{t('View all teams')}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{teamsLoading ? (
|
||||
<Loader size="small" />
|
||||
) : (
|
||||
<CompetitionsLeaderboard data={take(teamsData, 10)} />
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,395 @@
|
||||
import { useState, type ReactNode, type ButtonHTMLAttributes } from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import countBy from 'lodash/countBy';
|
||||
import {
|
||||
TradingButton as Button,
|
||||
Intent,
|
||||
Pill,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
Tooltip,
|
||||
Splash,
|
||||
truncateMiddle,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import classNames from 'classnames';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Table } from '../../components/table';
|
||||
import { formatNumberRounded, getDateTimeFormat } from '@vegaprotocol/utils';
|
||||
import {
|
||||
useTeam,
|
||||
type Team as TeamType,
|
||||
type TeamStats,
|
||||
type Member,
|
||||
type TeamGame,
|
||||
} from './hooks/use-team';
|
||||
import { DApp, EXPLORER_PARTIES, useLinks } from '@vegaprotocol/environment';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import { TeamAvatar } from '../../components/competitions/team-avatar';
|
||||
|
||||
export const CompetitionsTeam = () => {
|
||||
const t = useT();
|
||||
const { teamId } = useParams<{ teamId: string }>();
|
||||
const { team, stats, partyInTeam, members, games } = useTeam(teamId);
|
||||
|
||||
// const team = {
|
||||
// teamId: '12345678909876543212345678765432345676543234567',
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// name: 'The Kittens',
|
||||
// teamUrl: 'http://placekitten.com/g/200/300',
|
||||
// avatarUrl: 'http://placekitten.com/g/200/300',
|
||||
// createdAt: '2024-01-01',
|
||||
// createdAtEpoch: 123,
|
||||
// closed: true,
|
||||
// };
|
||||
|
||||
if (!team) {
|
||||
return (
|
||||
<Splash>
|
||||
<p>{t('Page not found')}</p>
|
||||
</Splash>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<TeamPage
|
||||
team={team}
|
||||
stats={stats}
|
||||
partyInTeam={partyInTeam}
|
||||
members={members}
|
||||
games={games}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const TeamPage = ({
|
||||
team,
|
||||
stats,
|
||||
partyInTeam,
|
||||
members,
|
||||
games,
|
||||
}: {
|
||||
team: TeamType;
|
||||
stats?: TeamStats;
|
||||
partyInTeam: boolean;
|
||||
members?: Member[];
|
||||
games?: TeamGame[];
|
||||
}) => {
|
||||
const t = useT();
|
||||
const [showGames, setShowGames] = useState(true);
|
||||
|
||||
return (
|
||||
<div className="relative h-full overflow-y-auto">
|
||||
<div className="absolute top-0 left-0 w-full h-[40%] -z-10 bg-[40%_0px] bg-cover bg-no-repeat bg-local bg-[url(/cover.png)]">
|
||||
<div className="absolute top-o left-0 w-full h-full bg-gradient-to-t from-white dark:from-vega-cdark-900 to-transparent from-20% to-60%" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 lg:gap-6 container p-4 mx-auto">
|
||||
<header className="flex gap-3 lg:gap-4 pt-5 lg:pt-10">
|
||||
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
|
||||
<div className="flex flex-col items-start gap-1 lg:gap-3">
|
||||
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
|
||||
{team.name}
|
||||
</h1>
|
||||
<JoinButton joined={partyInTeam} />
|
||||
</div>
|
||||
</header>
|
||||
<StatSection>
|
||||
<StatList>
|
||||
<Stat value={members ? members.length : 0} label={t('Members')} />
|
||||
<Stat
|
||||
value={stats ? stats.totalGamesPlayed : 0}
|
||||
label={t('Total games')}
|
||||
tooltip={t('Total number of games this team has participated in')}
|
||||
/>
|
||||
<StatSectionSeparator />
|
||||
<Stat
|
||||
value={
|
||||
stats
|
||||
? formatNumberRounded(
|
||||
new BigNumber(stats.totalQuantumVolume),
|
||||
'1e3'
|
||||
)
|
||||
: 0
|
||||
}
|
||||
label={t('Total volume')}
|
||||
/>
|
||||
<Stat
|
||||
value={
|
||||
stats
|
||||
? formatNumberRounded(
|
||||
new BigNumber(stats.totalQuantumRewards),
|
||||
'1e3'
|
||||
)
|
||||
: 0
|
||||
}
|
||||
label={t('Rewards paid')}
|
||||
tooltip={'Total amount of rewards paid out to this team in qUSD'}
|
||||
/>
|
||||
</StatList>
|
||||
</StatSection>
|
||||
{games && games.length ? (
|
||||
<StatSection>
|
||||
<FavoriteGame games={games} />
|
||||
<StatSectionSeparator />
|
||||
<LatestResults games={games} />
|
||||
</StatSection>
|
||||
) : null}
|
||||
<section>
|
||||
<div className="flex gap-4 lg:gap-8 mb-4 border-b border-default">
|
||||
<ToggleButton active={showGames} onClick={() => setShowGames(true)}>
|
||||
{t('Games ({{count}})', { count: games ? games.length : 0 })}
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
active={!showGames}
|
||||
onClick={() => setShowGames(false)}
|
||||
>
|
||||
{t('Members ({{count}})', {
|
||||
count: members ? members.length : 0,
|
||||
})}
|
||||
</ToggleButton>
|
||||
</div>
|
||||
{showGames ? <Games games={games} /> : <Members members={members} />}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Games = ({ games }: { games?: TeamGame[] }) => {
|
||||
const t = useT();
|
||||
|
||||
if (!games?.length) {
|
||||
return <p>{t('No games')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'rank', displayName: t('Rank') },
|
||||
{
|
||||
name: 'epoch',
|
||||
displayName: t('Epoch'),
|
||||
headerClassName: 'hidden md:block',
|
||||
className: 'hidden md:block',
|
||||
},
|
||||
{ name: 'type', displayName: t('Type') },
|
||||
{ name: 'amount', displayName: t('Amount earned') },
|
||||
{
|
||||
name: 'teams',
|
||||
displayName: t('No. of participating teams'),
|
||||
headerClassName: 'hidden md:block',
|
||||
className: 'hidden md:block',
|
||||
},
|
||||
{ name: 'status', displayName: t('Status') },
|
||||
]}
|
||||
data={games.map((game) => ({
|
||||
rank: game.team.rank,
|
||||
epoch: game.epoch,
|
||||
type: game.team.rewardMetric,
|
||||
amount: game.team.totalRewardsEarned,
|
||||
teams: game.numberOfParticipants,
|
||||
}))}
|
||||
noCollapse={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Members = ({ members }: { members?: Member[] }) => {
|
||||
const t = useT();
|
||||
|
||||
if (!members?.length) {
|
||||
return <p>{t('No members')}</p>;
|
||||
}
|
||||
|
||||
const data = orderBy(
|
||||
members.map((m) => ({
|
||||
referee: <RefereeCell pubkey={m.referee} />,
|
||||
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
|
||||
joinedAtEpoch: Number(m.joinedAtEpoch),
|
||||
explorerLink: <RefereeLink pubkey={m.referee} />,
|
||||
})),
|
||||
'joinedAtEpoch',
|
||||
'desc'
|
||||
);
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'referee', displayName: t('Referee') },
|
||||
{
|
||||
name: 'joinedAt',
|
||||
displayName: t('Joined at'),
|
||||
},
|
||||
{
|
||||
name: 'joinedAtEpoch',
|
||||
displayName: t('Joined epoch'),
|
||||
headerClassName: 'text-right',
|
||||
className: 'text-right',
|
||||
},
|
||||
{
|
||||
name: 'explorerLink',
|
||||
displayName: '',
|
||||
headerClassName: 'hidden md:block',
|
||||
className: 'hidden md:block text-right',
|
||||
},
|
||||
]}
|
||||
data={data}
|
||||
noCollapse={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const RefereeCell = ({ pubkey }: { pubkey: string }) => {
|
||||
return <span title={pubkey}>{truncateMiddle(pubkey)}</span>;
|
||||
};
|
||||
|
||||
const RefereeLink = ({ pubkey }: { pubkey: string }) => {
|
||||
const t = useT();
|
||||
|
||||
const linkCreator = useLinks(DApp.Explorer);
|
||||
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
|
||||
|
||||
return (
|
||||
<Link to={link} className="underline underline-offset-4">
|
||||
{t('View on explorer')}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
const JoinButton = ({ joined }: { joined: boolean }) => {
|
||||
const t = useT();
|
||||
|
||||
if (joined) {
|
||||
return (
|
||||
<Button intent={Intent.None} disabled={true}>
|
||||
<span className="flex items-center gap-2">
|
||||
{t('Joined')}{' '}
|
||||
<span className="text-vega-green-600 dark:text-vega-green">
|
||||
<VegaIcon name={VegaIconNames.TICK} />
|
||||
</span>
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return <Button intent={Intent.Primary}>{t('Join this team')}</Button>;
|
||||
};
|
||||
|
||||
const LatestResults = ({ games }: { games: TeamGame[] }) => {
|
||||
const t = useT();
|
||||
const latestGames = games.slice(0, 5);
|
||||
|
||||
return (
|
||||
<dl>
|
||||
<dt className="text-muted text-sm">
|
||||
{t('Last {{count}} game results', { count: latestGames.length })}
|
||||
</dt>
|
||||
<dd className="flex gap-1">
|
||||
{latestGames.map((game) => {
|
||||
return (
|
||||
<Pill key={game.id} className="text-sm">
|
||||
{t('place', { count: game.team.rank, ordinal: true })}
|
||||
</Pill>
|
||||
);
|
||||
})}
|
||||
</dd>
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
||||
const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
|
||||
const t = useT();
|
||||
|
||||
const rewardMetrics = games.map((game) => game.team.rewardMetric);
|
||||
const count = countBy(rewardMetrics);
|
||||
|
||||
let favoriteMetric = '';
|
||||
let mostOccurances = 0;
|
||||
|
||||
for (const key in count) {
|
||||
if (count[key] > mostOccurances) {
|
||||
favoriteMetric = key;
|
||||
mostOccurances = count[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (!favoriteMetric) return null;
|
||||
|
||||
return (
|
||||
<dl>
|
||||
<dt className="text-muted text-sm">{t('Favorite game')}</dt>
|
||||
<dd>
|
||||
<Pill className="flex-inline items-center gap-2 bg-transparent text-sm">
|
||||
<VegaIcon
|
||||
name={VegaIconNames.STAR}
|
||||
className="text-vega-yellow-400"
|
||||
/>{' '}
|
||||
{favoriteMetric}
|
||||
</Pill>
|
||||
</dd>
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
||||
const ToggleButton = ({
|
||||
active,
|
||||
...props
|
||||
}: ButtonHTMLAttributes<HTMLButtonElement> & { active: boolean }) => {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={classNames('relative top-px uppercase border-b-2 py-4', {
|
||||
'text-muted border-transparent': !active,
|
||||
'border-vega-yellow': active,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const StatSection = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<section className="flex flex-col lg:flex-row gap-2 lg:gap-8">
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
const StatSectionSeparator = () => {
|
||||
return <div className="hidden md:block border-r border-default" />;
|
||||
};
|
||||
|
||||
const StatList = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<dl className="grid grid-cols-[min-content_min-content] md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
|
||||
{children}
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
||||
const Stat = ({
|
||||
value,
|
||||
label,
|
||||
tooltip,
|
||||
}: {
|
||||
value: ReactNode;
|
||||
label: ReactNode;
|
||||
tooltip?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
<dd className="text-3xl lg:text-4xl">{value}</dd>
|
||||
<dt className="text-sm text-muted">
|
||||
{tooltip ? (
|
||||
<Tooltip description={tooltip} underline={false}>
|
||||
<span className="flex items-center gap-2">
|
||||
{label}
|
||||
<VegaIcon name={VegaIconNames.INFO} size={12} />
|
||||
</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
label
|
||||
)}
|
||||
</dt>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,241 @@
|
||||
import { ErrorBoundary } from '@sentry/react';
|
||||
import { CompetitionsHeader } from '../../components/competitions/competitions-header';
|
||||
import { usePageTitleStore } from '../../stores';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { titlefy } from '@vegaprotocol/utils';
|
||||
import { useTeams } from './hooks/use-teams';
|
||||
import { CompetitionsLeaderboard } from '../../components/competitions/competitions-leaderboard';
|
||||
import {
|
||||
Input,
|
||||
Loader,
|
||||
VegaIcon,
|
||||
VegaIconNames,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const CompetitionsTeams = () => {
|
||||
const t = useT();
|
||||
const { updateTitle } = usePageTitleStore((store) => ({
|
||||
updateTitle: store.updateTitle,
|
||||
}));
|
||||
useEffect(() => {
|
||||
updateTitle(titlefy([t('Competitions'), t('Teams')]));
|
||||
}, [updateTitle, t]);
|
||||
|
||||
const { data: teamsData, loading: teamsLoading } = useTeams({
|
||||
sortByField: ['totalQuantumRewards'],
|
||||
order: 'desc',
|
||||
});
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// const teamsData = [
|
||||
// {
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// avatarUrl: '',
|
||||
// teamUrl: 'https://vega.xyz',
|
||||
// closed: true,
|
||||
// teamId:
|
||||
// '8d81a2ba54c21cbb891e46a2a8debac508b33ee83e2d7c3b8a5bcd420753515e',
|
||||
// name: 'cat lovers 2000',
|
||||
// totalQuantumRewards: '1234567890',
|
||||
// totalGamesPlayed: 12,
|
||||
// totalQuantumVolume: '1234567890',
|
||||
// gamesPlayed: [],
|
||||
// createdAt: 123,
|
||||
// createdAtEpoch: 123,
|
||||
// },
|
||||
// {
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// avatarUrl: '',
|
||||
// teamUrl: 'https://vega.xyz',
|
||||
// closed: true,
|
||||
// teamId:
|
||||
// 'afe0f77322c9bc9ebe36faa8b0d846617018b86db78f33cadbe09dc15c95f408',
|
||||
// name: 'dog lovers 2000',
|
||||
// totalQuantumRewards: '1234567890',
|
||||
// totalGamesPlayed: 12,
|
||||
// totalQuantumVolume: '1234567890',
|
||||
// gamesPlayed: [],
|
||||
// createdAt: 123,
|
||||
// createdAtEpoch: 123,
|
||||
// },
|
||||
// {
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// avatarUrl: '',
|
||||
// teamUrl: 'https://vega.xyz',
|
||||
// closed: true,
|
||||
// teamId:
|
||||
// '580e52a0ef7f706898c6e0a3a34a6f870ef2a49e191408a3e1ed2c0a92e6e588',
|
||||
// name: 'we like vega',
|
||||
// totalQuantumRewards: '1234567890',
|
||||
// totalGamesPlayed: 12,
|
||||
// totalQuantumVolume: '1234567890',
|
||||
// gamesPlayed: [],
|
||||
// createdAt: 123,
|
||||
// createdAtEpoch: 123,
|
||||
// },
|
||||
// {
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// avatarUrl: '',
|
||||
// teamUrl: 'https://vega.xyz',
|
||||
// closed: true,
|
||||
// teamId:
|
||||
// '38c49f91670f536ce5ab6609899780cfb6bb9dcfc6c79842cbc348325e10c10f',
|
||||
// name: 'pure gold',
|
||||
// totalQuantumRewards: '1234567890',
|
||||
// totalGamesPlayed: 12,
|
||||
// totalQuantumVolume: '1234567890',
|
||||
// gamesPlayed: [],
|
||||
// createdAt: 123,
|
||||
// createdAtEpoch: 123,
|
||||
// },
|
||||
// {
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// avatarUrl: '',
|
||||
// teamUrl: 'https://vega.xyz',
|
||||
// closed: true,
|
||||
// teamId:
|
||||
// 'dfaaace4de83cab2443ebd7f75dc0d9a22a4f4a38f757b3adb5269aa930e0bdc',
|
||||
// name: 'diamond hands',
|
||||
// totalQuantumRewards: '1234567890',
|
||||
// totalGamesPlayed: 12,
|
||||
// totalQuantumVolume: '1234567890',
|
||||
// gamesPlayed: [],
|
||||
// createdAt: 123,
|
||||
// createdAtEpoch: 123,
|
||||
// },
|
||||
// {
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// avatarUrl: '',
|
||||
// teamUrl: 'https://vega.xyz',
|
||||
// closed: true,
|
||||
// teamId:
|
||||
// '1627a2ea10b07ed9cf08bfc7818bd76716493f5e6a5b5ffe4ed856ef34602a14',
|
||||
// name: 'to the moon',
|
||||
// totalQuantumRewards: '1234567890',
|
||||
// totalGamesPlayed: 12,
|
||||
// totalQuantumVolume: '1234567890',
|
||||
// gamesPlayed: [],
|
||||
// createdAt: 123,
|
||||
// createdAtEpoch: 123,
|
||||
// },
|
||||
// {
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// avatarUrl: '',
|
||||
// teamUrl: 'https://vega.xyz',
|
||||
// closed: true,
|
||||
// teamId:
|
||||
// 'd21f7b04985e5b5f510a53ee61c4d77c100374eafeffd754b5296d386b7454da',
|
||||
// name: 'beyond cats',
|
||||
// totalQuantumRewards: '1234567890',
|
||||
// totalGamesPlayed: 12,
|
||||
// totalQuantumVolume: '1234567890',
|
||||
// gamesPlayed: [],
|
||||
// createdAt: 123,
|
||||
// createdAtEpoch: 123,
|
||||
// },
|
||||
// {
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// avatarUrl: '',
|
||||
// teamUrl: 'https://vega.xyz',
|
||||
// closed: true,
|
||||
// teamId:
|
||||
// 'a40339f4cf31f4496e4d85ac7f56d5bbc9e257a337070fa361205c1f2fdaa0eb',
|
||||
// name: 'cat lovers 2000',
|
||||
// totalQuantumRewards: '1234567890',
|
||||
// totalGamesPlayed: 12,
|
||||
// totalQuantumVolume: '1234567890',
|
||||
// gamesPlayed: [],
|
||||
// createdAt: 123,
|
||||
// createdAtEpoch: 123,
|
||||
// },
|
||||
// {
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// avatarUrl: '',
|
||||
// teamUrl: 'https://vega.xyz',
|
||||
// closed: true,
|
||||
// teamId:
|
||||
// 'af059e7508a302cce9523edcbde52bf7f2e876f34854c6274a83aec4045b9657',
|
||||
// name: 'cat lovers 2000',
|
||||
// totalQuantumRewards: '1234567890',
|
||||
// totalGamesPlayed: 12,
|
||||
// totalQuantumVolume: '1234567890',
|
||||
// gamesPlayed: [],
|
||||
// createdAt: 123,
|
||||
// createdAtEpoch: 123,
|
||||
// },
|
||||
// {
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// avatarUrl: '',
|
||||
// teamUrl: 'https://vega.xyz',
|
||||
// closed: true,
|
||||
// teamId:
|
||||
// '3f06f0e0ec863e686212d6b100f03122c9997f890d304f15f55215ac32334e6f',
|
||||
// name: 'cat lovers 2000',
|
||||
// totalQuantumRewards: '1234567890',
|
||||
// totalGamesPlayed: 12,
|
||||
// totalQuantumVolume: '1234567890',
|
||||
// gamesPlayed: [],
|
||||
// createdAt: 123,
|
||||
// createdAtEpoch: 123,
|
||||
// },
|
||||
// {
|
||||
// referrer: '12345678909876543212345678765432345676543234567',
|
||||
// avatarUrl: '',
|
||||
// teamUrl: 'https://vega.xyz',
|
||||
// closed: true,
|
||||
// teamId:
|
||||
// '0fdc948a548ccd29001c2411629065bc5e248b6535b59b4b089f07a577074bf9',
|
||||
// name: 'cat lovers 2000',
|
||||
// totalQuantumRewards: '1234567890',
|
||||
// totalGamesPlayed: 12,
|
||||
// totalQuantumVolume: '1234567890',
|
||||
// gamesPlayed: [],
|
||||
// createdAt: 123,
|
||||
// createdAtEpoch: 123,
|
||||
// },
|
||||
// ];
|
||||
|
||||
const [filter, setFilter] = useState<string | null | undefined>(undefined);
|
||||
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<CompetitionsHeader title={t('Join a team')}>
|
||||
<p className="text-lg mb-1">{t('Choose a team to get involved')}</p>x
|
||||
</CompetitionsHeader>
|
||||
|
||||
<div className="mb-6 flex justify-end">
|
||||
<div className="w-40 h-10 relative">
|
||||
<span className="absolute z-10 pointer-events-none opacity-90 top-[5px] left-[5px]">
|
||||
<VegaIcon name={VegaIconNames.SEARCH} size={18} />
|
||||
</span>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
className="opacity-90 text-right"
|
||||
placeholder={t('Name')}
|
||||
onKeyUp={() => {
|
||||
const value = inputRef.current?.value;
|
||||
if (value != filter) setFilter(value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{teamsLoading ? (
|
||||
<Loader size="small" />
|
||||
) : (
|
||||
<CompetitionsLeaderboard
|
||||
data={teamsData.filter((td) => {
|
||||
if (filter && filter.length > 0) {
|
||||
const re = new RegExp(filter, 'i');
|
||||
return re.test(td.name);
|
||||
}
|
||||
return true;
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
fragment TeamFields on Team {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
}
|
||||
|
||||
fragment TeamStatsFields on TeamStatistics {
|
||||
teamId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
quantumRewards {
|
||||
epoch
|
||||
total_quantum_rewards
|
||||
}
|
||||
gamesPlayed
|
||||
}
|
||||
|
||||
fragment TeamRefereeFields on TeamReferee {
|
||||
teamId
|
||||
referee
|
||||
joinedAt
|
||||
joinedAtEpoch
|
||||
}
|
||||
|
||||
fragment TeamEntity on TeamGameEntity {
|
||||
rank
|
||||
volume
|
||||
rewardMetric
|
||||
rewardEarned
|
||||
totalRewardsEarned
|
||||
team {
|
||||
teamId
|
||||
}
|
||||
}
|
||||
|
||||
fragment TeamGameFields on Game {
|
||||
id
|
||||
epoch
|
||||
numberOfParticipants
|
||||
entities {
|
||||
... on TeamGameEntity {
|
||||
...TeamEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
query Team($teamId: ID!, $partyId: ID) {
|
||||
teams(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamFields
|
||||
}
|
||||
}
|
||||
}
|
||||
partyTeams: teams(partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamFields
|
||||
}
|
||||
}
|
||||
}
|
||||
teamsStatistics(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamStatsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
teamReferees(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamRefereeFields
|
||||
}
|
||||
}
|
||||
}
|
||||
games(entityScope: ENTITY_SCOPE_TEAMS) {
|
||||
edges {
|
||||
node {
|
||||
...TeamGameFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
query TeamReferees($teamId: ID!) {
|
||||
teamReferees(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
referee
|
||||
joinedAt
|
||||
joinedAtEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
query Teams($teamId: ID, $partyId: ID) {
|
||||
teams(teamId: $teamId, partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
query TeamsStatistics($teamId: ID, $aggregationEpochs: Int) {
|
||||
teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
gamesPlayed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TeamFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean };
|
||||
|
||||
export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> };
|
||||
|
||||
export type TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
|
||||
|
||||
export type TeamEntityFragment = { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } };
|
||||
|
||||
export type TeamGameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> };
|
||||
|
||||
export type TeamQueryVariables = Types.Exact<{
|
||||
teamId: Types.Scalars['ID'];
|
||||
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type TeamQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null, partyTeams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> } }> } | null, teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null, games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> } } | null> | null } };
|
||||
|
||||
export const TeamFieldsFragmentDoc = gql`
|
||||
fragment TeamFields on Team {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
}
|
||||
`;
|
||||
export const TeamStatsFieldsFragmentDoc = gql`
|
||||
fragment TeamStatsFields on TeamStatistics {
|
||||
teamId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
quantumRewards {
|
||||
epoch
|
||||
total_quantum_rewards
|
||||
}
|
||||
gamesPlayed
|
||||
}
|
||||
`;
|
||||
export const TeamRefereeFieldsFragmentDoc = gql`
|
||||
fragment TeamRefereeFields on TeamReferee {
|
||||
teamId
|
||||
referee
|
||||
joinedAt
|
||||
joinedAtEpoch
|
||||
}
|
||||
`;
|
||||
export const TeamEntityFragmentDoc = gql`
|
||||
fragment TeamEntity on TeamGameEntity {
|
||||
rank
|
||||
volume
|
||||
rewardMetric
|
||||
rewardEarned
|
||||
totalRewardsEarned
|
||||
team {
|
||||
teamId
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const TeamGameFieldsFragmentDoc = gql`
|
||||
fragment TeamGameFields on Game {
|
||||
id
|
||||
epoch
|
||||
numberOfParticipants
|
||||
entities {
|
||||
... on TeamGameEntity {
|
||||
...TeamEntity
|
||||
}
|
||||
}
|
||||
}
|
||||
${TeamEntityFragmentDoc}`;
|
||||
export const TeamDocument = gql`
|
||||
query Team($teamId: ID!, $partyId: ID) {
|
||||
teams(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamFields
|
||||
}
|
||||
}
|
||||
}
|
||||
partyTeams: teams(partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamFields
|
||||
}
|
||||
}
|
||||
}
|
||||
teamsStatistics(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamStatsFields
|
||||
}
|
||||
}
|
||||
}
|
||||
teamReferees(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
...TeamRefereeFields
|
||||
}
|
||||
}
|
||||
}
|
||||
games(entityScope: ENTITY_SCOPE_TEAMS) {
|
||||
edges {
|
||||
node {
|
||||
...TeamGameFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
${TeamFieldsFragmentDoc}
|
||||
${TeamStatsFieldsFragmentDoc}
|
||||
${TeamRefereeFieldsFragmentDoc}
|
||||
${TeamGameFieldsFragmentDoc}`;
|
||||
|
||||
/**
|
||||
* __useTeamQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useTeamQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useTeamQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useTeamQuery({
|
||||
* variables: {
|
||||
* teamId: // value for 'teamId'
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useTeamQuery(baseOptions: Apollo.QueryHookOptions<TeamQuery, TeamQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<TeamQuery, TeamQueryVariables>(TeamDocument, options);
|
||||
}
|
||||
export function useTeamLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamQuery, TeamQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<TeamQuery, TeamQueryVariables>(TeamDocument, options);
|
||||
}
|
||||
export type TeamQueryHookResult = ReturnType<typeof useTeamQuery>;
|
||||
export type TeamLazyQueryHookResult = ReturnType<typeof useTeamLazyQuery>;
|
||||
export type TeamQueryResult = Apollo.QueryResult<TeamQuery, TeamQueryVariables>;
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TeamRefereesQueryVariables = Types.Exact<{
|
||||
teamId: Types.Scalars['ID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type TeamRefereesQuery = { __typename?: 'Query', teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null };
|
||||
|
||||
|
||||
export const TeamRefereesDocument = gql`
|
||||
query TeamReferees($teamId: ID!) {
|
||||
teamReferees(teamId: $teamId) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
referee
|
||||
joinedAt
|
||||
joinedAtEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useTeamRefereesQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useTeamRefereesQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useTeamRefereesQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useTeamRefereesQuery({
|
||||
* variables: {
|
||||
* teamId: // value for 'teamId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useTeamRefereesQuery(baseOptions: Apollo.QueryHookOptions<TeamRefereesQuery, TeamRefereesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<TeamRefereesQuery, TeamRefereesQueryVariables>(TeamRefereesDocument, options);
|
||||
}
|
||||
export function useTeamRefereesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamRefereesQuery, TeamRefereesQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<TeamRefereesQuery, TeamRefereesQueryVariables>(TeamRefereesDocument, options);
|
||||
}
|
||||
export type TeamRefereesQueryHookResult = ReturnType<typeof useTeamRefereesQuery>;
|
||||
export type TeamRefereesLazyQueryHookResult = ReturnType<typeof useTeamRefereesLazyQuery>;
|
||||
export type TeamRefereesQueryResult = Apollo.QueryResult<TeamRefereesQuery, TeamRefereesQueryVariables>;
|
||||
@@ -0,0 +1,61 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TeamsQueryVariables = Types.Exact<{
|
||||
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null };
|
||||
|
||||
|
||||
export const TeamsDocument = gql`
|
||||
query Teams($teamId: ID, $partyId: ID) {
|
||||
teams(teamId: $teamId, partyId: $partyId) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
referrer
|
||||
name
|
||||
teamUrl
|
||||
avatarUrl
|
||||
createdAt
|
||||
createdAtEpoch
|
||||
closed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useTeamsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useTeamsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useTeamsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useTeamsQuery({
|
||||
* variables: {
|
||||
* teamId: // value for 'teamId'
|
||||
* partyId: // value for 'partyId'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useTeamsQuery(baseOptions?: Apollo.QueryHookOptions<TeamsQuery, TeamsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<TeamsQuery, TeamsQueryVariables>(TeamsDocument, options);
|
||||
}
|
||||
export function useTeamsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamsQuery, TeamsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<TeamsQuery, TeamsQueryVariables>(TeamsDocument, options);
|
||||
}
|
||||
export type TeamsQueryHookResult = ReturnType<typeof useTeamsQuery>;
|
||||
export type TeamsLazyQueryHookResult = ReturnType<typeof useTeamsLazyQuery>;
|
||||
export type TeamsQueryResult = Apollo.QueryResult<TeamsQuery, TeamsQueryVariables>;
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as Types from '@vegaprotocol/types';
|
||||
|
||||
import { gql } from '@apollo/client';
|
||||
import * as Apollo from '@apollo/client';
|
||||
const defaultOptions = {} as const;
|
||||
export type TeamsStatisticsQueryVariables = Types.Exact<{
|
||||
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||
aggregationEpochs?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type TeamsStatisticsQuery = { __typename?: 'Query', teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string> } }> } | null };
|
||||
|
||||
|
||||
export const TeamsStatisticsDocument = gql`
|
||||
query TeamsStatistics($teamId: ID, $aggregationEpochs: Int) {
|
||||
teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
|
||||
edges {
|
||||
node {
|
||||
teamId
|
||||
totalQuantumVolume
|
||||
totalQuantumRewards
|
||||
totalGamesPlayed
|
||||
gamesPlayed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useTeamsStatisticsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useTeamsStatisticsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useTeamsStatisticsQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useTeamsStatisticsQuery({
|
||||
* variables: {
|
||||
* teamId: // value for 'teamId'
|
||||
* aggregationEpochs: // value for 'aggregationEpochs'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useTeamsStatisticsQuery(baseOptions?: Apollo.QueryHookOptions<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>(TeamsStatisticsDocument, options);
|
||||
}
|
||||
export function useTeamsStatisticsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>(TeamsStatisticsDocument, options);
|
||||
}
|
||||
export type TeamsStatisticsQueryHookResult = ReturnType<typeof useTeamsStatisticsQuery>;
|
||||
export type TeamsStatisticsLazyQueryHookResult = ReturnType<typeof useTeamsStatisticsLazyQuery>;
|
||||
export type TeamsStatisticsQueryResult = Apollo.QueryResult<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>;
|
||||
@@ -0,0 +1,37 @@
|
||||
import compact from 'lodash/compact';
|
||||
import { useActiveRewardsQuery } from '../../../components/rewards-container/__generated__/Rewards';
|
||||
import { isActiveReward } from '../../../components/rewards-container/active-rewards';
|
||||
import { EntityScope, type TransferNode } from '@vegaprotocol/types';
|
||||
|
||||
const isScopedToTeams = (node: TransferNode) =>
|
||||
node.transfer.kind.__typename === 'RecurringTransfer' &&
|
||||
node.transfer.kind.dispatchStrategy?.entityScope ===
|
||||
EntityScope.ENTITY_SCOPE_TEAMS;
|
||||
|
||||
export const useGames = ({
|
||||
currentEpoch,
|
||||
onlyActive,
|
||||
}: {
|
||||
currentEpoch: number;
|
||||
onlyActive: boolean;
|
||||
}) => {
|
||||
const { data, loading, error } = useActiveRewardsQuery({
|
||||
variables: {
|
||||
isReward: true,
|
||||
},
|
||||
});
|
||||
|
||||
const games = compact(data?.transfersConnection?.edges?.map((n) => n?.node))
|
||||
.map((n) => n as TransferNode)
|
||||
.filter((node) => {
|
||||
const recurring = node.transfer.kind.__typename !== 'RecurringTransfer';
|
||||
const active = onlyActive ? isActiveReward(node, currentEpoch) : true;
|
||||
return active && recurring && isScopedToTeams(node);
|
||||
});
|
||||
|
||||
return {
|
||||
data: games,
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import compact from 'lodash/compact';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import {
|
||||
useTeamQuery,
|
||||
type TeamFieldsFragment,
|
||||
type TeamStatsFieldsFragment,
|
||||
type TeamRefereeFieldsFragment,
|
||||
type TeamEntityFragment,
|
||||
} from './__generated__/Team';
|
||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||
|
||||
export type Team = TeamFieldsFragment;
|
||||
export type TeamStats = TeamStatsFieldsFragment;
|
||||
export type Member = TeamRefereeFieldsFragment;
|
||||
export type TeamEntity = TeamEntityFragment;
|
||||
export type TeamGame = ReturnType<typeof useTeam>['games'][number];
|
||||
|
||||
export const useTeam = (teamId?: string) => {
|
||||
const { pubKey } = useVegaWallet();
|
||||
|
||||
const { data, loading, error } = useTeamQuery({
|
||||
variables: { teamId: teamId || '', partyId: pubKey },
|
||||
skip: !teamId,
|
||||
});
|
||||
|
||||
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
|
||||
const partyTeamEdge = data?.partyTeams?.edges[0];
|
||||
const teamStatsEdge = data?.teamsStatistics?.edges.find(
|
||||
(e) => e.node.teamId === teamId
|
||||
);
|
||||
const members = data?.teamReferees?.edges
|
||||
.filter((e) => e.node.teamId === teamId)
|
||||
.map((e) => e.node);
|
||||
|
||||
// Find games where the current team participated in
|
||||
const gamesWithTeam = compact(data?.games.edges).map((edge) => {
|
||||
const team = edge.node.entities.find((e) => {
|
||||
if (e.__typename !== 'TeamGameEntity') return false;
|
||||
if (e.team.teamId !== teamId) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!team) return null;
|
||||
|
||||
return {
|
||||
id: edge.node.id,
|
||||
epoch: edge.node.epoch,
|
||||
numberOfParticipants: edge.node.numberOfParticipants,
|
||||
team: team as TeamEntity, // TS can't infer that all the game entities are teams
|
||||
};
|
||||
});
|
||||
const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
stats: teamStatsEdge?.node,
|
||||
team: teamEdge?.node,
|
||||
members,
|
||||
games,
|
||||
partyInTeam: Boolean(partyTeamEdge),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useMemo } from 'react';
|
||||
import { type TeamsQuery, useTeamsQuery } from './__generated__/Teams';
|
||||
import {
|
||||
type TeamsStatisticsQuery,
|
||||
useTeamsStatisticsQuery,
|
||||
} from './__generated__/TeamsStatistics';
|
||||
import compact from 'lodash/compact';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { type ArrayElement } from 'type-fest/source/internal';
|
||||
|
||||
type SortableField = keyof Omit<
|
||||
ArrayElement<NonNullable<TeamsQuery['teams']>['edges']>['node'] &
|
||||
ArrayElement<
|
||||
NonNullable<TeamsStatisticsQuery['teamsStatistics']>['edges']
|
||||
>['node'],
|
||||
'__typename'
|
||||
>;
|
||||
|
||||
type UseTeamsArgs = {
|
||||
aggregationEpochs?: number;
|
||||
sortByField?: SortableField[];
|
||||
order?: 'asc' | 'desc';
|
||||
};
|
||||
|
||||
const DEFAULT_AGGREGATION_EPOCHS = 10;
|
||||
|
||||
export const useTeams = ({
|
||||
aggregationEpochs = DEFAULT_AGGREGATION_EPOCHS,
|
||||
sortByField = ['createdAtEpoch'],
|
||||
order = 'asc',
|
||||
}: UseTeamsArgs) => {
|
||||
const {
|
||||
data: teamsData,
|
||||
loading: teamsLoading,
|
||||
error: teamsError,
|
||||
} = useTeamsQuery();
|
||||
|
||||
const {
|
||||
data: statsData,
|
||||
loading: statsLoading,
|
||||
error: statsError,
|
||||
} = useTeamsStatisticsQuery({
|
||||
variables: {
|
||||
aggregationEpochs,
|
||||
},
|
||||
});
|
||||
|
||||
const teams = compact(teamsData?.teams?.edges).map((e) => e.node);
|
||||
const stats = compact(statsData?.teamsStatistics?.edges).map((e) => e.node);
|
||||
|
||||
const data = useMemo(() => {
|
||||
const data = teams.map((t) => ({
|
||||
...t,
|
||||
...stats.find((s) => s.teamId === t.teamId),
|
||||
}));
|
||||
|
||||
const sorted = sortBy(data, sortByField);
|
||||
if (order === 'desc') {
|
||||
return sorted.reverse();
|
||||
}
|
||||
return sorted;
|
||||
}, [teams, sortByField, order, stats]);
|
||||
|
||||
return {
|
||||
data,
|
||||
loading: teamsLoading && statsLoading,
|
||||
error: teamsError || statsError,
|
||||
};
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { Teams } from './teams';
|
||||
@@ -1,7 +0,0 @@
|
||||
export const Teams = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Teams</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import invert from 'lodash/invert';
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
import { type Interval } from '@vegaprotocol/types';
|
||||
import {
|
||||
TradingViewContainer,
|
||||
ALLOWED_TRADINGVIEW_HOSTNAMES,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
TradingDropdownTrigger,
|
||||
Icon,
|
||||
} from '@vegaprotocol/ui-toolkit';
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
import { type Interval } from '@vegaprotocol/types';
|
||||
import { useEnvironment } from '@vegaprotocol/environment';
|
||||
import { ALLOWED_TRADINGVIEW_HOSTNAMES } from '@vegaprotocol/trading-view';
|
||||
import { IconNames, type IconName } from '@blueprintjs/icons';
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import classNames from 'classnames';
|
||||
import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
|
||||
import { DudeBadge } from './graphics/dude-badge';
|
||||
|
||||
export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
||||
export const GRADIENT =
|
||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||
|
||||
export const CompetitionsActionsContainer = ({
|
||||
children,
|
||||
}: {
|
||||
children:
|
||||
| ReactElement<typeof CompetitionsAction>
|
||||
| Iterable<ReactElement<typeof CompetitionsAction>>;
|
||||
}) => (
|
||||
<div className="grid grid-rows-3 grid-cols-1 md:grid-rows-1 md:grid-cols-3 gap-6 mb-12">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const CompetitionsAction = ({
|
||||
variant,
|
||||
title,
|
||||
description,
|
||||
actionElement,
|
||||
children,
|
||||
}: {
|
||||
variant: ComponentProps<typeof DudeBadge>['variant'];
|
||||
title: string;
|
||||
description?: string;
|
||||
actionElement: ReactNode;
|
||||
children?: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
BORDER_COLOR,
|
||||
GRADIENT,
|
||||
'border rounded-lg',
|
||||
'p-6 flex flex-col items-center gap-6 text-center'
|
||||
)}
|
||||
>
|
||||
<DudeBadge variant={variant} />
|
||||
<h2 className="text-2xl">{title}</h2>
|
||||
{description && <p className="text-muted">{description}</p>}
|
||||
{actionElement}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import classNames from 'classnames';
|
||||
import { AnimatedDudeWithWire } from '../../client-pages/referrals/graphics/dude';
|
||||
import { type ReactNode } from 'react';
|
||||
|
||||
export const CompetitionsHeader = ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
children?: ReactNode;
|
||||
}) => {
|
||||
return (
|
||||
<div className={classNames('relative mb-10 lg:mb-20')}>
|
||||
<div className="">
|
||||
<div
|
||||
aria-hidden
|
||||
className="absolute top-20 right-[220px] md:right-[240px] max-sm:hidden"
|
||||
>
|
||||
<AnimatedDudeWithWire />
|
||||
</div>
|
||||
<div className="pt-10 lg:pt-20 sm:w-[50%]">
|
||||
<h1 className="text-3xl _text-[6vw] lg:!text-6xl leading-[1em] font-alpha calt mb-10">
|
||||
{title}
|
||||
</h1>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,73 @@
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { getNumberFormat } from '@vegaprotocol/utils';
|
||||
import { type useTeams } from '../../client-pages/competitions/hooks/use-teams';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Table } from '../table';
|
||||
import { Rank } from './graphics/rank';
|
||||
import { Links } from '../../lib/links';
|
||||
import { TeamAvatar } from './team-avatar';
|
||||
|
||||
export const CompetitionsLeaderboard = ({
|
||||
data,
|
||||
}: {
|
||||
data: ReturnType<typeof useTeams>['data'];
|
||||
}) => {
|
||||
const t = useT();
|
||||
|
||||
const num = (n?: number | string) =>
|
||||
!n ? '-' : getNumberFormat(0).format(Number(n));
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return <Splash>{t('Could not find any teams')}</Splash>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={[
|
||||
{ name: 'rank', displayName: '#' },
|
||||
{ name: 'avatar', displayName: '' },
|
||||
{ name: 'team', displayName: t('Team') },
|
||||
{ name: 'earned', displayName: t('Rewards earned') },
|
||||
{ name: 'games', displayName: t('Total games') },
|
||||
{ name: 'members', displayName: t('No. of members') },
|
||||
{ name: 'status', displayName: t('Status') },
|
||||
{ name: 'volume', displayName: t('Volume') },
|
||||
]}
|
||||
data={data.map((td, i) => {
|
||||
// leaderboard place or medal
|
||||
let rank: number | React.ReactNode = i + 1;
|
||||
if (rank === 1) rank = <Rank variant="gold" />;
|
||||
if (rank === 2) rank = <Rank variant="silver" />;
|
||||
if (rank === 3) rank = <Rank variant="bronze" />;
|
||||
|
||||
const avatar = (
|
||||
<TeamAvatar
|
||||
teamId={td.teamId}
|
||||
imgUrl={td.avatarUrl}
|
||||
alt={td.name}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
|
||||
return {
|
||||
rank,
|
||||
avatar,
|
||||
team: (
|
||||
<Link
|
||||
className="hover:underline"
|
||||
to={Links.COMPETITIONS_TEAM(td.teamId)}
|
||||
>
|
||||
{td.name}
|
||||
</Link>
|
||||
),
|
||||
earned: num(td.totalQuantumRewards),
|
||||
games: num(td.totalGamesPlayed),
|
||||
members: 0,
|
||||
status: td.closed ? t('Closed') : t('Open'),
|
||||
volume: num(td.totalQuantumVolume),
|
||||
};
|
||||
})}
|
||||
></Table>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { type TransferNode } from '@vegaprotocol/types';
|
||||
import { ActiveRewardCard } from '../rewards-container/active-rewards';
|
||||
import { useT } from '../../lib/use-t';
|
||||
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const GamesContainer = ({
|
||||
data,
|
||||
currentEpoch,
|
||||
}: {
|
||||
data: TransferNode[];
|
||||
currentEpoch: number;
|
||||
}) => {
|
||||
const t = useT();
|
||||
if (!data || data.length === 0) {
|
||||
return <Splash>{t('There are currently no games available.')}</Splash>;
|
||||
}
|
||||
return (
|
||||
<div className="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{data.map((game, i) => {
|
||||
// TODO: Remove `kind` prop from ActiveRewardCard
|
||||
const { transfer } = game;
|
||||
if (
|
||||
transfer.kind.__typename !== 'RecurringTransfer' ||
|
||||
!transfer.kind.dispatchStrategy?.dispatchMetric
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<ActiveRewardCard
|
||||
key={i}
|
||||
transferNode={game}
|
||||
currentEpoch={currentEpoch}
|
||||
kind={transfer.kind}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import classNames from 'classnames';
|
||||
import { DudeWithFlag } from './dude-with-flag';
|
||||
|
||||
/**
|
||||
* Pre-defined badge gradients
|
||||
*/
|
||||
|
||||
export const BADGE_GRADIENT_VARIANT_A =
|
||||
'bg-gradient-to-r from-vega-blue-500 via-vega-purple-500 to-vega-pink-500';
|
||||
export const BADGE_GRADIENT_VARIANT_B =
|
||||
'bg-gradient-to-r from-vega-purple-500 via-vega-green-500 to-vega-blue-500';
|
||||
export const BADGE_GRADIENT_VARIANT_C =
|
||||
'bg-gradient-to-r from-vega-blue-500 via-vega-purple-500 to-vega-green-500';
|
||||
|
||||
/** Badge */
|
||||
|
||||
export const DudeBadge = ({
|
||||
variant,
|
||||
className,
|
||||
}: {
|
||||
variant: 'A' | 'B' | 'C' | undefined;
|
||||
className?: classNames.Argument;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'w-24 h-24 rounded-full bg-black relative',
|
||||
'rotate-12',
|
||||
{
|
||||
[BADGE_GRADIENT_VARIANT_A]: variant === 'A',
|
||||
[BADGE_GRADIENT_VARIANT_B]: variant === 'B',
|
||||
[BADGE_GRADIENT_VARIANT_C]: variant === 'C',
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<DudeWithFlag className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 -rotate-12" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { theme } from '@vegaprotocol/tailwindcss-config';
|
||||
|
||||
type DudeWithFlagProps = {
|
||||
flagColor?: string;
|
||||
withStar?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const DEFAULT_FLAG_COLOR = theme.colors.vega.green[500];
|
||||
|
||||
export const DudeWithFlag = ({
|
||||
flagColor = DEFAULT_FLAG_COLOR,
|
||||
withStar = true,
|
||||
className,
|
||||
}: DudeWithFlagProps) => {
|
||||
return (
|
||||
<svg
|
||||
width="49"
|
||||
height="43"
|
||||
viewBox="0 0 49 43"
|
||||
fill="none"
|
||||
className={className}
|
||||
>
|
||||
{withStar && (
|
||||
<>
|
||||
<path d="M3.99992 0H2V1.99993H3.99992V0Z" fill="white" />
|
||||
<path
|
||||
d="M2 1.99993L0 1.99981V3.99974H1.99992L2 1.99993Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M3.99995 3.99992L1.99992 3.99974L2 5.99988H3.99995V3.99992Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M5.99997 1.99981L3.99992 1.99993L3.99995 3.99992L5.99997 3.99974V1.99981Z"
|
||||
fill="white"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<path
|
||||
d="M32 4H11V33H15V43H20V33H23V43H28V33H32V4ZM20 17H15V12H20V17ZM28 17H23V12H28V17Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path d="M41 25L32 25L32 20L41 20L41 25Z" fill="white" />
|
||||
<path d="M36 29V4H35V29" fill="white" />
|
||||
<path d="M36 13H49L44.55 8.5L49 4H36V13Z" fill={flagColor} />
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export const Rank = ({
|
||||
variant,
|
||||
className,
|
||||
}: {
|
||||
variant?: 'gold' | 'silver' | 'bronze';
|
||||
className?: classNames.Argument;
|
||||
}) => {
|
||||
const { theme } = useThemeSwitcher();
|
||||
return (
|
||||
<div
|
||||
title={classNames({
|
||||
'1': variant === 'gold',
|
||||
'2': variant === 'silver',
|
||||
'3': variant === 'bronze',
|
||||
})}
|
||||
className={classNames(
|
||||
{
|
||||
'text-yellow-300': variant === 'gold',
|
||||
'text-vega-clight-500': variant === 'silver',
|
||||
'text-vega-orange-500': variant === 'bronze',
|
||||
'text-black dark:text-white': variant === undefined,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
<svg width="18" height="30" viewBox="0 0 18 30" fill="none">
|
||||
<defs>
|
||||
<linearGradient x1="0" y1="0" x2="100%" y2="100%" id="medal">
|
||||
<stop offset="33%" stop-color="transparent" />
|
||||
<stop offset="100%" stop-color="black" stop-opacity="50%" />
|
||||
</linearGradient>
|
||||
<clipPath id="shape">
|
||||
<path d="M2 2H4V4H2V2Z" />
|
||||
<path d="M2 2H4V4H2V2Z" />
|
||||
<path d="M2 2H6V4H2V2Z" />
|
||||
<path d="M2 2H6V4H2V2Z" />
|
||||
<path d="M0 4H4V6H0V4Z" />
|
||||
<path d="M0 4H4V6H0V4Z" />
|
||||
<path d="M4 0H14V2H4V0Z" />
|
||||
<path d="M4 0H14V2H4V0Z" />
|
||||
<path d="M0 14V4H2V14H0Z" />
|
||||
<path d="M0 14V4H2V14H0Z" />
|
||||
<path d="M2 30L2 18H4L4 30H2Z" />
|
||||
<path d="M2 30L2 18H4L4 30H2Z" />
|
||||
<path d="M14 30L14 18H16L16 30H14Z" />
|
||||
<path d="M14 30L14 18H16L16 30H14Z" />
|
||||
<path d="M16 14L16 4H18V14H16Z" />
|
||||
<path d="M16 14L16 4H18V14H16Z" />
|
||||
<path d="M2 6V2H4V6H2Z" />
|
||||
<path d="M2 6V2H4V6H2Z" />
|
||||
<path d="M16 2V6H14V2H16Z" />
|
||||
<path d="M16 2V6H14V2H16Z" />
|
||||
<path d="M12 2H16L16 4H12V2Z" />
|
||||
<path d="M12 2H16L16 4H12V2Z" />
|
||||
<path d="M14 4H18V6H14V4Z" />
|
||||
<path d="M14 4H18V6H14V4Z" />
|
||||
<path d="M16 16H12V14H16V16Z" />
|
||||
<path d="M16 16H12V14H16V16Z" />
|
||||
<path d="M14 18H4L4 16L14 16L14 18Z" />
|
||||
<path d="M14 18H4L4 16L14 16L14 18Z" />
|
||||
<path d="M16 12V16H14V12H16Z" />
|
||||
<path d="M16 12V16H14V12H16Z" />
|
||||
<path d="M6 16H2V14H6V16Z" />
|
||||
<path d="M6 16H2V14H6V16Z" />
|
||||
<path d="M6 28H4L4 26H6V28Z" />
|
||||
<path d="M6 28H4L4 26H6V28Z" />
|
||||
<path d="M8 26H6L6 24H8V26Z" />
|
||||
<path d="M8 26H6L6 24H8V26Z" />
|
||||
<path d="M10 24H8V22H10V24Z" />
|
||||
<path d="M10 24H8V22H10V24Z" />
|
||||
<path d="M12 26H10L10 24H12V26Z" />
|
||||
<path d="M12 26H10L10 24H12V26Z" />
|
||||
<path d="M14 28H12L12 26H14V28Z" />
|
||||
<path d="M14 28H12L12 26H14V28Z" />
|
||||
<path d="M4 14H0L2.04189e-07 12H4V14Z" />
|
||||
<path d="M4 14H0L2.04189e-07 12H4V14Z" />
|
||||
<path d="M6 4H12V14H6V4Z" />
|
||||
<path d="M6 4H12V14H6V4Z" />
|
||||
<path d="M4 6H14V12H4V6Z" />
|
||||
<path d="M4 6H14V12H4V6Z" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<rect
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="18"
|
||||
height="30"
|
||||
fill="currentColor"
|
||||
clipPath="url(#shape)"
|
||||
/>
|
||||
<rect
|
||||
rx="0"
|
||||
ry="0"
|
||||
width="18"
|
||||
height="30"
|
||||
fill="url(#medal)"
|
||||
clipPath="url(#shape)"
|
||||
style={{ mixBlendMode: theme === 'dark' ? 'darken' : 'overlay' }}
|
||||
/>
|
||||
<g style={{ mixBlendMode: 'overlay' }}>
|
||||
<path d="M10.5 6H8.5V8H10.5V6Z" fill="white" />
|
||||
<path d="M12.5 8H10.5V10H12.5V8Z" fill="white" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
const NUM_AVATARS = 20;
|
||||
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
|
||||
|
||||
const getFallbackAvatar = (teamId: string) => {
|
||||
const avatarId = ((parseInt(teamId, 16) % NUM_AVATARS) + 1)
|
||||
.toString()
|
||||
.padStart(2, '0'); // between 01 - 20
|
||||
|
||||
return AVATAR_PATHNAME_PATTERN.replace('{id}', avatarId);
|
||||
};
|
||||
|
||||
export const TeamAvatar = ({
|
||||
teamId,
|
||||
imgUrl,
|
||||
alt,
|
||||
size = 'large',
|
||||
}: {
|
||||
teamId: string;
|
||||
imgUrl: string;
|
||||
alt?: string;
|
||||
size?: 'large' | 'small';
|
||||
}) => {
|
||||
const img = imgUrl && imgUrl.length > 0 ? imgUrl : getFallbackAvatar(teamId);
|
||||
return (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={img}
|
||||
alt={alt || 'Team avatar'}
|
||||
className={classNames(
|
||||
'rounded-full bg-vega-clight-700 dark:bg-vega-cdark-700 shrink-0',
|
||||
{
|
||||
'w-20 h-20 lg:w-[112px] lg:h-[112px]': size === 'large',
|
||||
'w-10 h-10': size === 'small',
|
||||
}
|
||||
)}
|
||||
referrerPolicy="no-referrer"
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -202,6 +202,13 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
||||
{t('Portfolio')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
{featureFlags.TEAM_COMPETITION && (
|
||||
<NavbarItem>
|
||||
<NavbarLink to={Links.COMPETITIONS()} onClick={onClick}>
|
||||
{t('Competitions')}
|
||||
</NavbarLink>
|
||||
</NavbarItem>
|
||||
)}
|
||||
{featureFlags.REFERRALS && (
|
||||
<NavbarItem>
|
||||
<NavbarLink end={false} to={Links.REFERRALS()} onClick={onClick}>
|
||||
|
||||
@@ -11,14 +11,21 @@ type TableColumnDefinition = {
|
||||
name: string;
|
||||
tooltip?: string;
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
testId?: string;
|
||||
};
|
||||
|
||||
type DataEntry = {
|
||||
[key: TableColumnDefinition['name']]: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
type TableProps = {
|
||||
columns: TableColumnDefinition[];
|
||||
data: Record<TableColumnDefinition['name'] | 'className', React.ReactNode>[];
|
||||
data: DataEntry[];
|
||||
noHeader?: boolean;
|
||||
noCollapse?: boolean;
|
||||
onRowClick?: (index: number) => void;
|
||||
};
|
||||
|
||||
const INNER_BORDER_STYLE = `border-b ${BORDER_COLOR}`;
|
||||
@@ -34,6 +41,7 @@ export const Table = forwardRef<
|
||||
noHeader = false,
|
||||
noCollapse = false,
|
||||
className,
|
||||
onRowClick,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
@@ -41,13 +49,14 @@ export const Table = forwardRef<
|
||||
const header = (
|
||||
<thead className={classNames({ 'max-md:hidden': !noCollapse })}>
|
||||
<tr>
|
||||
{columns.map(({ displayName, name, tooltip }) => (
|
||||
{columns.map(({ displayName, name, tooltip, headerClassName }) => (
|
||||
<th
|
||||
key={name}
|
||||
col-id={name}
|
||||
className={classNames(
|
||||
'px-5 py-3 text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
|
||||
INNER_BORDER_STYLE
|
||||
INNER_BORDER_STYLE,
|
||||
headerClassName
|
||||
)}
|
||||
>
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
@@ -79,12 +88,17 @@ export const Table = forwardRef<
|
||||
>
|
||||
{!noHeader && header}
|
||||
<tbody>
|
||||
{data.map((d, i) => (
|
||||
{data.map((dataEntry, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className={classNames(d['className'] as string, {
|
||||
className={classNames(dataEntry['className'] as string, {
|
||||
'max-md:flex flex-col w-full': !noCollapse,
|
||||
})}
|
||||
onClick={() => {
|
||||
if (onRowClick) {
|
||||
onRowClick(i);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{columns.map(({ name, displayName, className, testId }, j) => (
|
||||
<td
|
||||
@@ -114,7 +128,9 @@ export const Table = forwardRef<
|
||||
{displayName}
|
||||
</span>
|
||||
)}
|
||||
<span data-testid={`${testId || name}-${i}`}>{d[name]}</span>
|
||||
<span data-testid={`${testId || name}-${i}`}>
|
||||
{dataEntry[name]}
|
||||
</span>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
||||
VEGA_VERSION=v0.73.10
|
||||
LOCAL_SERVER=false
|
||||
LOCAL_SERVER=true
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import pytest
|
||||
from playwright.sync_api import expect, Page
|
||||
from vega_sim.null_service import VegaServiceNull
|
||||
from actions.utils import next_epoch
|
||||
from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D
|
||||
|
||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||
def test_teams(vega: VegaServiceNull, page: Page):
|
||||
|
||||
# submit_transaction will fail with spam statistics error if block height is 0
|
||||
# so go forward at least one block
|
||||
vega.wait_fn(1)
|
||||
|
||||
for key in [PARTY_A, PARTY_B, PARTY_C, PARTY_D]:
|
||||
vega.wallet.create_key(key.name)
|
||||
|
||||
team_name = create_team(vega)
|
||||
|
||||
next_epoch(vega)
|
||||
|
||||
# would be better to derive the team id from the signature if possible but
|
||||
# vega.submit_transaction does not return anything
|
||||
teams = vega.list_teams()
|
||||
|
||||
# list_teams actually returns a dictionary {"team_id": Team}
|
||||
team_id = list(teams.keys())[0]
|
||||
|
||||
vega.apply_referral_code(PARTY_B.name, team_id)
|
||||
|
||||
# go to next epoch so we can check joinedAt and joinedAtEpoch appropriately
|
||||
next_epoch(vega)
|
||||
|
||||
vega.apply_referral_code(PARTY_C.name, team_id)
|
||||
|
||||
next_epoch(vega)
|
||||
|
||||
vega.apply_referral_code(PARTY_D.name, team_id)
|
||||
|
||||
vega.wait_fn(1)
|
||||
vega.forward("10s")
|
||||
vega.wait_for_total_catchup()
|
||||
|
||||
page.goto(f"/#/competitions/team/{team_id}")
|
||||
expect(page.get_by_role('heading', level=1)).to_have_text(team_name)
|
||||
expect(page.get_by_text('Members (3)')).to_be_visible()
|
||||
|
||||
def create_team(vega: VegaServiceNull):
|
||||
team_name = "Foobar"
|
||||
|
||||
vega.create_referral_set(
|
||||
key_name=PARTY_A.name,
|
||||
name=team_name,
|
||||
team_url="https://vega.xyz",
|
||||
avatar_url="http://placekitten.com/200/200",
|
||||
closed=False
|
||||
)
|
||||
|
||||
return team_name
|
||||
@@ -16,7 +16,11 @@ export const Routes = {
|
||||
REFERRALS: '/referrals',
|
||||
REFERRALS_APPLY_CODE: '/referrals/apply-code',
|
||||
REFERRALS_CREATE_CODE: '/referrals/create-code',
|
||||
TEAMS: '/teams',
|
||||
TEAM: '/competitions/team/:teamId',
|
||||
COMPETITIONS: '/competitions',
|
||||
COMPETITIONS_TEAMS: '/competitions/teams',
|
||||
COMPETITIONS_CREATE_TEAM: '/competitions/teams/create',
|
||||
COMPETITIONS_TEAM: '/competitions/teams/:teamId',
|
||||
FEES: '/fees',
|
||||
REWARDS: '/rewards',
|
||||
} as const;
|
||||
@@ -41,7 +45,12 @@ export const Links: ConsoleLinks = {
|
||||
REFERRALS: () => Routes.REFERRALS,
|
||||
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
|
||||
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
|
||||
TEAMS: () => Routes.TEAMS,
|
||||
TEAM: (teamId: string) => trimEnd(Routes.TEAM.replace(':teamId', teamId)),
|
||||
COMPETITIONS: () => Routes.COMPETITIONS,
|
||||
COMPETITIONS_TEAMS: () => Routes.COMPETITIONS_TEAMS,
|
||||
COMPETITIONS_CREATE_TEAM: () => Routes.COMPETITIONS_CREATE_TEAM,
|
||||
COMPETITIONS_TEAM: (teamId: string) =>
|
||||
Routes.COMPETITIONS_TEAM.replace(':teamId', teamId),
|
||||
FEES: () => Routes.FEES,
|
||||
REWARDS: () => Routes.REWARDS,
|
||||
};
|
||||
|
||||
@@ -14,7 +14,6 @@ import { Withdraw } from '../client-pages/withdraw';
|
||||
import { Transfer } from '../client-pages/transfer';
|
||||
import { Fees } from '../client-pages/fees';
|
||||
import { Rewards } from '../client-pages/rewards';
|
||||
import { Teams } from '../client-pages/teams';
|
||||
import { Routes as AppRoutes } from '../lib/links';
|
||||
import { LayoutWithSky } from '../client-pages/referrals/layout';
|
||||
import { Referrals } from '../client-pages/referrals/referrals';
|
||||
@@ -30,6 +29,9 @@ import { PortfolioSidebar } from '../client-pages/portfolio/portfolio-sidebar';
|
||||
import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar';
|
||||
import { MarketsSidebar } from '../client-pages/markets/markets-sidebar';
|
||||
import { useT } from '../lib/use-t';
|
||||
import { CompetitionsHome } from '../client-pages/competitions/competitions-home';
|
||||
import { CompetitionsTeams } from '../client-pages/competitions/competitions-teams';
|
||||
import { CompetitionsTeam } from '../client-pages/competitions/competitions-team';
|
||||
|
||||
// These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM
|
||||
// Using dynamic imports is a workaround for this until pennant is published as ESM
|
||||
@@ -95,8 +97,30 @@ export const useRouterConfig = (): RouteObject[] => {
|
||||
: undefined,
|
||||
featureFlags.TEAM_COMPETITION
|
||||
? {
|
||||
path: AppRoutes.TEAMS,
|
||||
element: <Teams />,
|
||||
path: AppRoutes.COMPETITIONS,
|
||||
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
// pages with planets and stars
|
||||
{
|
||||
element: <LayoutWithSky />,
|
||||
children: [
|
||||
{ index: true, element: <CompetitionsHome /> },
|
||||
{
|
||||
path: AppRoutes.COMPETITIONS_TEAMS,
|
||||
element: <CompetitionsTeams />,
|
||||
},
|
||||
],
|
||||
},
|
||||
// pages with blurred background
|
||||
{
|
||||
path: AppRoutes.COMPETITIONS_TEAM,
|
||||
element: <CompetitionsTeam />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
{
|
||||
|
||||
|
After Width: | Height: | Size: 668 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 100 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 98 KiB |
@@ -161,6 +161,7 @@ export const useProtocolUpgradeProposalLink = () => {
|
||||
export const EXPLORER_TX = '/txs/:hash';
|
||||
export const EXPLORER_ORACLE = '/oracles/:id';
|
||||
export const EXPLORER_MARKET = '/markets/:id';
|
||||
export const EXPLORER_PARTIES = '/parties/:id';
|
||||
|
||||
// Etherscan pages
|
||||
export const ETHERSCAN_ADDRESS = '/address/:hash';
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"About the referral program": "About the referral program",
|
||||
"Active": "Active",
|
||||
"All": "All",
|
||||
"Amount earned": "Amount earned",
|
||||
"An unknown error occurred.": "An unknown error occurred.",
|
||||
"Anonymous": "Anonymous",
|
||||
"Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction": "Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction",
|
||||
@@ -79,6 +80,7 @@
|
||||
"Earned by me": "Earned by me",
|
||||
"Enactment date reached and usual auction exit checks pass": "Enactment date reached and usual auction exit checks pass",
|
||||
"Environment not configured": "Environment not configured",
|
||||
"Epoch": "Epoch",
|
||||
"epochs in referral set": "epochs in referral set",
|
||||
"Epochs in set": "Epochs in set",
|
||||
"Epochs to next tier": "Epochs to next tier",
|
||||
@@ -87,6 +89,7 @@
|
||||
"Experiment for free with virtual assets on <0>Fairground Testnet</0>": "Experiment for free with virtual assets on <0>Fairground Testnet</0>",
|
||||
"Expiry": "Expiry",
|
||||
"Explore": "Explore",
|
||||
"Favorite game": "Favorite game",
|
||||
"Fees": "Fees",
|
||||
"Fees paid": "Fees paid",
|
||||
"Fees work like a CEX with no per-transaction gas for orders": "Fees work like a CEX with no per-transaction gas for orders",
|
||||
@@ -103,6 +106,7 @@
|
||||
"Funding Rate": "Funding Rate",
|
||||
"Funding rate": "Funding rate",
|
||||
"Futures": "Futures",
|
||||
"Games ({{count}})": "Games ({{count}})",
|
||||
"Generate a referral code to share with your friends and start earning commission.": "Generate a referral code to share with your friends and start earning commission.",
|
||||
"Generate code": "Generate code",
|
||||
"Get started": "Get started",
|
||||
@@ -132,6 +136,11 @@
|
||||
"INTERVAL_I6H": "6H",
|
||||
"INTERVAL_I1D": "1D",
|
||||
"Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.": "Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.",
|
||||
"Join this team": "Join this team",
|
||||
"Joined": "Joined",
|
||||
"Joined at": "Joined at",
|
||||
"Joined epoch": "Joined epoch",
|
||||
"Last {{count}} game results": "Last {{count}} game results",
|
||||
"Learn about providing liquidity": "Learn about providing liquidity",
|
||||
"Learn more": "Learn more",
|
||||
"Ledger entries": "Ledger entries",
|
||||
@@ -150,6 +159,8 @@
|
||||
"Market specification": "Market specification",
|
||||
"Market triggers cancellation or governance vote has passed to cancel": "Market triggers cancellation or governance vote has passed to cancel",
|
||||
"Markets": "Markets",
|
||||
"Members": "Members",
|
||||
"Members ({{count}})": "Members ({{count}})",
|
||||
"Menu": "Menu",
|
||||
"Metamask Snap <0>quick start</0>": "Metamask Snap <0>quick start</0>",
|
||||
"Min. epochs": "Min. epochs",
|
||||
@@ -169,10 +180,12 @@
|
||||
"No deposits": "No deposits",
|
||||
"No funding history data": "No funding history data",
|
||||
"No future markets.": "No future markets.",
|
||||
"No games": "No games",
|
||||
"No ledger entries to export": "No ledger entries to export",
|
||||
"No market": "No market",
|
||||
"No markets": "No markets",
|
||||
"No markets.": "No markets.",
|
||||
"No members": "No members",
|
||||
"No MetaMask version that supports snaps detected. Learn more about <0>MetaMask Snaps</0>": "No MetaMask version that supports snaps detected. Learn more about <0>MetaMask Snaps</0>",
|
||||
"No open orders": "No open orders",
|
||||
"No orders": "No orders",
|
||||
@@ -185,6 +198,7 @@
|
||||
"No third party has access to your funds.": "No third party has access to your funds.",
|
||||
"No volume discount program active": "No volume discount program active",
|
||||
"No withdrawals": "No withdrawals",
|
||||
"No. of participating teams": "No. of participating teams",
|
||||
"Node: {{VEGA_URL}} is unsuitable": "Node: {{VEGA_URL}} is unsuitable",
|
||||
"Non-custodial and pseudonymous": "Non-custodial and pseudonymous",
|
||||
"None": "None",
|
||||
@@ -204,6 +218,10 @@
|
||||
"pastEpochs_other": "Past {{count}} epochs",
|
||||
"Pennant": "Pennant",
|
||||
"Perpetuals": "Perpetuals",
|
||||
"place_ordinal_one": "{{count}}st",
|
||||
"place_ordinal_two": "{{count}}nd",
|
||||
"place_ordinal_few": "{{count}}rd",
|
||||
"place_ordinal_other": "{{count}}th",
|
||||
"Please choose another market from the <0>market list</0>": "Please choose another market from the <0>market list</0>",
|
||||
"Please connect Vega wallet": "Please connect Vega wallet",
|
||||
"Portfolio": "Portfolio",
|
||||
@@ -218,10 +236,12 @@
|
||||
"Purpose built proof of stake blockchain": "Purpose built proof of stake blockchain",
|
||||
"qUSD": "qUSD",
|
||||
"qUSD provides a rough USD equivalent of balances across all assets using the value of \"Quantum\" for that asset": "qUSD provides a rough USD equivalent of balances across all assets using the value of \"Quantum\" for that asset",
|
||||
"Rank": "Rank",
|
||||
"Read the terms": "Read the terms",
|
||||
"Ready to trade": "Ready to trade",
|
||||
"Ready to trade with real funds? <0>Switch to Mainnet</0>": "Ready to trade with real funds? <0>Switch to Mainnet</0>",
|
||||
"Redeem rewards": "Redeem rewards",
|
||||
"Referee": "Referee",
|
||||
"Referral benefits": "Referral benefits",
|
||||
"Referral discount": "Referral discount",
|
||||
"Referrals": "Referrals",
|
||||
@@ -293,10 +313,14 @@
|
||||
"to": "to",
|
||||
"To protect the network from spam, you must have at least {{requiredFunds}} qUSD of any asset on the network to proceed.": "To protect the network from spam, you must have at least {{requiredFunds}} qUSD of any asset on the network to proceed.",
|
||||
"Toast location": "Toast location",
|
||||
"Total amount of rewards paid out to this team in qUSD": "Total amount of rewards paid out to this team in qUSD",
|
||||
"Total discount": "Total discount",
|
||||
"Total distributed": "Total distributed",
|
||||
"Total fee after discount": "Total fee after discount",
|
||||
"Total fee before discount": "Total fee before discount",
|
||||
"Total games": "Total games",
|
||||
"Total number of games this team has participated in": "Total number of games this team has participated in",
|
||||
"Total volume": "Total volume",
|
||||
"totalCommission": "Total commission (<0>last {{count}} epochs</0>)",
|
||||
"totalCommission_one": "Total commission (<0>last {{count}} epoch</0>)",
|
||||
"totalCommission_other": "Total commission (<0>last {{count}} epochs</0>)",
|
||||
@@ -310,6 +334,7 @@
|
||||
"Trading on market {{name}} may stop. There are open proposals to close this market": "Trading on market {{name}} may stop. There are open proposals to close this market",
|
||||
"Trading on market {{name}} will stop on {{date}}": "Trading on market {{name}} will stop on {{date}}",
|
||||
"Transfer": "Transfer",
|
||||
"Type": "Type",
|
||||
"Unknown": "Unknown",
|
||||
"Unknown settlement date": "Unknown settlement date",
|
||||
"Vega chart": "Vega chart",
|
||||
@@ -365,10 +390,11 @@
|
||||
"Rewards funded using the pro-rata strategy should be distributed pro-rata by each entity's reward metric scaled by any active multipliers that party has": " Rewards funded using the pro-rata strategy should be distributed pro-rata by each entity's reward metric scaled by any active multipliers that party has",
|
||||
"Tier {{tier}}": "Tier {{tier}}",
|
||||
"Reward {{reward}}x": "Reward {{reward}}x",
|
||||
"Rewards paid": "Rewards paid",
|
||||
"Reward bonus": "Reward bonus",
|
||||
"Vesting {{vesting}}x": "Vesting {{vesting}}x",
|
||||
"Tier {{userTier}}": "Tier {{userTier}}",
|
||||
"{{reward}}x": "{{reward}}x",
|
||||
"Reward bonus": "Reward bonus",
|
||||
"Activity Streak": "Activity Streak",
|
||||
"userActive": "{{active}} trader: {{count}} epochs so far",
|
||||
"(Tier {{tier}} as of last epoch)": "(Tier {{tier}} as of last epoch)",
|
||||
|
||||
@@ -8,13 +8,15 @@ export type VegaIconSize = 8 | 10 | 12 | 13 | 14 | 16 | 18 | 20 | 24 | 28 | 32;
|
||||
export interface VegaIconProps {
|
||||
name: VegaIconNames;
|
||||
size?: VegaIconSize;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const VegaIcon = ({ size = 16, name }: VegaIconProps) => {
|
||||
export const VegaIcon = ({ size = 16, name, className }: VegaIconProps) => {
|
||||
const effectiveClassName = classNames(
|
||||
'inline-block',
|
||||
'align-text-bottom',
|
||||
'fill-current stroke-none'
|
||||
'fill-current stroke-none',
|
||||
className
|
||||
);
|
||||
const Element = VegaIconNameMap[name];
|
||||
return (
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
quantumDecimalPlaces,
|
||||
toDecimal,
|
||||
toNumberParts,
|
||||
formatNumberRounded,
|
||||
} from './number';
|
||||
|
||||
describe('number utils', () => {
|
||||
@@ -246,3 +247,30 @@ describe('getUnlimitedThreshold', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('formatNumberRounded', () => {
|
||||
it('rounds number with symbol', () => {
|
||||
expect(formatNumberRounded(new BigNumber(1))).toBe('1');
|
||||
expect(formatNumberRounded(new BigNumber(1_000))).toBe('1,000');
|
||||
expect(formatNumberRounded(new BigNumber(1_000_000))).toBe('1m');
|
||||
expect(formatNumberRounded(new BigNumber(1_000_000_000))).toBe('1b');
|
||||
expect(formatNumberRounded(new BigNumber(1_000_000_000_000))).toBe('1t');
|
||||
});
|
||||
|
||||
it('respects the limit parameter', () => {
|
||||
expect(formatNumberRounded(new BigNumber(1_000), '1e3')).toBe('1k');
|
||||
expect(formatNumberRounded(new BigNumber(1_000_000), '1e9')).toBe(
|
||||
'1,000,000'
|
||||
);
|
||||
expect(formatNumberRounded(new BigNumber(9_999_999), '1e9')).toBe(
|
||||
'9,999,999'
|
||||
);
|
||||
expect(formatNumberRounded(new BigNumber(1_000_000_000), '1e9')).toBe('1b');
|
||||
expect(formatNumberRounded(new BigNumber(1_000_000_000), '1e12')).toBe(
|
||||
'1,000,000,000'
|
||||
);
|
||||
expect(formatNumberRounded(new BigNumber(1_000_000_000_000), '1e9')).toBe(
|
||||
'1t'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,7 +184,10 @@ export const isNumeric = (
|
||||
* Format a number greater than 1 million with m for million, b for billion
|
||||
* and t for trillion
|
||||
*/
|
||||
export const formatNumberRounded = (num: BigNumber) => {
|
||||
export const formatNumberRounded = (
|
||||
num: BigNumber,
|
||||
limit: '1e12' | '1e9' | '1e6' | '1e3' = '1e6'
|
||||
) => {
|
||||
let value = '';
|
||||
|
||||
const format = (divisor: string) => {
|
||||
@@ -194,15 +197,29 @@ export const formatNumberRounded = (num: BigNumber) => {
|
||||
|
||||
if (num.isGreaterThan(new BigNumber('1e14'))) {
|
||||
value = '>100t';
|
||||
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e12'))) {
|
||||
} else if (
|
||||
num.isGreaterThanOrEqualTo(limit) &&
|
||||
num.isGreaterThanOrEqualTo(new BigNumber('1e12'))
|
||||
) {
|
||||
// Trillion
|
||||
value = `${format('1e12')}t`;
|
||||
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e9'))) {
|
||||
} else if (
|
||||
num.isGreaterThanOrEqualTo(limit) &&
|
||||
num.isGreaterThanOrEqualTo(new BigNumber('1e9'))
|
||||
) {
|
||||
// Billion
|
||||
value = `${format('1e9')}b`;
|
||||
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e6'))) {
|
||||
} else if (
|
||||
num.isGreaterThanOrEqualTo(limit) &&
|
||||
num.isGreaterThanOrEqualTo(new BigNumber('1e6'))
|
||||
) {
|
||||
// Million
|
||||
value = `${format('1e6')}m`;
|
||||
} else if (
|
||||
num.isGreaterThanOrEqualTo(limit) &&
|
||||
num.isGreaterThanOrEqualTo(new BigNumber('1e3'))
|
||||
) {
|
||||
value = `${format('1e3')}k`;
|
||||
} else {
|
||||
value = formatNumber(num);
|
||||
}
|
||||
|
||||