feat(trading): competitions (#5621)
Co-authored-by: asiaznik <artur@vegaprotocol.io> Co-authored-by: Ben <ben@vega.xyz>
@ -26,6 +26,7 @@ NX_ICEBERG_ORDERS=true
|
|||||||
NX_METAMASK_SNAPS=true
|
NX_METAMASK_SNAPS=true
|
||||||
NX_REFERRALS=true
|
NX_REFERRALS=true
|
||||||
# NX_DISABLE_CLOSE_POSITION=false
|
# NX_DISABLE_CLOSE_POSITION=false
|
||||||
|
NX_TEAM_COMPETITION=true
|
||||||
|
|
||||||
NX_TENDERMINT_URL=https://be.vega.community
|
NX_TENDERMINT_URL=https://be.vega.community
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||||
|
@ -28,3 +28,8 @@ NX_REFERRALS=true
|
|||||||
|
|
||||||
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
|
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
|
||||||
|
|
||||||
|
NX_TEAM_COMPETITION=true
|
||||||
|
|
||||||
|
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
||||||
|
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
||||||
|
@ -26,6 +26,7 @@ NX_ICEBERG_ORDERS=true
|
|||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
NX_METAMASK_SNAPS=true
|
NX_METAMASK_SNAPS=true
|
||||||
NX_REFERRALS=true
|
NX_REFERRALS=true
|
||||||
|
NX_TEAM_COMPETITION=true
|
||||||
|
|
||||||
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
||||||
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
||||||
|
@ -26,6 +26,7 @@ NX_ISOLATED_MARGIN=true
|
|||||||
NX_ICEBERG_ORDERS=true
|
NX_ICEBERG_ORDERS=true
|
||||||
NX_METAMASK_SNAPS=true
|
NX_METAMASK_SNAPS=true
|
||||||
NX_REFERRALS=true
|
NX_REFERRALS=true
|
||||||
|
NX_TEAM_COMPETITION=true
|
||||||
|
|
||||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
import { useSearchParams } from 'react-router-dom';
|
||||||
|
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||||
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
||||||
|
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
|
||||||
|
import { RainbowButton } from '../../components/rainbow-button';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
|
import { Box } from '../../components/competitions/box';
|
||||||
|
import { LayoutWithGradient } from '../../components/layouts-inner';
|
||||||
|
import { Links } from '../../lib/links';
|
||||||
|
import { TeamForm, TransactionType } from './team-form';
|
||||||
|
|
||||||
|
export const CompetitionsCreateTeam = () => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const isSolo = Boolean(searchParams.get('solo'));
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
usePageTitle(t('Create a team'));
|
||||||
|
|
||||||
|
const { isReadOnly, pubKey } = useVegaWallet();
|
||||||
|
const openWalletDialog = useVegaWalletDialogStore(
|
||||||
|
(store) => store.openVegaWalletDialog
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary feature="create-team">
|
||||||
|
<LayoutWithGradient>
|
||||||
|
<div className="mx-auto md:w-2/3 max-w-xl">
|
||||||
|
<Box className="flex flex-col gap-4">
|
||||||
|
<h1 className="calt text-2xl lg:text-3xl xl:text-4xl">
|
||||||
|
{t('Create a team')}
|
||||||
|
</h1>
|
||||||
|
{pubKey && !isReadOnly ? (
|
||||||
|
<CreateTeamFormContainer isSolo={isSolo} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
'Create a team to participate in team based rewards as well as access the discount benefits of the current referral program.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<RainbowButton variant="border" onClick={openWalletDialog}>
|
||||||
|
{t('Connect wallet')}
|
||||||
|
</RainbowButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</LayoutWithGradient>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
|
||||||
|
const t = useT();
|
||||||
|
const createLink = useLinks(DApp.Governance);
|
||||||
|
|
||||||
|
const { err, status, code, isEligible, requiredStake, onSubmit } =
|
||||||
|
useReferralSetTransaction({
|
||||||
|
onSuccess: (code) => {
|
||||||
|
// For some reason team creation takes a long time, too long even to make
|
||||||
|
// polling viable, so its not feasible to navigate to the team page
|
||||||
|
// after creation
|
||||||
|
//
|
||||||
|
// navigate(Links.COMPETITIONS_TEAM(code));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (status === 'confirmed') {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-start gap-2">
|
||||||
|
<p className="text-sm">{t('Team creation transaction successful')}</p>
|
||||||
|
{code && (
|
||||||
|
<>
|
||||||
|
<p className="text-sm">
|
||||||
|
Your team ID is:{' '}
|
||||||
|
<span className="font-mono break-all">{code}</span>
|
||||||
|
</p>
|
||||||
|
<TradingAnchorButton
|
||||||
|
href={Links.COMPETITIONS_TEAM(code)}
|
||||||
|
intent={Intent.Info}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('View team')}
|
||||||
|
</TradingAnchorButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isEligible) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{requiredStake !== undefined && (
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
'You need at least {{requiredStake}} VEGA staked to generate a referral code and participate in the referral program.',
|
||||||
|
{
|
||||||
|
requiredStake: addDecimalsFormatNumber(
|
||||||
|
requiredStake.toString(),
|
||||||
|
18
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<TradingAnchorButton
|
||||||
|
href={createLink(TokenStaticLinks.ASSOCIATE)}
|
||||||
|
intent={Intent.Primary}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{t('Stake some $VEGA now')}
|
||||||
|
</TradingAnchorButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TeamForm
|
||||||
|
type={TransactionType.CreateReferralSet}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
status={status}
|
||||||
|
err={err}
|
||||||
|
isSolo={isSolo}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
133
apps/trading/client-pages/competitions/competitions-home.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
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 '../../lib/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 '../../lib/hooks/use-teams';
|
||||||
|
import take from 'lodash/take';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
|
||||||
|
export const CompetitionsHome = () => {
|
||||||
|
const t = useT();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
usePageTitle(t('Competitions'));
|
||||||
|
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
|
||||||
|
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_SOLO());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
};
|
238
apps/trading/client-pages/competitions/competitions-team.tsx
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
import { useState, type ButtonHTMLAttributes } from 'react';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import { Splash, truncateMiddle, Loader } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { Table } from '../../components/table';
|
||||||
|
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/utils';
|
||||||
|
import {
|
||||||
|
useTeam,
|
||||||
|
type TeamStats as ITeamStats,
|
||||||
|
type Team as TeamType,
|
||||||
|
type Member,
|
||||||
|
type TeamGame,
|
||||||
|
} from '../../lib/hooks/use-team';
|
||||||
|
import { DApp, EXPLORER_PARTIES, useLinks } from '@vegaprotocol/environment';
|
||||||
|
import { TeamAvatar } from '../../components/competitions/team-avatar';
|
||||||
|
import { TeamStats } from '../../components/competitions/team-stats';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
|
import { LayoutWithGradient } from '../../components/layouts-inner';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import { JoinTeam } from './join-team';
|
||||||
|
import { UpdateTeamButton } from './update-team-button';
|
||||||
|
|
||||||
|
export const CompetitionsTeam = () => {
|
||||||
|
const t = useT();
|
||||||
|
const { teamId } = useParams<{ teamId: string }>();
|
||||||
|
usePageTitle([t('Competitions'), t('Team')]);
|
||||||
|
return (
|
||||||
|
<ErrorBoundary feature="team">
|
||||||
|
<TeamPageContainer teamId={teamId} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
|
||||||
|
const t = useT();
|
||||||
|
const { pubKey } = useVegaWallet();
|
||||||
|
const { team, partyTeam, stats, members, games, loading, refetch } = useTeam(
|
||||||
|
teamId,
|
||||||
|
pubKey || undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Splash>
|
||||||
|
<Loader />
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!team) {
|
||||||
|
return (
|
||||||
|
<Splash>
|
||||||
|
<p>{t('Page not found')}</p>
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TeamPage
|
||||||
|
team={team}
|
||||||
|
partyTeam={partyTeam}
|
||||||
|
stats={stats}
|
||||||
|
members={members}
|
||||||
|
games={games}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TeamPage = ({
|
||||||
|
team,
|
||||||
|
partyTeam,
|
||||||
|
stats,
|
||||||
|
members,
|
||||||
|
games,
|
||||||
|
refetch,
|
||||||
|
}: {
|
||||||
|
team: TeamType;
|
||||||
|
partyTeam?: TeamType;
|
||||||
|
stats?: ITeamStats;
|
||||||
|
members?: Member[];
|
||||||
|
games?: TeamGame[];
|
||||||
|
refetch: () => void;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const [showGames, setShowGames] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutWithGradient>
|
||||||
|
<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"
|
||||||
|
data-testid="team-name"
|
||||||
|
>
|
||||||
|
{team.name}
|
||||||
|
</h1>
|
||||||
|
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
|
||||||
|
<UpdateTeamButton team={team} />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<TeamStats stats={stats} members={members} games={games} />
|
||||||
|
<section>
|
||||||
|
<div className="flex gap-4 lg:gap-8 mb-4 border-b border-default">
|
||||||
|
<ToggleButton
|
||||||
|
active={showGames}
|
||||||
|
onClick={() => setShowGames(true)}
|
||||||
|
data-testid="games-toggle"
|
||||||
|
>
|
||||||
|
{t('Games ({{count}})', { count: games ? games.length : 0 })}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
active={!showGames}
|
||||||
|
onClick={() => setShowGames(false)}
|
||||||
|
data-testid="members-toggle"
|
||||||
|
>
|
||||||
|
{t('Members ({{count}})', {
|
||||||
|
count: members ? members.length : 0,
|
||||||
|
})}
|
||||||
|
</ToggleButton>
|
||||||
|
</div>
|
||||||
|
{showGames ? <Games games={games} /> : <Members members={members} />}
|
||||||
|
</section>
|
||||||
|
</LayoutWithGradient>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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:table-cell',
|
||||||
|
className: 'hidden md:table-cell',
|
||||||
|
},
|
||||||
|
{ name: 'type', displayName: t('Type') },
|
||||||
|
{ name: 'amount', displayName: t('Amount earned') },
|
||||||
|
{
|
||||||
|
name: 'participatingTeams',
|
||||||
|
displayName: t('No. of participating teams'),
|
||||||
|
headerClassName: 'hidden md:table-cell',
|
||||||
|
className: 'hidden md:table-cell',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'participatingMembers',
|
||||||
|
displayName: t('No. of participating members'),
|
||||||
|
headerClassName: 'hidden md:table-cell',
|
||||||
|
className: 'hidden md:table-cell',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
data={games.map((game) => ({
|
||||||
|
rank: game.team.rank,
|
||||||
|
epoch: game.epoch,
|
||||||
|
type: DispatchMetricLabels[game.team.rewardMetric as DispatchMetric],
|
||||||
|
amount: formatNumber(game.team.totalRewardsEarned),
|
||||||
|
participatingTeams: game.entities.length,
|
||||||
|
participatingMembers: 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: <RefereeLink pubkey={m.referee} />,
|
||||||
|
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
|
||||||
|
joinedAtEpoch: Number(m.joinedAtEpoch),
|
||||||
|
})),
|
||||||
|
'joinedAtEpoch',
|
||||||
|
'desc'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
columns={[
|
||||||
|
{ name: 'referee', displayName: t('Referee') },
|
||||||
|
{
|
||||||
|
name: 'joinedAt',
|
||||||
|
displayName: t('Joined at'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'joinedAtEpoch',
|
||||||
|
displayName: t('Joined epoch'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
data={data}
|
||||||
|
noCollapse={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RefereeLink = ({ pubkey }: { pubkey: string }) => {
|
||||||
|
const linkCreator = useLinks(DApp.Explorer);
|
||||||
|
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link to={link} target="_blank" className="underline underline-offset-4">
|
||||||
|
{truncateMiddle(pubkey)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,67 @@
|
|||||||
|
import { ErrorBoundary } from '@sentry/react';
|
||||||
|
import { CompetitionsHeader } from '../../components/competitions/competitions-header';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { useTeams } from '../../lib/hooks/use-teams';
|
||||||
|
import { CompetitionsLeaderboard } from '../../components/competitions/competitions-leaderboard';
|
||||||
|
import {
|
||||||
|
Input,
|
||||||
|
Loader,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
|
||||||
|
export const CompetitionsTeams = () => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
usePageTitle([t('Competitions'), t('Teams')]);
|
||||||
|
|
||||||
|
const { data: teamsData, loading: teamsLoading } = useTeams({
|
||||||
|
sortByField: ['totalQuantumRewards'],
|
||||||
|
order: 'desc',
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
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>
|
||||||
|
</CompetitionsHeader>
|
||||||
|
|
||||||
|
<div className="mb-6 flex justify-end">
|
||||||
|
<div className="w-full md:w-60 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,106 @@
|
|||||||
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
import { Box } from '../../components/competitions/box';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||||
|
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { RainbowButton } from '../../components/rainbow-button';
|
||||||
|
import { Link, Navigate, useParams } from 'react-router-dom';
|
||||||
|
import { Links } from '../../lib/links';
|
||||||
|
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
||||||
|
import { type FormFields, TeamForm, TransactionType } from './team-form';
|
||||||
|
import { useTeam } from '../../lib/hooks/use-team';
|
||||||
|
import { LayoutWithGradient } from '../../components/layouts-inner';
|
||||||
|
|
||||||
|
export const CompetitionsUpdateTeam = () => {
|
||||||
|
const t = useT();
|
||||||
|
usePageTitle([t('Competitions'), t('Update a team')]);
|
||||||
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
const openWalletDialog = useVegaWalletDialogStore(
|
||||||
|
(store) => store.openVegaWalletDialog
|
||||||
|
);
|
||||||
|
const { teamId } = useParams<{ teamId: string }>();
|
||||||
|
if (!teamId) {
|
||||||
|
return <Navigate to={Links.COMPETITIONS()} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary feature="update-team">
|
||||||
|
<LayoutWithGradient>
|
||||||
|
<div className="mx-auto md:w-2/3 max-w-xl">
|
||||||
|
<Box className="flex flex-col gap-4">
|
||||||
|
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
|
||||||
|
{t('Update a team')}
|
||||||
|
</h1>
|
||||||
|
{pubKey && !isReadOnly ? (
|
||||||
|
<UpdateTeamFormContainer teamId={teamId} pubKey={pubKey} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>{t('Connect to update the details of your team.')}</p>
|
||||||
|
<RainbowButton variant="border" onClick={openWalletDialog}>
|
||||||
|
{t('Connect wallet')}
|
||||||
|
</RainbowButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</LayoutWithGradient>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdateTeamFormContainer = ({
|
||||||
|
teamId,
|
||||||
|
pubKey,
|
||||||
|
}: {
|
||||||
|
teamId: string;
|
||||||
|
pubKey: string;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const { team, loading, error } = useTeam(teamId, pubKey);
|
||||||
|
|
||||||
|
const { err, status, onSubmit } = useReferralSetTransaction({
|
||||||
|
onSuccess: () => {
|
||||||
|
// NOOP
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loader size="small" />;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Splash className="gap-1">
|
||||||
|
<span>{t('Something went wrong.')}</span>
|
||||||
|
<Link to={Links.COMPETITIONS_TEAM(teamId)} className="underline">
|
||||||
|
{t("Go back to the team's profile")}
|
||||||
|
</Link>
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMyTeam = team?.referrer === pubKey;
|
||||||
|
if (!isMyTeam) {
|
||||||
|
return <Navigate to={Links.COMPETITIONS_TEAM(teamId)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues: FormFields = {
|
||||||
|
id: team.teamId,
|
||||||
|
name: team.name,
|
||||||
|
url: team.teamUrl,
|
||||||
|
avatarUrl: team.avatarUrl,
|
||||||
|
private: team.closed,
|
||||||
|
allowList: team.allowList.join(','),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TeamForm
|
||||||
|
type={TransactionType.UpdateReferralSet}
|
||||||
|
status={status}
|
||||||
|
err={err}
|
||||||
|
isSolo={team.closed}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
88
apps/trading/client-pages/competitions/join-team.spec.tsx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { JoinButton } from './join-team';
|
||||||
|
import { type Team } from '../../lib/hooks/use-team';
|
||||||
|
|
||||||
|
describe('JoinButton', () => {
|
||||||
|
const teamA = {
|
||||||
|
teamId: 'teamA',
|
||||||
|
name: 'Team A',
|
||||||
|
referrer: 'referrerA',
|
||||||
|
} as Team;
|
||||||
|
|
||||||
|
const teamB = {
|
||||||
|
teamId: 'teamB',
|
||||||
|
name: 'Team B',
|
||||||
|
referrer: 'referrerrB',
|
||||||
|
} as Team;
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
pubKey: 'pubkey',
|
||||||
|
isReadOnly: false,
|
||||||
|
team: teamA,
|
||||||
|
partyTeam: teamB,
|
||||||
|
onJoin: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
props.onJoin.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables button if not connected', async () => {
|
||||||
|
render(<JoinButton {...props} pubKey={null} />);
|
||||||
|
const button = screen.getByRole('button');
|
||||||
|
expect(button).toBeDisabled();
|
||||||
|
await userEvent.hover(button);
|
||||||
|
const tooltip = await screen.findByRole('tooltip');
|
||||||
|
expect(tooltip).toHaveTextContent(/Connect your wallet/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables button if you created the current team', () => {
|
||||||
|
render(
|
||||||
|
<JoinButton
|
||||||
|
{...props}
|
||||||
|
pubKey={teamA.referrer}
|
||||||
|
team={teamA}
|
||||||
|
partyTeam={teamA}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: /Owner/ });
|
||||||
|
expect(button).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disables button if you created a team', async () => {
|
||||||
|
render(<JoinButton {...props} pubKey={teamB.referrer} />);
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: /Switch team/ });
|
||||||
|
expect(button).toBeDisabled();
|
||||||
|
await userEvent.hover(button);
|
||||||
|
const tooltip = await screen.findByRole('tooltip');
|
||||||
|
expect(tooltip).toHaveTextContent(/As a team creator/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows if party is already in team', async () => {
|
||||||
|
render(<JoinButton {...props} team={teamA} partyTeam={teamA} />);
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: /Joined/ });
|
||||||
|
expect(button).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enables switch team if party is in a different team', async () => {
|
||||||
|
render(<JoinButton {...props} />);
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: /Switch team/ });
|
||||||
|
expect(button).toBeEnabled();
|
||||||
|
await userEvent.click(button);
|
||||||
|
expect(props.onJoin).toHaveBeenCalledWith('switch');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('enables join team if party is not in a team', async () => {
|
||||||
|
render(<JoinButton {...props} partyTeam={undefined} />);
|
||||||
|
|
||||||
|
const button = screen.getByRole('button', { name: /Join team/ });
|
||||||
|
expect(button).toBeEnabled();
|
||||||
|
await userEvent.click(button);
|
||||||
|
expect(props.onJoin).toHaveBeenCalledWith('join');
|
||||||
|
});
|
||||||
|
});
|
225
apps/trading/client-pages/competitions/join-team.tsx
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import {
|
||||||
|
TradingButton as Button,
|
||||||
|
Dialog,
|
||||||
|
Intent,
|
||||||
|
Tooltip,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import {
|
||||||
|
useSimpleTransaction,
|
||||||
|
useVegaWallet,
|
||||||
|
type Status,
|
||||||
|
} from '@vegaprotocol/wallet';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { type Team } from '../../lib/hooks/use-team';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
type JoinType = 'switch' | 'join';
|
||||||
|
|
||||||
|
export const JoinTeam = ({
|
||||||
|
team,
|
||||||
|
partyTeam,
|
||||||
|
refetch,
|
||||||
|
}: {
|
||||||
|
team: Team;
|
||||||
|
partyTeam?: Team;
|
||||||
|
refetch: () => void;
|
||||||
|
}) => {
|
||||||
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
const { send, status } = useSimpleTransaction({
|
||||||
|
onSuccess: refetch,
|
||||||
|
});
|
||||||
|
const [confirmDialog, setConfirmDialog] = useState<JoinType>();
|
||||||
|
|
||||||
|
const joinTeam = () => {
|
||||||
|
send({
|
||||||
|
joinTeam: {
|
||||||
|
id: team.teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<JoinButton
|
||||||
|
team={team}
|
||||||
|
partyTeam={partyTeam}
|
||||||
|
pubKey={pubKey}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
onJoin={setConfirmDialog}
|
||||||
|
/>
|
||||||
|
<Dialog
|
||||||
|
open={confirmDialog !== undefined}
|
||||||
|
onChange={() => setConfirmDialog(undefined)}
|
||||||
|
>
|
||||||
|
{confirmDialog !== undefined && (
|
||||||
|
<DialogContent
|
||||||
|
type={confirmDialog}
|
||||||
|
status={status}
|
||||||
|
team={team}
|
||||||
|
partyTeam={partyTeam}
|
||||||
|
onConfirm={joinTeam}
|
||||||
|
onCancel={() => setConfirmDialog(undefined)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const JoinButton = ({
|
||||||
|
pubKey,
|
||||||
|
isReadOnly,
|
||||||
|
team,
|
||||||
|
partyTeam,
|
||||||
|
onJoin,
|
||||||
|
}: {
|
||||||
|
pubKey: string | null;
|
||||||
|
isReadOnly: boolean;
|
||||||
|
team: Team;
|
||||||
|
partyTeam?: Team;
|
||||||
|
onJoin: (type: JoinType) => void;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
if (!pubKey || isReadOnly) {
|
||||||
|
return (
|
||||||
|
<Tooltip description={t('Connect your wallet to join the team')}>
|
||||||
|
<Button intent={Intent.Primary} disabled={true}>
|
||||||
|
{t('Join team')}{' '}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Party is the creator of a team
|
||||||
|
else if (partyTeam && partyTeam.referrer === pubKey) {
|
||||||
|
// Party is the creator of THIS team
|
||||||
|
if (partyTeam.teamId === team.teamId) {
|
||||||
|
return (
|
||||||
|
<Button intent={Intent.None} disabled={true}>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{t('Owner')}{' '}
|
||||||
|
<span className="text-vega-green-600 dark:text-vega-green">
|
||||||
|
<VegaIcon name={VegaIconNames.TICK} />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Not creator of the team, but still can't switch because
|
||||||
|
// creators cannot leave their own team
|
||||||
|
return (
|
||||||
|
<Tooltip description="As a team creator, you cannot switch teams">
|
||||||
|
<Button intent={Intent.Primary} disabled={true}>
|
||||||
|
{t('Switch team')}{' '}
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Party is in a team, but not this one
|
||||||
|
else if (partyTeam && partyTeam.teamId !== team.teamId) {
|
||||||
|
return (
|
||||||
|
<Button onClick={() => onJoin('switch')} intent={Intent.Primary}>
|
||||||
|
{t('Switch team')}{' '}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Joined. Current party is already in this team
|
||||||
|
else if (partyTeam && partyTeam.teamId === team.teamId) {
|
||||||
|
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 onClick={() => onJoin('join')} intent={Intent.Primary}>
|
||||||
|
{t('Join team')}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DialogContent = ({
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
team,
|
||||||
|
partyTeam,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
}: {
|
||||||
|
type: JoinType;
|
||||||
|
status: Status;
|
||||||
|
team: Team;
|
||||||
|
partyTeam?: Team;
|
||||||
|
onConfirm: () => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
if (status === 'requested') {
|
||||||
|
return <p>{t('Confirm in wallet...')}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'pending') {
|
||||||
|
return <p>{t('Confirming transaction...')}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'confirmed') {
|
||||||
|
if (type === 'switch') {
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
'Team switch successful. You will switch team at the end of the epoch.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <p>{t('Team joined')}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{type === 'switch' && (
|
||||||
|
<>
|
||||||
|
<h2 className="font-alpha text-xl">{t('Switch team')}</h2>
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
"Switching team will move you from '{{fromTeam}}' to '{{toTeam}}' at the end of the epoch. Are you sure?",
|
||||||
|
{
|
||||||
|
fromTeam: partyTeam?.name,
|
||||||
|
toTeam: team.name,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{type === 'join' && (
|
||||||
|
<>
|
||||||
|
<h2 className="font-alpha text-xl">{t('Join team')}</h2>
|
||||||
|
<p>
|
||||||
|
{t('Are you sure you want to join team: {{team}}', {
|
||||||
|
team: team.name,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between gap-2">
|
||||||
|
<Button onClick={onConfirm} intent={Intent.Success}>
|
||||||
|
{t('Confirm')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onCancel} intent={Intent.Danger}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
254
apps/trading/client-pages/competitions/team-form.tsx
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
import {
|
||||||
|
TradingFormGroup,
|
||||||
|
TradingInput,
|
||||||
|
TradingInputError,
|
||||||
|
TradingCheckbox,
|
||||||
|
TextArea,
|
||||||
|
TradingButton,
|
||||||
|
Intent,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
|
||||||
|
|
||||||
|
import { type useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import type {
|
||||||
|
CreateReferralSet,
|
||||||
|
UpdateReferralSet,
|
||||||
|
Status,
|
||||||
|
} from '@vegaprotocol/wallet';
|
||||||
|
|
||||||
|
export type FormFields = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
private: boolean;
|
||||||
|
allowList: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum TransactionType {
|
||||||
|
CreateReferralSet,
|
||||||
|
UpdateReferralSet,
|
||||||
|
}
|
||||||
|
|
||||||
|
const prepareTransaction = (
|
||||||
|
type: TransactionType,
|
||||||
|
fields: FormFields
|
||||||
|
): CreateReferralSet | UpdateReferralSet => {
|
||||||
|
switch (type) {
|
||||||
|
case TransactionType.CreateReferralSet:
|
||||||
|
return {
|
||||||
|
createReferralSet: {
|
||||||
|
isTeam: true,
|
||||||
|
team: {
|
||||||
|
name: fields.name,
|
||||||
|
teamUrl: fields.url,
|
||||||
|
avatarUrl: fields.avatarUrl,
|
||||||
|
closed: fields.private,
|
||||||
|
allowList: fields.private
|
||||||
|
? parseAllowListText(fields.allowList)
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case TransactionType.UpdateReferralSet:
|
||||||
|
return {
|
||||||
|
updateReferralSet: {
|
||||||
|
id: fields.id,
|
||||||
|
isTeam: true,
|
||||||
|
team: {
|
||||||
|
name: fields.name,
|
||||||
|
teamUrl: fields.url,
|
||||||
|
avatarUrl: fields.avatarUrl,
|
||||||
|
closed: fields.private,
|
||||||
|
allowList: fields.private
|
||||||
|
? parseAllowListText(fields.allowList)
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TeamForm = ({
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
err,
|
||||||
|
isSolo,
|
||||||
|
onSubmit,
|
||||||
|
defaultValues,
|
||||||
|
}: {
|
||||||
|
type: TransactionType;
|
||||||
|
status: ReturnType<typeof useReferralSetTransaction>['status'];
|
||||||
|
err: ReturnType<typeof useReferralSetTransaction>['err'];
|
||||||
|
isSolo: boolean;
|
||||||
|
onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit'];
|
||||||
|
defaultValues?: FormFields;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormFields>({
|
||||||
|
defaultValues: {
|
||||||
|
private: isSolo,
|
||||||
|
...defaultValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPrivate = watch('private');
|
||||||
|
|
||||||
|
const sendTransaction = (fields: FormFields) => {
|
||||||
|
onSubmit(prepareTransaction(type, fields));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(sendTransaction)}>
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
{...register('id', {
|
||||||
|
disabled: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<TradingFormGroup label={t('Team name')} labelFor="name">
|
||||||
|
<TradingInput {...register('name', { required: t('Required') })} />
|
||||||
|
{errors.name?.message && (
|
||||||
|
<TradingInputError forInput="name">
|
||||||
|
{errors.name.message}
|
||||||
|
</TradingInputError>
|
||||||
|
)}
|
||||||
|
</TradingFormGroup>
|
||||||
|
<TradingFormGroup
|
||||||
|
label={t('URL')}
|
||||||
|
labelFor="url"
|
||||||
|
labelDescription={t(
|
||||||
|
'Provide a link so users can learn more about your team'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TradingInput
|
||||||
|
{...register('url', {
|
||||||
|
pattern: { value: URL_REGEX, message: t('Invalid URL') },
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{errors.url?.message && (
|
||||||
|
<TradingInputError forInput="url">
|
||||||
|
{errors.url.message}
|
||||||
|
</TradingInputError>
|
||||||
|
)}
|
||||||
|
</TradingFormGroup>
|
||||||
|
<TradingFormGroup
|
||||||
|
label={t('Avatar URL')}
|
||||||
|
labelFor="avatarUrl"
|
||||||
|
labelDescription={t('Provide a URL to a hosted image')}
|
||||||
|
>
|
||||||
|
<TradingInput
|
||||||
|
{...register('avatarUrl', {
|
||||||
|
pattern: {
|
||||||
|
value: URL_REGEX,
|
||||||
|
message: t('Invalid image URL'),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{errors.avatarUrl?.message && (
|
||||||
|
<TradingInputError forInput="avatarUrl">
|
||||||
|
{errors.avatarUrl.message}
|
||||||
|
</TradingInputError>
|
||||||
|
)}
|
||||||
|
</TradingFormGroup>
|
||||||
|
<TradingFormGroup
|
||||||
|
label={t('Make team private')}
|
||||||
|
labelFor="private"
|
||||||
|
hideLabel={true}
|
||||||
|
>
|
||||||
|
<Controller
|
||||||
|
name="private"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<TradingCheckbox
|
||||||
|
label={t('Make team private')}
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
}}
|
||||||
|
disabled={isSolo}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TradingFormGroup>
|
||||||
|
{isPrivate && (
|
||||||
|
<TradingFormGroup
|
||||||
|
label={t('Public key allow list')}
|
||||||
|
labelFor="allowList"
|
||||||
|
labelDescription={t(
|
||||||
|
'Use a comma separated list to allow only specific public keys to join the team'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
{...register('allowList', {
|
||||||
|
required: t('Required'),
|
||||||
|
disabled: isSolo,
|
||||||
|
validate: {
|
||||||
|
allowList: (value) => {
|
||||||
|
const publicKeys = parseAllowListText(value);
|
||||||
|
if (publicKeys.every((pk) => isValidVegaPublicKey(pk))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return t('Invalid public key found in allow list');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
{errors.allowList?.message && (
|
||||||
|
<TradingInputError forInput="avatarUrl">
|
||||||
|
{errors.allowList.message}
|
||||||
|
</TradingInputError>
|
||||||
|
)}
|
||||||
|
</TradingFormGroup>
|
||||||
|
)}
|
||||||
|
{err && <p className="text-danger text-xs mb-4 capitalize">{err}</p>}
|
||||||
|
<SubmitButton type={type} status={status} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SubmitButton = ({
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
type?: TransactionType;
|
||||||
|
status: Status;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const disabled = status === 'pending' || status === 'requested';
|
||||||
|
|
||||||
|
let text = t('Create');
|
||||||
|
if (type === TransactionType.UpdateReferralSet) {
|
||||||
|
text = t('Update');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'requested') {
|
||||||
|
text = t('Confirm in wallet...');
|
||||||
|
} else if (status === 'pending') {
|
||||||
|
text = t('Confirming transaction...');
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TradingButton type="submit" intent={Intent.Info} disabled={disabled}>
|
||||||
|
{text}
|
||||||
|
</TradingButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseAllowListText = (str: string) => {
|
||||||
|
return str
|
||||||
|
.split(',')
|
||||||
|
.map((v) => v.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import { type Team } from '../../lib/hooks/use-team';
|
||||||
|
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { Links } from '../../lib/links';
|
||||||
|
|
||||||
|
export const UpdateTeamButton = ({ team }: { team: Team }) => {
|
||||||
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
|
||||||
|
if (pubKey && !isReadOnly && pubKey === team.referrer) {
|
||||||
|
return (
|
||||||
|
<TradingAnchorButton
|
||||||
|
data-testid="update-team-button"
|
||||||
|
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
|
||||||
|
intent={Intent.Info}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
@ -5,17 +5,19 @@ import {
|
|||||||
VegaIcon,
|
VegaIcon,
|
||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import type { FieldValues } from 'react-hook-form';
|
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
|
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { RainbowButton } from './buttons';
|
import { RainbowButton } from '../../components/rainbow-button';
|
||||||
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
import {
|
||||||
|
useSimpleTransaction,
|
||||||
|
useVegaWallet,
|
||||||
|
useVegaWalletDialogStore,
|
||||||
|
} from '@vegaprotocol/wallet';
|
||||||
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
|
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
|
||||||
import { Routes } from '../../lib/links';
|
import { Routes } from '../../lib/links';
|
||||||
import { useTransactionEventSubscription } from '@vegaprotocol/web3';
|
|
||||||
import { Statistics, useStats } from './referral-statistics';
|
import { Statistics, useStats } from './referral-statistics';
|
||||||
import { useReferralProgram } from './hooks/use-referral-program';
|
import { useReferralProgram } from './hooks/use-referral-program';
|
||||||
import { ns, useT } from '../../lib/use-t';
|
import { ns, useT } from '../../lib/use-t';
|
||||||
@ -73,6 +75,10 @@ export const ApplyCodeFormContainer = ({
|
|||||||
return <ApplyCodeForm onSuccess={onSuccess} />;
|
return <ApplyCodeForm onSuccess={onSuccess} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type FormFields = {
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const program = useReferralProgram();
|
const program = useReferralProgram();
|
||||||
@ -81,31 +87,47 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
(store) => store.openVegaWalletDialog
|
(store) => store.openVegaWalletDialog
|
||||||
);
|
);
|
||||||
|
|
||||||
const [status, setStatus] = useState<
|
const { isReadOnly, pubKey } = useVegaWallet();
|
||||||
'requested' | 'no-funds' | 'successful' | null
|
|
||||||
>(null);
|
|
||||||
const txHash = useRef<string | null>(null);
|
|
||||||
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
|
|
||||||
const { isEligible, requiredFunds } = useFundsAvailable();
|
const { isEligible, requiredFunds } = useFundsAvailable();
|
||||||
|
|
||||||
const currentRouteId = useGetCurrentRouteId();
|
const currentRouteId = useGetCurrentRouteId();
|
||||||
const setViews = useSidebar((s) => s.setViews);
|
const setViews = useSidebar((s) => s.setViews);
|
||||||
|
|
||||||
|
const [params] = useSearchParams();
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
setValue,
|
|
||||||
setError,
|
setError,
|
||||||
watch,
|
watch,
|
||||||
} = useForm();
|
} = useForm<FormFields>({
|
||||||
const [params] = useSearchParams();
|
defaultValues: {
|
||||||
|
code: params.get('code') || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const codeField = watch('code');
|
const codeField = watch('code');
|
||||||
|
|
||||||
const { data: previewData, loading: previewLoading } = useReferral({
|
const { data: previewData, loading: previewLoading } = useReferral({
|
||||||
code: validateCode(codeField, t) ? codeField : undefined,
|
code: validateCode(codeField, t) ? codeField : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { send, status } = useSimpleTransaction({
|
||||||
|
onSuccess: () => {
|
||||||
|
// go to main page when successfully applied
|
||||||
|
setTimeout(() => {
|
||||||
|
if (onSuccess) onSuccess();
|
||||||
|
navigate(Routes.REFERRALS);
|
||||||
|
}, RELOAD_DELAY);
|
||||||
|
},
|
||||||
|
onError: (msg) => {
|
||||||
|
setError('code', {
|
||||||
|
type: 'required',
|
||||||
|
message: msg,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates if a connected party can apply a code (min funds span protection)
|
* Validates if a connected party can apply a code (min funds span protection)
|
||||||
*/
|
*/
|
||||||
@ -135,99 +157,55 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
return true;
|
return true;
|
||||||
}, [codeField, previewData, previewLoading, t]);
|
}, [codeField, previewData, previewLoading, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
const noFunds = validateFundsAvailable() !== true ? true : false;
|
||||||
const code = params.get('code');
|
|
||||||
if (code) setValue('code', code);
|
|
||||||
}, [params, setValue]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const onSubmit = ({ code }: FormFields) => {
|
||||||
const err = validateFundsAvailable();
|
|
||||||
if (err !== true) {
|
|
||||||
setStatus('no-funds');
|
|
||||||
} else {
|
|
||||||
setStatus(null);
|
|
||||||
}
|
|
||||||
}, [isEligible, validateFundsAvailable]);
|
|
||||||
|
|
||||||
const onSubmit = ({ code }: FieldValues) => {
|
|
||||||
if (isReadOnly || !pubKey || !code || code.length === 0) {
|
if (isReadOnly || !pubKey || !code || code.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus('requested');
|
send({
|
||||||
|
|
||||||
sendTx(pubKey, {
|
|
||||||
applyReferralCode: {
|
applyReferralCode: {
|
||||||
id: code as string,
|
id: code as string,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
.then((res) => {
|
|
||||||
if (!res) {
|
|
||||||
setError('code', {
|
|
||||||
type: 'required',
|
|
||||||
message: t('The transaction could not be sent'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (res) {
|
|
||||||
txHash.current = res.transactionHash.toLowerCase();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err.message.includes('user rejected')) {
|
|
||||||
setStatus(null);
|
|
||||||
} else {
|
|
||||||
setStatus(null);
|
|
||||||
setError('code', {
|
|
||||||
type: 'required',
|
|
||||||
message:
|
|
||||||
err instanceof Error
|
|
||||||
? err.message
|
|
||||||
: t('Your code has been rejected'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useTransactionEventSubscription({
|
// sendTx(pubKey, {
|
||||||
variables: { partyId: pubKey || '' },
|
// applyReferralCode: {
|
||||||
skip: !pubKey,
|
// id: code as string,
|
||||||
fetchPolicy: 'no-cache',
|
// },
|
||||||
onData: ({ data: result }) =>
|
// })
|
||||||
result.data?.busEvents?.forEach((event) => {
|
// .then((res) => {
|
||||||
if (event.event.__typename === 'TransactionResult') {
|
// if (!res) {
|
||||||
const hash = event.event.hash.toLowerCase();
|
// setError('code', {
|
||||||
if (txHash.current && txHash.current === hash) {
|
// type: 'required',
|
||||||
const err = event.event.error;
|
// message: t('The transaction could not be sent'),
|
||||||
const status = event.event.status;
|
// });
|
||||||
if (err) {
|
// }
|
||||||
setStatus(null);
|
// if (res) {
|
||||||
setError('code', {
|
// txHash.current = res.transactionHash.toLowerCase();
|
||||||
type: 'required',
|
// }
|
||||||
message: err,
|
// })
|
||||||
});
|
// .catch((err) => {
|
||||||
}
|
// if (err.message.includes('user rejected')) {
|
||||||
if (status && !err) {
|
// setStatus(null);
|
||||||
setStatus('successful');
|
// } else {
|
||||||
}
|
// setStatus(null);
|
||||||
}
|
// setError('code', {
|
||||||
}
|
// type: 'required',
|
||||||
}),
|
// message:
|
||||||
});
|
// err instanceof Error
|
||||||
|
// ? err.message
|
||||||
|
// : t('Your code has been rejected'),
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
const { epochsValue, nextBenefitTierValue } = useStats({ program });
|
const { epochsValue, nextBenefitTierValue } = useStats({ program });
|
||||||
|
|
||||||
// go to main page when successfully applied
|
|
||||||
useEffect(() => {
|
|
||||||
if (status === 'successful') {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (onSuccess) onSuccess();
|
|
||||||
navigate(Routes.REFERRALS);
|
|
||||||
}, RELOAD_DELAY);
|
|
||||||
}
|
|
||||||
}, [navigate, onSuccess, status]);
|
|
||||||
|
|
||||||
// show "code applied" message when successfully applied
|
// show "code applied" message when successfully applied
|
||||||
if (status === 'successful') {
|
if (status === 'confirmed') {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-1/2">
|
<div className="mx-auto w-1/2">
|
||||||
<h3 className="calt mb-5 flex flex-row items-center justify-center gap-2 text-center text-xl uppercase">
|
<h3 className="calt mb-5 flex flex-row items-center justify-center gap-2 text-center text-xl uppercase">
|
||||||
@ -261,7 +239,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'no-funds') {
|
if (noFunds) {
|
||||||
return {
|
return {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
children: t('Deposit funds'),
|
children: t('Deposit funds'),
|
||||||
@ -332,7 +310,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
|
|||||||
</label>
|
</label>
|
||||||
<RainbowButton variant="border" {...getButtonProps()} />
|
<RainbowButton variant="border" {...getButtonProps()} />
|
||||||
</form>
|
</form>
|
||||||
{status === 'no-funds' ? (
|
{noFunds ? (
|
||||||
<InputError intent="warning" className="overflow-auto break-words">
|
<InputError intent="warning" className="overflow-auto break-words">
|
||||||
<span>
|
<span>
|
||||||
<SpamProtectionErr requiredFunds={requiredFunds?.toString()} />
|
<SpamProtectionErr requiredFunds={requiredFunds?.toString()} />
|
||||||
|
@ -4,41 +4,6 @@ import type { ComponentProps, ButtonHTMLAttributes } from 'react';
|
|||||||
import { forwardRef } from 'react';
|
import { forwardRef } from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
type RainbowButtonProps = {
|
|
||||||
variant?: 'full' | 'border';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const RainbowButton = ({
|
|
||||||
variant = 'full',
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
...props
|
|
||||||
}: RainbowButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) => (
|
|
||||||
<button
|
|
||||||
className={classNames(
|
|
||||||
'bg-rainbow rounded-lg overflow-hidden disabled:opacity-40',
|
|
||||||
'hover:bg-rainbow-180 hover:animate-spin-rainbow',
|
|
||||||
{
|
|
||||||
'px-5 py-3 text-white': variant === 'full',
|
|
||||||
'p-[0.125rem]': variant === 'border',
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
{
|
|
||||||
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white px-5 py-3 rounded-[0.35rem] overflow-hidden':
|
|
||||||
variant === 'border',
|
|
||||||
},
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
|
|
||||||
const RAINBOW_TAB_STYLE = classNames(
|
const RAINBOW_TAB_STYLE = classNames(
|
||||||
'inline-block',
|
'inline-block',
|
||||||
'bg-vega-clight-500 dark:bg-vega-cdark-500',
|
'bg-vega-clight-500 dark:bg-vega-cdark-500',
|
||||||
|
@ -2,9 +2,6 @@ export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
|||||||
export const GRADIENT =
|
export const GRADIENT =
|
||||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||||
|
|
||||||
export const SKY_BACKGROUND =
|
|
||||||
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[37%_0px] bg-[length:1440px] bg-no-repeat bg-local';
|
|
||||||
|
|
||||||
// TODO: Update the links to use the correct referral related pages
|
// TODO: Update the links to use the correct referral related pages
|
||||||
export const REFERRAL_DOCS_LINK =
|
export const REFERRAL_DOCS_LINK =
|
||||||
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
|
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
|
||||||
|
@ -1,9 +1,5 @@
|
|||||||
import {
|
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||||
useVegaWallet,
|
import { RainbowButton } from '../../components/rainbow-button';
|
||||||
useVegaWalletDialogStore,
|
|
||||||
determineId,
|
|
||||||
} from '@vegaprotocol/wallet';
|
|
||||||
import { RainbowButton } from './buttons';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
CopyWithTooltip,
|
CopyWithTooltip,
|
||||||
@ -11,6 +7,7 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
InputError,
|
InputError,
|
||||||
Intent,
|
Intent,
|
||||||
|
Tooltip,
|
||||||
TradingAnchorButton,
|
TradingAnchorButton,
|
||||||
TradingButton,
|
TradingButton,
|
||||||
VegaIcon,
|
VegaIcon,
|
||||||
@ -18,34 +15,28 @@ import {
|
|||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
|
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
|
||||||
import { useStakeAvailable } from './hooks/use-stake-available';
|
|
||||||
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
|
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
|
||||||
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
|
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Link, Navigate, useNavigate } from 'react-router-dom';
|
||||||
import { Routes } from '../../lib/links';
|
import { Links, Routes } from '../../lib/links';
|
||||||
import { useReferralProgram } from './hooks/use-referral-program';
|
import { useReferralProgram } from './hooks/use-referral-program';
|
||||||
|
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
|
|
||||||
export const CreateCodeContainer = () => {
|
export const CreateCodeContainer = () => {
|
||||||
const { pubKey } = useVegaWallet();
|
const t = useT();
|
||||||
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
const isInReferralSet = useIsInReferralSet(pubKey);
|
const isInReferralSet = useIsInReferralSet(pubKey);
|
||||||
|
const openWalletDialog = useVegaWalletDialogStore(
|
||||||
|
(store) => store.openVegaWalletDialog
|
||||||
|
);
|
||||||
|
|
||||||
// Navigate to the index page when already in the referral set.
|
// Navigate to the index page when already in the referral set.
|
||||||
if (isInReferralSet) {
|
if (isInReferralSet) {
|
||||||
return <Navigate to={Routes.REFERRALS} />;
|
return <Navigate to={Routes.REFERRALS} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <CreateCodeForm />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CreateCodeForm = () => {
|
|
||||||
const t = useT();
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
|
||||||
const openWalletDialog = useVegaWalletDialogStore(
|
|
||||||
(store) => store.openVegaWalletDialog
|
|
||||||
);
|
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="referral-create-code-form"
|
data-testid="referral-create-code-form"
|
||||||
@ -60,22 +51,82 @@ export const CreateCodeForm = () => {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="w-full flex flex-col">
|
<div className="w-full flex flex-col gap-4 items-stretch">
|
||||||
<RainbowButton
|
{pubKey ? (
|
||||||
variant="border"
|
<CreateCodeForm />
|
||||||
disabled={isReadOnly}
|
) : (
|
||||||
onClick={() => {
|
<RainbowButton
|
||||||
if (pubKey) {
|
variant="border"
|
||||||
setDialogOpen(true);
|
disabled={isReadOnly}
|
||||||
} else {
|
onClick={openWalletDialog}
|
||||||
openWalletDialog();
|
>
|
||||||
}
|
{t('Connect wallet')}
|
||||||
}}
|
</RainbowButton>
|
||||||
>
|
)}
|
||||||
{pubKey ? t('Create a referral code') : t('Connect wallet')}
|
|
||||||
</RainbowButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateCodeForm = () => {
|
||||||
|
const t = useT();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const { isReadOnly } = useVegaWallet();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip
|
||||||
|
description={t(
|
||||||
|
'Create a simple referral code to enjoy the referrer commission outlined in the current referral program'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<RainbowButton
|
||||||
|
variant="border"
|
||||||
|
disabled={isReadOnly}
|
||||||
|
onClick={() => setDialogOpen(true)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{t('Create a referral code')}
|
||||||
|
</RainbowButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
description={
|
||||||
|
<Trans
|
||||||
|
i18nKey={
|
||||||
|
'Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards'
|
||||||
|
}
|
||||||
|
components={[
|
||||||
|
<Link
|
||||||
|
key="homepage-link"
|
||||||
|
to={Links.COMPETITIONS()}
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
|
Compeitionts Homepage
|
||||||
|
</Link>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<RainbowButton
|
||||||
|
role="link"
|
||||||
|
variant="border"
|
||||||
|
disabled={isReadOnly}
|
||||||
|
onClick={() => navigate(Links.COMPETITIONS_CREATE_TEAM())}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{t('Create a team')}
|
||||||
|
</RainbowButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<p className="text-xs">
|
||||||
|
<Link className="underline" to={Links.COMPETITIONS()}>
|
||||||
|
{t('Go to competitions')}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
<Dialog
|
<Dialog
|
||||||
title={t('Create a referral code')}
|
title={t('Create a referral code')}
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
@ -84,7 +135,7 @@ export const CreateCodeForm = () => {
|
|||||||
>
|
>
|
||||||
<CreateCodeDialog setDialogOpen={setDialogOpen} />
|
<CreateCodeDialog setDialogOpen={setDialogOpen} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -95,67 +146,42 @@ const CreateCodeDialog = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const createLink = useLinks(DApp.Governance);
|
const createLink = useLinks(DApp.Governance);
|
||||||
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
const { refetch } = useReferral({ pubKey, role: 'referrer' });
|
const { refetch } = useReferral({ pubKey, role: 'referrer' });
|
||||||
const [err, setErr] = useState<string | null>(null);
|
const {
|
||||||
const [code, setCode] = useState<string | null>(null);
|
err,
|
||||||
const [status, setStatus] = useState<
|
code,
|
||||||
'idle' | 'loading' | 'success' | 'error'
|
status,
|
||||||
>('idle');
|
stakeAvailable: currentStakeAvailable,
|
||||||
|
requiredStake,
|
||||||
const { stakeAvailable: currentStakeAvailable, requiredStake } =
|
onSubmit,
|
||||||
useStakeAvailable();
|
} = useReferralSetTransaction();
|
||||||
|
|
||||||
const { details: programDetails } = useReferralProgram();
|
const { details: programDetails } = useReferralProgram();
|
||||||
|
|
||||||
const onSubmit = () => {
|
|
||||||
if (isReadOnly || !pubKey) {
|
|
||||||
setErr('Not connected');
|
|
||||||
} else {
|
|
||||||
setErr(null);
|
|
||||||
setStatus('loading');
|
|
||||||
setCode(null);
|
|
||||||
sendTx(pubKey, {
|
|
||||||
createReferralSet: {
|
|
||||||
isTeam: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((res) => {
|
|
||||||
if (!res) {
|
|
||||||
setErr(`Invalid response: ${JSON.stringify(res)}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const code = determineId(res.signature);
|
|
||||||
setCode(code);
|
|
||||||
setStatus('success');
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (err.message.includes('user rejected')) {
|
|
||||||
setStatus('idle');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setStatus('error');
|
|
||||||
setErr(err.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getButtonProps = () => {
|
const getButtonProps = () => {
|
||||||
if (status === 'idle' || status === 'error') {
|
if (status === 'idle') {
|
||||||
return {
|
return {
|
||||||
children: t('Generate code'),
|
children: t('Generate code'),
|
||||||
onClick: () => onSubmit(),
|
onClick: () => onSubmit({ createReferralSet: { isTeam: false } }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'loading') {
|
if (status === 'requested') {
|
||||||
return {
|
return {
|
||||||
children: t('Confirm in wallet...'),
|
children: t('Confirm in wallet...'),
|
||||||
disabled: true,
|
disabled: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 'success') {
|
if (status === 'pending') {
|
||||||
|
return {
|
||||||
|
children: t('Waiting for transaction...'),
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'confirmed') {
|
||||||
return {
|
return {
|
||||||
children: t('Close'),
|
children: t('Close'),
|
||||||
intent: Intent.Success,
|
intent: Intent.Success,
|
||||||
@ -209,7 +235,10 @@ const CreateCodeDialog = ({
|
|||||||
if (!programDetails) {
|
if (!programDetails) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{(status === 'idle' || status === 'loading' || status === 'error') && (
|
{(status === 'idle' ||
|
||||||
|
status === 'requested' ||
|
||||||
|
status === 'pending' ||
|
||||||
|
err) && (
|
||||||
<>
|
<>
|
||||||
{
|
{
|
||||||
<p>
|
<p>
|
||||||
@ -220,7 +249,7 @@ const CreateCodeDialog = ({
|
|||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{status === 'success' && code && (
|
{status === 'confirmed' && code && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
|
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
|
||||||
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
|
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
@ -240,7 +269,7 @@ const CreateCodeDialog = ({
|
|||||||
<TradingButton
|
<TradingButton
|
||||||
fill={true}
|
fill={true}
|
||||||
intent={Intent.Primary}
|
intent={Intent.Primary}
|
||||||
onClick={() => onSubmit()}
|
onClick={() => onSubmit({ createReferralSet: { isTeam: false } })}
|
||||||
{...getButtonProps()}
|
{...getButtonProps()}
|
||||||
>
|
>
|
||||||
{t('Yes')}
|
{t('Yes')}
|
||||||
@ -269,14 +298,17 @@ const CreateCodeDialog = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{(status === 'idle' || status === 'loading' || status === 'error') && (
|
{(status === 'idle' ||
|
||||||
|
status === 'requested' ||
|
||||||
|
status === 'pending' ||
|
||||||
|
err) && (
|
||||||
<p>
|
<p>
|
||||||
{t(
|
{t(
|
||||||
'Generate a referral code to share with your friends and access the commission benefits of the current program.'
|
'Generate a referral code to share with your friends and access the commission benefits of the current program.'
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{status === 'success' && code && (
|
{status === 'confirmed' && code && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
|
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
|
||||||
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
|
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
|
||||||
|
@ -1,53 +1,10 @@
|
|||||||
import { isRouteErrorResponse, useNavigate, useRouteError } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { RainbowButton } from './buttons';
|
import { RainbowButton } from '../../components/rainbow-button';
|
||||||
|
import { LayoutWithSky } from '../../components/layouts-inner';
|
||||||
import { AnimatedDudeWithWire } from './graphics/dude';
|
import { AnimatedDudeWithWire } from './graphics/dude';
|
||||||
import { LayoutWithSky } from './layout';
|
|
||||||
import { Routes } from '../../lib/links';
|
import { Routes } from '../../lib/links';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
|
|
||||||
export const ErrorBoundary = () => {
|
|
||||||
const t = useT();
|
|
||||||
const error = useRouteError();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const title = isRouteErrorResponse(error)
|
|
||||||
? `${error.status} ${error.statusText}`
|
|
||||||
: t('Something went wrong');
|
|
||||||
|
|
||||||
const code = isRouteErrorResponse(error) ? error.status : 0;
|
|
||||||
|
|
||||||
const messages: Record<number, string> = {
|
|
||||||
0: t('An unknown error occurred.'),
|
|
||||||
404: t("The page you're looking for doesn't exists."),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LayoutWithSky className="pt-32">
|
|
||||||
<div
|
|
||||||
aria-hidden
|
|
||||||
className="absolute top-64 right-[220px] md:right-[340px] max-sm:hidden"
|
|
||||||
>
|
|
||||||
<AnimatedDudeWithWire className="animate-spin" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-6xl font-alpha calt mb-10">{title}</h1>
|
|
||||||
|
|
||||||
{Object.keys(messages).includes(code.toString()) ? (
|
|
||||||
<p className="text-lg mb-10">{messages[code]}</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<p className="text-lg mb-10">
|
|
||||||
<RainbowButton
|
|
||||||
onClick={() => navigate('..')}
|
|
||||||
variant="border"
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{t('Go back and try again')}
|
|
||||||
</RainbowButton>
|
|
||||||
</p>
|
|
||||||
</LayoutWithSky>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NotFound = () => {
|
export const NotFound = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -8,13 +8,13 @@ import type {
|
|||||||
ReferralSetsQueryVariables,
|
ReferralSetsQueryVariables,
|
||||||
} from './__generated__/ReferralSets';
|
} from './__generated__/ReferralSets';
|
||||||
import { useReferralSetsQuery } from './__generated__/ReferralSets';
|
import { useReferralSetsQuery } from './__generated__/ReferralSets';
|
||||||
import { useStakeAvailable } from './use-stake-available';
|
import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
|
||||||
|
|
||||||
export const DEFAULT_AGGREGATION_DAYS = 30;
|
export const DEFAULT_AGGREGATION_DAYS = 30;
|
||||||
|
|
||||||
export type Role = 'referrer' | 'referee';
|
export type Role = 'referrer' | 'referee';
|
||||||
type UseReferralArgs = (
|
type UseReferralArgs = (
|
||||||
| { code: string }
|
| { code: string | undefined }
|
||||||
| { pubKey: string | null; role: Role }
|
| { pubKey: string | null; role: Role }
|
||||||
) & {
|
) & {
|
||||||
aggregationEpochs?: number;
|
aggregationEpochs?: number;
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
|
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
|
||||||
import { render, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
import { type VegaWalletContextShape } from '@vegaprotocol/wallet';
|
import {
|
||||||
|
VegaWalletContext,
|
||||||
|
type VegaWalletContextShape,
|
||||||
|
} from '@vegaprotocol/wallet';
|
||||||
import { ReferralStatistics } from './referral-statistics';
|
import { ReferralStatistics } from './referral-statistics';
|
||||||
import {
|
import {
|
||||||
ReferralProgramDocument,
|
ReferralProgramDocument,
|
||||||
@ -15,7 +18,7 @@ import {
|
|||||||
StakeAvailableDocument,
|
StakeAvailableDocument,
|
||||||
type StakeAvailableQueryVariables,
|
type StakeAvailableQueryVariables,
|
||||||
type StakeAvailableQuery,
|
type StakeAvailableQuery,
|
||||||
} from './hooks/__generated__/StakeAvailable';
|
} from '../../lib/hooks/__generated__/StakeAvailable';
|
||||||
import {
|
import {
|
||||||
RefereesDocument,
|
RefereesDocument,
|
||||||
type RefereesQueryVariables,
|
type RefereesQueryVariables,
|
||||||
@ -296,122 +299,99 @@ const refereesMock30: MockedResponse<RefereesQuery, RefereesQueryVariables> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.mock('@vegaprotocol/wallet', () => {
|
|
||||||
return {
|
|
||||||
...jest.requireActual('@vegaprotocol/wallet'),
|
|
||||||
useVegaWallet: () => {
|
|
||||||
const ctx: Partial<VegaWalletContextShape> = {
|
|
||||||
pubKey: MOCK_PUBKEY,
|
|
||||||
};
|
|
||||||
return ctx;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ReferralStatistics', () => {
|
describe('ReferralStatistics', () => {
|
||||||
it('displays apply code when no data has been found for given pubkey', () => {
|
const renderComponent = (mocks: MockedResponse[]) => {
|
||||||
const { queryByTestId } = render(
|
const walletContext = {
|
||||||
|
pubKey: MOCK_PUBKEY,
|
||||||
|
isReadOnly: false,
|
||||||
|
sendTx: jest.fn(),
|
||||||
|
} as unknown as VegaWalletContextShape;
|
||||||
|
|
||||||
|
return render(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<MockedProvider mocks={[]} showWarnings={false}>
|
<VegaWalletContext.Provider value={walletContext}>
|
||||||
<ReferralStatistics />
|
<MockedProvider mocks={mocks} showWarnings={false}>
|
||||||
</MockedProvider>
|
<ReferralStatistics />
|
||||||
|
</MockedProvider>
|
||||||
|
</VegaWalletContext.Provider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
|
it('displays apply code when no data has been found for given pubkey', () => {
|
||||||
|
renderComponent([]);
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId('referral-apply-code-form')
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays referrer stats when given pubkey is a referrer', async () => {
|
it('displays referrer stats when given pubkey is a referrer', async () => {
|
||||||
const { queryByTestId } = render(
|
renderComponent([
|
||||||
<MemoryRouter>
|
programMock,
|
||||||
<MockedProvider
|
referralSetAsReferrerMock,
|
||||||
mocks={[
|
noReferralSetAsRefereeMock,
|
||||||
programMock,
|
stakeAvailableMock,
|
||||||
referralSetAsReferrerMock,
|
refereesMock,
|
||||||
noReferralSetAsRefereeMock,
|
refereesMock30,
|
||||||
stakeAvailableMock,
|
]);
|
||||||
refereesMock,
|
|
||||||
refereesMock30,
|
|
||||||
]}
|
|
||||||
showWarnings={false}
|
|
||||||
>
|
|
||||||
<ReferralStatistics />
|
|
||||||
</MockedProvider>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(
|
||||||
queryByTestId('referral-create-code-form')
|
screen.queryByTestId('referral-create-code-form')
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
|
expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
|
||||||
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
|
expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
|
||||||
'referrer'
|
'referrer'
|
||||||
);
|
);
|
||||||
// gets commision from 30 epochs query
|
// gets commision from 30 epochs query
|
||||||
expect(queryByTestId('total-commission-value')).toHaveTextContent(
|
expect(screen.queryByTestId('total-commission-value')).toHaveTextContent(
|
||||||
'12,340'
|
'12,340'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays referee stats when given pubkey is a referee', async () => {
|
it('displays referee stats when given pubkey is a referee', async () => {
|
||||||
const { queryByTestId } = render(
|
renderComponent([
|
||||||
<MemoryRouter>
|
programMock,
|
||||||
<MockedProvider
|
noReferralSetAsReferrerMock,
|
||||||
mocks={[
|
referralSetAsRefereeMock,
|
||||||
programMock,
|
stakeAvailableMock,
|
||||||
noReferralSetAsReferrerMock,
|
refereesMock,
|
||||||
referralSetAsRefereeMock,
|
]);
|
||||||
stakeAvailableMock,
|
|
||||||
refereesMock,
|
|
||||||
]}
|
|
||||||
showWarnings={false}
|
|
||||||
>
|
|
||||||
<ReferralStatistics />
|
|
||||||
</MockedProvider>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(
|
||||||
queryByTestId('referral-create-code-form')
|
screen.queryByTestId('referral-create-code-form')
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
|
expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
|
||||||
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
|
expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
|
||||||
'referee'
|
'referee'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => {
|
it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => {
|
||||||
const { queryByTestId } = render(
|
renderComponent([
|
||||||
<MemoryRouter>
|
programMock,
|
||||||
<MockedProvider
|
noReferralSetAsReferrerMock,
|
||||||
mocks={[
|
referralSetAsRefereeMock,
|
||||||
programMock,
|
nonEligibleStakeAvailableMock,
|
||||||
noReferralSetAsReferrerMock,
|
refereesMock,
|
||||||
referralSetAsRefereeMock,
|
]);
|
||||||
nonEligibleStakeAvailableMock,
|
|
||||||
refereesMock,
|
|
||||||
]}
|
|
||||||
showWarnings={false}
|
|
||||||
>
|
|
||||||
<ReferralStatistics />
|
|
||||||
</MockedProvider>
|
|
||||||
</MemoryRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(
|
expect(
|
||||||
queryByTestId('referral-create-code-form')
|
screen.queryByTestId('referral-create-code-form')
|
||||||
).not.toBeInTheDocument();
|
).not.toBeInTheDocument();
|
||||||
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
|
expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
|
||||||
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
|
expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
|
||||||
'referee'
|
'referee'
|
||||||
);
|
);
|
||||||
expect(queryByTestId('referral-eligibility-warning')).toBeInTheDocument();
|
expect(
|
||||||
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
|
screen.queryByTestId('referral-eligibility-warning')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(
|
||||||
|
screen.queryByTestId('referral-apply-code-form')
|
||||||
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,20 +1,17 @@
|
|||||||
|
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
import minBy from 'lodash/minBy';
|
import minBy from 'lodash/minBy';
|
||||||
import { CodeTile, StatTile } from './tile';
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import compact from 'lodash/compact';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
VegaIcon,
|
VegaIcon,
|
||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
truncateMiddle,
|
truncateMiddle,
|
||||||
TextChildrenTooltip as Tooltip,
|
TextChildrenTooltip as Tooltip,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
import {
|
|
||||||
DEFAULT_AGGREGATION_DAYS,
|
|
||||||
useReferral,
|
|
||||||
useUpdateReferees,
|
|
||||||
} from './hooks/use-referral';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Table } from '../../components/table';
|
|
||||||
import {
|
import {
|
||||||
addDecimalsFormatNumber,
|
addDecimalsFormatNumber,
|
||||||
getDateFormat,
|
getDateFormat,
|
||||||
@ -24,17 +21,22 @@ import {
|
|||||||
removePaginationWrapper,
|
removePaginationWrapper,
|
||||||
} from '@vegaprotocol/utils';
|
} from '@vegaprotocol/utils';
|
||||||
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
|
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
|
||||||
import compact from 'lodash/compact';
|
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
|
||||||
import { useReferralProgram } from './hooks/use-referral-program';
|
|
||||||
import { useStakeAvailable } from './hooks/use-stake-available';
|
|
||||||
import sortBy from 'lodash/sortBy';
|
|
||||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import { useT, ns } from '../../lib/use-t';
|
import { useT, ns } from '../../lib/use-t';
|
||||||
import { Trans } from 'react-i18next';
|
import { useTeam } from '../../lib/hooks/use-team';
|
||||||
|
import { TeamAvatar } from '../../components/competitions/team-avatar';
|
||||||
|
import { TeamStats } from '../../components/competitions/team-stats';
|
||||||
|
import { Table } from '../../components/table';
|
||||||
|
import {
|
||||||
|
DEFAULT_AGGREGATION_DAYS,
|
||||||
|
useReferral,
|
||||||
|
useUpdateReferees,
|
||||||
|
} from './hooks/use-referral';
|
||||||
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
|
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
|
||||||
|
import { useReferralProgram } from './hooks/use-referral-program';
|
||||||
|
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
|
||||||
import { QUSDTooltip } from './qusd-tooltip';
|
import { QUSDTooltip } from './qusd-tooltip';
|
||||||
|
import { CodeTile, StatTile, Tile } from './tile';
|
||||||
|
|
||||||
export const ReferralStatistics = () => {
|
export const ReferralStatistics = () => {
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
@ -192,10 +194,7 @@ export const Statistics = ({
|
|||||||
nextBenefitTierEpochsValue,
|
nextBenefitTierEpochsValue,
|
||||||
} = useStats({ data, program });
|
} = useStats({ data, program });
|
||||||
|
|
||||||
const isApplyCodePreview = useMemo(
|
const isApplyCodePreview = data.referee === null;
|
||||||
() => data.referee === null,
|
|
||||||
[data.referee]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { benefitTiers } = useReferralProgram();
|
const { benefitTiers } = useReferralProgram();
|
||||||
|
|
||||||
@ -328,23 +327,6 @@ export const Statistics = ({
|
|||||||
</StatTile>
|
</StatTile>
|
||||||
);
|
);
|
||||||
|
|
||||||
const referrerTiles = (
|
|
||||||
<>
|
|
||||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
|
||||||
{baseCommissionTile}
|
|
||||||
{stakingMultiplierTile}
|
|
||||||
{finalCommissionTile}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
{codeTile}
|
|
||||||
{referrerVolumeTile}
|
|
||||||
{numberOfTradersTile}
|
|
||||||
{totalCommissionTile}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentBenefitTierTile = (
|
const currentBenefitTierTile = (
|
||||||
<StatTile
|
<StatTile
|
||||||
title={t('Current tier')}
|
title={t('Current tier')}
|
||||||
@ -416,8 +398,41 @@ export const Statistics = ({
|
|||||||
</StatTile>
|
</StatTile>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const eligibilityWarningOverlay = as === 'referee' && !isEligible && (
|
||||||
|
<div
|
||||||
|
data-testid="referral-eligibility-warning"
|
||||||
|
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const referrerTiles = (
|
||||||
|
<>
|
||||||
|
<Team teamId={data.code} />
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||||
|
{baseCommissionTile}
|
||||||
|
{stakingMultiplierTile}
|
||||||
|
{finalCommissionTile}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
|
{codeTile}
|
||||||
|
{referrerVolumeTile}
|
||||||
|
{numberOfTradersTile}
|
||||||
|
{totalCommissionTile}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const refereeTiles = (
|
const refereeTiles = (
|
||||||
<>
|
<>
|
||||||
|
<Team teamId={data.code} />
|
||||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||||
{currentBenefitTierTile}
|
{currentBenefitTierTile}
|
||||||
{runningVolumeTile}
|
{runningVolumeTile}
|
||||||
@ -432,20 +447,6 @@ export const Statistics = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const eligibilityWarning = as === 'referee' && !isEligible && (
|
|
||||||
<div
|
|
||||||
data-testid="referral-eligibility-warning"
|
|
||||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
|
|
||||||
>
|
|
||||||
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="referral-statistics"
|
data-testid="referral-statistics"
|
||||||
@ -460,8 +461,7 @@ export const Statistics = ({
|
|||||||
{as === 'referrer' && referrerTiles}
|
{as === 'referrer' && referrerTiles}
|
||||||
{as === 'referee' && refereeTiles}
|
{as === 'referee' && refereeTiles}
|
||||||
</div>
|
</div>
|
||||||
|
{eligibilityWarningOverlay}
|
||||||
{eligibilityWarning}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -574,3 +574,19 @@ export const RefereesTable = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Team = ({ teamId }: { teamId?: string }) => {
|
||||||
|
const { team, games, members } = useTeam(teamId);
|
||||||
|
|
||||||
|
if (!team) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tile className="flex gap-3 lg:gap-4">
|
||||||
|
<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>
|
||||||
|
<TeamStats members={members} games={games} />
|
||||||
|
</div>
|
||||||
|
</Tile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -13,11 +13,9 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
|||||||
import { useReferral } from './hooks/use-referral';
|
import { useReferral } from './hooks/use-referral';
|
||||||
import { REFERRAL_DOCS_LINK } from './constants';
|
import { REFERRAL_DOCS_LINK } from './constants';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { usePageTitleStore } from '../../stores';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { titlefy } from '@vegaprotocol/utils';
|
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
|
||||||
const Nav = () => {
|
const Nav = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@ -57,13 +55,7 @@ export const Referrals = () => {
|
|||||||
const loading = refereeLoading || referrerLoading;
|
const loading = refereeLoading || referrerLoading;
|
||||||
const showNav = !loading && !error && !referrer && !referee;
|
const showNav = !loading && !error && !referrer && !referee;
|
||||||
|
|
||||||
const { updateTitle } = usePageTitleStore((store) => ({
|
usePageTitle(t('Referrals'));
|
||||||
updateTitle: store.updateTitle,
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateTitle(titlefy([t('Referrals')]));
|
|
||||||
}, [updateTitle, t]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary feature="referrals">
|
<ErrorBoundary feature="referrals">
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export { Teams } from './teams';
|
|
@ -1,7 +0,0 @@
|
|||||||
export const Teams = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Teams</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
21
apps/trading/components/competitions/box.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { type HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
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 Box = (props: HTMLAttributes<HTMLDivElement>) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={classNames(
|
||||||
|
BORDER_COLOR,
|
||||||
|
GRADIENT,
|
||||||
|
'border rounded-lg',
|
||||||
|
'p-6',
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
41
apps/trading/components/competitions/competitions-cta.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { Box } from './box';
|
||||||
|
import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
|
||||||
|
import { DudeBadge } from './graphics/dude-badge';
|
||||||
|
|
||||||
|
export const CompetitionsActionsContainer = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children:
|
||||||
|
| ReactElement<typeof CompetitionsAction>
|
||||||
|
| Iterable<ReactElement<typeof CompetitionsAction>>;
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-1 md:grid-cols-3 grid-rows-4'
|
||||||
|
gap-6 mb-12"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CompetitionsAction = ({
|
||||||
|
variant,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actionElement,
|
||||||
|
}: {
|
||||||
|
variant: ComponentProps<typeof DudeBadge>['variant'];
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
actionElement: ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Box className="grid md:grid-rows-[subgrid] gap-6 row-span-4 text-center">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<DudeBadge variant={variant} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl">{title}</h2>
|
||||||
|
{description && <p className="text-muted">{description}</p>}
|
||||||
|
<div className="flex justify-center">{actionElement}</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
27
apps/trading/components/competitions/competitions-header.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
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="relative mb-4 lg:mb-20">
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="absolute top-20 right-[220px] md:right-[240px] max-sm:hidden"
|
||||||
|
>
|
||||||
|
<AnimatedDudeWithWire />
|
||||||
|
</div>
|
||||||
|
<div className="pt-6 lg:pt-20 sm:w-1/2">
|
||||||
|
<h1 className="text-3xl lg:text-6xl leading-[1em] font-alpha calt mb-2 lg:mb-10">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,71 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { getNumberFormat } from '@vegaprotocol/utils';
|
||||||
|
import { type useTeams } from '../../lib/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: '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),
|
||||||
|
status: td.closed ? t('Closed') : t('Open'),
|
||||||
|
volume: num(td.totalQuantumVolume),
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
44
apps/trading/components/competitions/games-container.tsx
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import { type TransferNode } from '@vegaprotocol/types';
|
||||||
|
import { ActiveRewardCard } from '../rewards-container/active-rewards';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
|
||||||
|
export const GamesContainer = ({
|
||||||
|
data,
|
||||||
|
currentEpoch,
|
||||||
|
}: {
|
||||||
|
data: TransferNode[];
|
||||||
|
currentEpoch: number;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
if (!data || data.length === 0) {
|
||||||
|
return (
|
||||||
|
<p className="mb-6 text-muted">
|
||||||
|
{t('There are currently no games available.')}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
40
apps/trading/components/competitions/graphics/dude-badge.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
};
|
110
apps/trading/components/competitions/graphics/rank.tsx
Normal file
@ -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%" stopColor="transparent" />
|
||||||
|
<stop offset="100%" stopColor="black" stopOpacity="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>
|
||||||
|
);
|
||||||
|
};
|
41
apps/trading/components/competitions/team-avatar.tsx
Normal file
@ -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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
195
apps/trading/components/competitions/team-stats.tsx
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
import { type ReactNode } from 'react';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import countBy from 'lodash/countBy';
|
||||||
|
import {
|
||||||
|
Pill,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
Tooltip,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { formatNumberRounded } from '@vegaprotocol/utils';
|
||||||
|
import {
|
||||||
|
type TeamStats as ITeamStats,
|
||||||
|
type Member,
|
||||||
|
type TeamGame,
|
||||||
|
} from '../../lib/hooks/use-team';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
export const TeamStats = ({
|
||||||
|
stats,
|
||||||
|
members,
|
||||||
|
games,
|
||||||
|
}: {
|
||||||
|
stats?: ITeamStats;
|
||||||
|
members?: Member[];
|
||||||
|
games?: TeamGame[];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StatSection>
|
||||||
|
<StatList>
|
||||||
|
<Stat
|
||||||
|
value={members ? members.length : 0}
|
||||||
|
label={t('Members')}
|
||||||
|
valueTestId="members-count-stat"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
value={stats ? stats.totalGamesPlayed : 0}
|
||||||
|
label={t('Total games')}
|
||||||
|
tooltip={t('Total number of games this team has participated in')}
|
||||||
|
valueTestId="total-games-stat"
|
||||||
|
/>
|
||||||
|
<StatSectionSeparator />
|
||||||
|
<Stat
|
||||||
|
value={
|
||||||
|
stats
|
||||||
|
? formatNumberRounded(
|
||||||
|
new BigNumber(stats.totalQuantumVolume || 0),
|
||||||
|
'1e3'
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
label={t('Total volume')}
|
||||||
|
valueTestId="total-volume-stat"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
value={
|
||||||
|
stats
|
||||||
|
? formatNumberRounded(
|
||||||
|
new BigNumber(stats.totalQuantumRewards || 0),
|
||||||
|
'1e3'
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
label={t('Rewards paid out')}
|
||||||
|
tooltip={'Total amount of rewards paid out to this team in qUSD'}
|
||||||
|
valueTestId="rewards-paid-stat"
|
||||||
|
/>
|
||||||
|
</StatList>
|
||||||
|
</StatSection>
|
||||||
|
{games && games.length ? (
|
||||||
|
<StatSection>
|
||||||
|
<FavoriteGame games={games} />
|
||||||
|
<StatSectionSeparator />
|
||||||
|
<LatestResults games={games} />
|
||||||
|
</StatSection>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LatestResults = ({ games }: { games: TeamGame[] }) => {
|
||||||
|
const t = useT();
|
||||||
|
const latestGames = games.slice(0, 5);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dl className="flex flex-col gap-1">
|
||||||
|
<dt className="text-muted text-sm">
|
||||||
|
{t('gameCount', { 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 as DispatchMetric
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
|
||||||
|
// rewardMetric is a string, should be typed as DispatchMetric
|
||||||
|
const favoriteMetricLabel =
|
||||||
|
DispatchMetricLabels[favoriteMetric as DispatchMetric];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dl className="flex flex-col gap-1">
|
||||||
|
<dt className="text-muted text-sm">{t('Favorite game')}</dt>
|
||||||
|
<dd>
|
||||||
|
<Pill className="inline-flex items-center gap-1 bg-transparent text-sm">
|
||||||
|
<VegaIcon
|
||||||
|
name={VegaIconNames.STAR}
|
||||||
|
className="text-vega-yellow-400 relative top-[-1px]"
|
||||||
|
/>{' '}
|
||||||
|
{favoriteMetricLabel}
|
||||||
|
</Pill>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StatSection = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<section className="flex flex-col lg:flex-row gap-4 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-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
|
||||||
|
{children}
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Stat = ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
tooltip,
|
||||||
|
valueTestId,
|
||||||
|
}: {
|
||||||
|
value: ReactNode;
|
||||||
|
label: ReactNode;
|
||||||
|
tooltip?: string;
|
||||||
|
valueTestId?: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
};
|
2
apps/trading/components/layouts-inner/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { LayoutWithSky } from './layout-with-sky';
|
||||||
|
export { LayoutWithGradient } from './layout-with-gradient';
|
@ -0,0 +1,14 @@
|
|||||||
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
export const LayoutWithGradient = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="relative h-full pt-5 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">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,10 +1,12 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { SKY_BACKGROUND } from './constants';
|
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
|
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
export const Layout = ({
|
export const SKY_BACKGROUND =
|
||||||
|
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[37%_0px] bg-[length:1440px] bg-no-repeat bg-local';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
@ -1 +1,2 @@
|
|||||||
export * from './layout-with-sidebar';
|
export { LayoutWithSidebar } from './layout-with-sidebar';
|
||||||
|
export { LayoutCentered } from './layout-centered';
|
||||||
|
@ -204,6 +204,13 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
|
|||||||
{t('Portfolio')}
|
{t('Portfolio')}
|
||||||
</NavbarLink>
|
</NavbarLink>
|
||||||
</NavbarItem>
|
</NavbarItem>
|
||||||
|
{featureFlags.TEAM_COMPETITION && (
|
||||||
|
<NavbarItem>
|
||||||
|
<NavbarLink to={Links.COMPETITIONS()} onClick={onClick}>
|
||||||
|
{t('Competitions')}
|
||||||
|
</NavbarLink>
|
||||||
|
</NavbarItem>
|
||||||
|
)}
|
||||||
{featureFlags.REFERRALS && (
|
{featureFlags.REFERRALS && (
|
||||||
<NavbarItem>
|
<NavbarItem>
|
||||||
<NavbarLink end={false} to={Links.REFERRALS()} onClick={onClick}>
|
<NavbarLink end={false} to={Links.REFERRALS()} onClick={onClick}>
|
||||||
|
1
apps/trading/components/rainbow-button/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { RainbowButton } from './rainbow-button';
|
35
apps/trading/components/rainbow-button/rainbow-button.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { type ButtonHTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
type RainbowButtonProps = {
|
||||||
|
variant?: 'full' | 'border';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const RainbowButton = ({
|
||||||
|
variant = 'full',
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: RainbowButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) => (
|
||||||
|
<button
|
||||||
|
className={classNames(
|
||||||
|
'bg-rainbow rounded-lg overflow-hidden disabled:opacity-40',
|
||||||
|
'hover:bg-rainbow-180 hover:animate-spin-rainbow',
|
||||||
|
{
|
||||||
|
'px-5 py-3 text-white': variant === 'full',
|
||||||
|
'p-[0.125rem]': variant === 'border',
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames({
|
||||||
|
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white px-5 py-3 rounded-[0.35rem] overflow-hidden':
|
||||||
|
variant === 'border',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
@ -105,7 +105,7 @@ describe('ActiveRewards', () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.getByText(/Liquidity provision fees received/i)
|
screen.getByText(/Liquidity provision fees received/i)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText('Entity scope')).toBeInTheDocument();
|
expect(screen.getByText('Individual scope')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Average position')).toBeInTheDocument();
|
expect(screen.getByText('Average position')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Ends in')).toBeInTheDocument();
|
expect(screen.getByText('Ends in')).toBeInTheDocument();
|
||||||
expect(screen.getByText('115431 epochs')).toBeInTheDocument();
|
expect(screen.getByText('115431 epochs')).toBeInTheDocument();
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
TradingInput,
|
TradingInput,
|
||||||
TinyScroll,
|
TinyScroll,
|
||||||
|
truncateMiddle,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import {
|
import {
|
||||||
@ -30,6 +31,9 @@ import {
|
|||||||
DispatchMetricLabels,
|
DispatchMetricLabels,
|
||||||
EntityScopeLabelMapping,
|
EntityScopeLabelMapping,
|
||||||
MarketState,
|
MarketState,
|
||||||
|
type DispatchStrategy,
|
||||||
|
IndividualScopeMapping,
|
||||||
|
IndividualScopeDescriptionMapping,
|
||||||
} from '@vegaprotocol/types';
|
} from '@vegaprotocol/types';
|
||||||
import { Card } from '../card/card';
|
import { Card } from '../card/card';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
@ -308,6 +312,9 @@ export const ActiveRewardCard = ({
|
|||||||
MarketState.STATE_CLOSED,
|
MarketState.STATE_CLOSED,
|
||||||
].includes(m.state)
|
].includes(m.state)
|
||||||
);
|
);
|
||||||
|
if (marketSettled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const assetInSettledMarket =
|
const assetInSettledMarket =
|
||||||
allMarkets &&
|
allMarkets &&
|
||||||
@ -326,10 +333,6 @@ export const ActiveRewardCard = ({
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (marketSettled) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gray out the cards that are related to suspended markets
|
// Gray out the cards that are related to suspended markets
|
||||||
const suspended = transferNode.markets?.some(
|
const suspended = transferNode.markets?.some(
|
||||||
(m) =>
|
(m) =>
|
||||||
@ -359,6 +362,7 @@ export const ActiveRewardCard = ({
|
|||||||
: getGradientClasses(dispatchStrategy.dispatchMetric);
|
: getGradientClasses(dispatchStrategy.dispatchMetric);
|
||||||
|
|
||||||
const entityScope = dispatchStrategy.entityScope;
|
const entityScope = dispatchStrategy.entityScope;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@ -474,83 +478,127 @@ export const ActiveRewardCard = ({
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{dispatchStrategy?.dispatchMetric && (
|
{dispatchStrategy?.dispatchMetric && (
|
||||||
<span className="text-muted text-sm h-[2rem]">
|
<span className="text-muted text-sm h-[2rem]">
|
||||||
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
|
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className="border-[0.5px] border-gray-700" />
|
<span className="border-[0.5px] border-gray-700" />
|
||||||
|
{kind.dispatchStrategy && (
|
||||||
<div className="flex justify-between flex-wrap items-center gap-3 text-xs">
|
<RewardRequirements
|
||||||
<span className="flex flex-col gap-1">
|
dispatchStrategy={kind.dispatchStrategy}
|
||||||
<span className="flex items-center gap-1 text-muted">
|
assetDecimalPlaces={transfer.asset?.decimals}
|
||||||
{t('Entity scope')}{' '}
|
/>
|
||||||
</span>
|
)}
|
||||||
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
{kind.dispatchStrategy?.teamScope && (
|
|
||||||
<Tooltip
|
|
||||||
description={
|
|
||||||
<span>{kind.dispatchStrategy?.teamScope}</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="flex items-center p-1 rounded-full border border-gray-600">
|
|
||||||
{<VegaIcon name={VegaIconNames.TEAM} size={16} />}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{kind.dispatchStrategy?.individualScope && (
|
|
||||||
<Tooltip
|
|
||||||
description={
|
|
||||||
<span>{kind.dispatchStrategy?.individualScope}</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="flex items-center p-1 rounded-full border border-gray-600">
|
|
||||||
{<VegaIcon name={VegaIconNames.MAN} size={16} />}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{/* Shows transfer status */}
|
|
||||||
{/* <StatusIndicator
|
|
||||||
status={transfer.status}
|
|
||||||
reason={transfer.reason}
|
|
||||||
/> */}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="flex flex-col gap-1">
|
|
||||||
<span className="flex items-center gap-1 text-muted">
|
|
||||||
{t('Staked VEGA')}{' '}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
{addDecimalsFormatNumber(
|
|
||||||
kind.dispatchStrategy?.stakingRequirement || 0,
|
|
||||||
transfer.asset?.decimals || 0
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="flex flex-col gap-1">
|
|
||||||
<span className="flex items-center gap-1 text-muted">
|
|
||||||
{t('Average position')}{' '}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
{addDecimalsFormatNumber(
|
|
||||||
kind.dispatchStrategy
|
|
||||||
?.notionalTimeWeightedAveragePositionRequirement || 0,
|
|
||||||
transfer.asset?.decimals || 0
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RewardRequirements = ({
|
||||||
|
dispatchStrategy,
|
||||||
|
assetDecimalPlaces = 0,
|
||||||
|
}: {
|
||||||
|
dispatchStrategy: DispatchStrategy;
|
||||||
|
assetDecimalPlaces: number | undefined;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dl className="flex justify-between flex-wrap items-center gap-3 text-xs">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<dt className="flex items-center gap-1 text-muted">
|
||||||
|
{t('{{entity}} scope', {
|
||||||
|
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
|
||||||
|
})}
|
||||||
|
</dt>
|
||||||
|
<dd className="flex items-center gap-1">
|
||||||
|
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<dt className="flex items-center gap-1 text-muted">
|
||||||
|
{t('Staked VEGA')}
|
||||||
|
</dt>
|
||||||
|
<dd className="flex items-center gap-1">
|
||||||
|
{addDecimalsFormatNumber(
|
||||||
|
dispatchStrategy?.stakingRequirement || 0,
|
||||||
|
assetDecimalPlaces
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<dt className="flex items-center gap-1 text-muted">
|
||||||
|
{t('Average position')}
|
||||||
|
</dt>
|
||||||
|
<dd className="flex items-center gap-1">
|
||||||
|
{addDecimalsFormatNumber(
|
||||||
|
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
|
||||||
|
0,
|
||||||
|
assetDecimalPlaces
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RewardEntityScope = ({
|
||||||
|
dispatchStrategy,
|
||||||
|
}: {
|
||||||
|
dispatchStrategy: DispatchStrategy;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
if (dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_TEAMS) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
description={
|
||||||
|
dispatchStrategy.teamScope?.length ? (
|
||||||
|
<div className="text-xs">
|
||||||
|
<p className="mb-1">{t('Eligible teams')}</p>
|
||||||
|
<ul>
|
||||||
|
{dispatchStrategy.teamScope.map((teamId) => {
|
||||||
|
if (!teamId) return null;
|
||||||
|
return <li key={teamId}>{truncateMiddle(teamId)}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
t('All teams are eligible')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{dispatchStrategy.teamScope?.length
|
||||||
|
? t('Some teams')
|
||||||
|
: t('All teams')}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
|
||||||
|
dispatchStrategy.individualScope
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
description={
|
||||||
|
IndividualScopeDescriptionMapping[dispatchStrategy.individualScope]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{IndividualScopeMapping[dispatchStrategy.individualScope]}</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const getGradientClasses = (d: DispatchMetric | undefined) => {
|
const getGradientClasses = (d: DispatchMetric | undefined) => {
|
||||||
switch (d) {
|
switch (d) {
|
||||||
case DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION:
|
case DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION:
|
||||||
|
@ -11,14 +11,21 @@ type TableColumnDefinition = {
|
|||||||
name: string;
|
name: string;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
headerClassName?: string;
|
||||||
testId?: string;
|
testId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type DataEntry = {
|
||||||
|
[key: TableColumnDefinition['name']]: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
type TableProps = {
|
type TableProps = {
|
||||||
columns: TableColumnDefinition[];
|
columns: TableColumnDefinition[];
|
||||||
data: Record<TableColumnDefinition['name'] | 'className', React.ReactNode>[];
|
data: DataEntry[];
|
||||||
noHeader?: boolean;
|
noHeader?: boolean;
|
||||||
noCollapse?: boolean;
|
noCollapse?: boolean;
|
||||||
|
onRowClick?: (index: number) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const INNER_BORDER_STYLE = `border-b ${BORDER_COLOR}`;
|
const INNER_BORDER_STYLE = `border-b ${BORDER_COLOR}`;
|
||||||
@ -34,6 +41,7 @@ export const Table = forwardRef<
|
|||||||
noHeader = false,
|
noHeader = false,
|
||||||
noCollapse = false,
|
noCollapse = false,
|
||||||
className,
|
className,
|
||||||
|
onRowClick,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
@ -41,13 +49,14 @@ export const Table = forwardRef<
|
|||||||
const header = (
|
const header = (
|
||||||
<thead className={classNames({ 'max-md:hidden': !noCollapse })}>
|
<thead className={classNames({ 'max-md:hidden': !noCollapse })}>
|
||||||
<tr>
|
<tr>
|
||||||
{columns.map(({ displayName, name, tooltip }) => (
|
{columns.map(({ displayName, name, tooltip, headerClassName }) => (
|
||||||
<th
|
<th
|
||||||
key={name}
|
key={name}
|
||||||
col-id={name}
|
col-id={name}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'px-5 py-3 text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
|
'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">
|
<span className="flex flex-row items-center gap-2">
|
||||||
@ -79,12 +88,17 @@ export const Table = forwardRef<
|
|||||||
>
|
>
|
||||||
{!noHeader && header}
|
{!noHeader && header}
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.map((d, i) => (
|
{data.map((dataEntry, i) => (
|
||||||
<tr
|
<tr
|
||||||
key={i}
|
key={i}
|
||||||
className={classNames(d['className'] as string, {
|
className={classNames(dataEntry['className'] as string, {
|
||||||
'max-md:flex flex-col w-full': !noCollapse,
|
'max-md:flex flex-col w-full': !noCollapse,
|
||||||
})}
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
if (onRowClick) {
|
||||||
|
onRowClick(i);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{columns.map(({ name, displayName, className, testId }, j) => (
|
{columns.map(({ name, displayName, className, testId }, j) => (
|
||||||
<td
|
<td
|
||||||
@ -114,7 +128,9 @@ export const Table = forwardRef<
|
|||||||
{displayName}
|
{displayName}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span data-testid={`${testId || name}-${i}`}>{d[name]}</span>
|
<span data-testid={`${testId || name}-${i}`}>
|
||||||
|
{dataEntry[name]}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
|
200
apps/trading/e2e/tests/teams/test_teams.py
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
import pytest
|
||||||
|
from playwright.sync_api import expect, Page
|
||||||
|
import vega_sim.proto.vega as vega_protos
|
||||||
|
from vega_sim.null_service import VegaServiceNull
|
||||||
|
from conftest import init_vega
|
||||||
|
from actions.utils import next_epoch
|
||||||
|
from fixtures.market import setup_continuous_market
|
||||||
|
from conftest import auth_setup, init_page, init_vega, risk_accepted_setup
|
||||||
|
from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D, MM_WALLET
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def vega(request):
|
||||||
|
with init_vega(request) as vega:
|
||||||
|
yield vega
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def team_page(vega, browser, request, setup_teams_and_games):
|
||||||
|
with init_page(vega, browser, request) as page:
|
||||||
|
risk_accepted_setup(page)
|
||||||
|
auth_setup(vega, page)
|
||||||
|
team_id = setup_teams_and_games["team_id"]
|
||||||
|
page.goto(f"/#/competitions/teams/{team_id}")
|
||||||
|
yield page
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def setup_teams_and_games(vega: VegaServiceNull):
|
||||||
|
tDAI_market = setup_continuous_market(vega)
|
||||||
|
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
|
||||||
|
vega.mint(key_name=PARTY_B.name, asset=tDAI_asset_id, amount=100000)
|
||||||
|
vega.mint(key_name=PARTY_C.name, asset=tDAI_asset_id, amount=100000)
|
||||||
|
vega.mint(key_name=PARTY_A.name, asset=tDAI_asset_id, amount=100000)
|
||||||
|
vega.mint(key_name=PARTY_D.name, asset=tDAI_asset_id, amount=100000)
|
||||||
|
next_epoch(vega=vega)
|
||||||
|
|
||||||
|
tDAI_asset_id = vega.find_asset_id(symbol="tDAI")
|
||||||
|
vega.update_network_parameter(
|
||||||
|
MM_WALLET.name, parameter="reward.asset", new_value=tDAI_asset_id
|
||||||
|
)
|
||||||
|
|
||||||
|
next_epoch(vega=vega)
|
||||||
|
team_name = create_team(vega)
|
||||||
|
|
||||||
|
next_epoch(vega)
|
||||||
|
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.wait_for_total_catchup()
|
||||||
|
|
||||||
|
current_epoch = vega.statistics().epoch_seq
|
||||||
|
game_start = current_epoch + 1
|
||||||
|
game_end = current_epoch + 11
|
||||||
|
|
||||||
|
current_epoch = vega.statistics().epoch_seq
|
||||||
|
print(f"[EPOCH: {current_epoch}] creating recurring transfer")
|
||||||
|
print(f"Game start: {game_start}")
|
||||||
|
print(f"Game game end: {game_end}")
|
||||||
|
|
||||||
|
vega.recurring_transfer(
|
||||||
|
from_key_name=PARTY_A.name,
|
||||||
|
from_account_type=vega_protos.vega.ACCOUNT_TYPE_GENERAL,
|
||||||
|
to_account_type=vega_protos.vega.ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES,
|
||||||
|
asset=tDAI_asset_id,
|
||||||
|
reference="reward",
|
||||||
|
asset_for_metric=tDAI_asset_id,
|
||||||
|
metric=vega_protos.vega.DISPATCH_METRIC_MAKER_FEES_PAID,
|
||||||
|
entity_scope=vega_protos.vega.ENTITY_SCOPE_TEAMS,
|
||||||
|
n_top_performers=1,
|
||||||
|
amount=100,
|
||||||
|
factor=1.0,
|
||||||
|
start_epoch=game_start,
|
||||||
|
end_epoch=game_end,
|
||||||
|
window_length=10
|
||||||
|
)
|
||||||
|
|
||||||
|
next_epoch(vega)
|
||||||
|
print(f"[EPOCH: {vega.statistics().epoch_seq}] starting order activity")
|
||||||
|
|
||||||
|
# Team statistics will only return data when team has been active
|
||||||
|
# for DEFAULT_AGGREGATION_EPOCHS epochs
|
||||||
|
#
|
||||||
|
# https://vegaprotocol.slack.com/archives/C02KVKMAE82/p1706635625851769?thread_ts=1706631542.576449&cid=C02KVKMAE82
|
||||||
|
|
||||||
|
# Create trading activity for 10 epochs (which is the default)
|
||||||
|
for i in range(10):
|
||||||
|
vega.submit_order(
|
||||||
|
trading_key=PARTY_B.name,
|
||||||
|
market_id=tDAI_market,
|
||||||
|
order_type="TYPE_MARKET",
|
||||||
|
time_in_force="TIME_IN_FORCE_IOC",
|
||||||
|
side="SIDE_BUY",
|
||||||
|
volume=1,
|
||||||
|
)
|
||||||
|
vega.submit_order(
|
||||||
|
trading_key=PARTY_A.name,
|
||||||
|
market_id=tDAI_market,
|
||||||
|
order_type="TYPE_MARKET",
|
||||||
|
time_in_force="TIME_IN_FORCE_IOC",
|
||||||
|
side="SIDE_BUY",
|
||||||
|
volume=1,
|
||||||
|
)
|
||||||
|
next_epoch(vega)
|
||||||
|
print(f"[EPOCH: {vega.statistics().epoch_seq}] {i} epoch passed")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"market_id": tDAI_market,
|
||||||
|
"asset_id": tDAI_asset_id,
|
||||||
|
"team_id": team_id,
|
||||||
|
"team_name": team_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
def test_team_page_games_table(team_page: Page):
|
||||||
|
team_page.get_by_test_id("games-toggle").click()
|
||||||
|
expect(team_page.get_by_test_id("games-toggle")).to_have_text("Games (1)")
|
||||||
|
expect(team_page.get_by_test_id("rank-0")).to_have_text("1")
|
||||||
|
expect(team_page.get_by_test_id("epoch-0")).to_have_text("18")
|
||||||
|
expect(team_page.get_by_test_id("type-0")).to_have_text("Price maker fees paid")
|
||||||
|
expect(team_page.get_by_test_id("amount-0")).to_have_text("100,000,000")
|
||||||
|
expect(team_page.get_by_test_id("participatingTeams-0")).to_have_text(
|
||||||
|
"1"
|
||||||
|
)
|
||||||
|
expect(team_page.get_by_test_id("participatingMembers-0")).to_have_text(
|
||||||
|
"2"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_team_page_members_table(team_page: Page):
|
||||||
|
team_page.get_by_test_id("members-toggle").click()
|
||||||
|
expect(team_page.get_by_test_id("members-toggle")).to_have_text("Members (3)")
|
||||||
|
expect(team_page.get_by_test_id("referee-0")).to_be_visible()
|
||||||
|
expect(team_page.get_by_test_id("joinedAt-0")).to_be_visible()
|
||||||
|
expect(team_page.get_by_test_id("joinedAtEpoch-0")).to_have_text("8")
|
||||||
|
|
||||||
|
def test_team_page_headline(team_page: Page, setup_teams_and_games
|
||||||
|
):
|
||||||
|
team_name = setup_teams_and_games["team_name"]
|
||||||
|
expect(team_page.get_by_test_id("team-name")).to_have_text(team_name)
|
||||||
|
expect(team_page.get_by_test_id("members-count-stat")).to_have_text("3")
|
||||||
|
|
||||||
|
expect(team_page.get_by_test_id("total-games-stat")).to_have_text(
|
||||||
|
"1"
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO this still seems wrong as its always 0
|
||||||
|
expect(team_page.get_by_test_id("total-volume-stat")).to_have_text(
|
||||||
|
"0"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(team_page.get_by_test_id("rewards-paid-stat")).to_have_text(
|
||||||
|
"100m"
|
||||||
|
)
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def competitions_page(vega, browser, request):
|
||||||
|
with init_page(vega, browser, request) as page:
|
||||||
|
risk_accepted_setup(page)
|
||||||
|
auth_setup(vega, page)
|
||||||
|
yield page
|
||||||
|
|
||||||
|
def test_leaderboard(competitions_page: Page, setup_teams_and_games):
|
||||||
|
team_name = setup_teams_and_games["team_name"]
|
||||||
|
competitions_page.goto(f"/#/competitions/")
|
||||||
|
expect(competitions_page.get_by_test_id("rank-0").locator(".text-yellow-300")).to_have_count(1)
|
||||||
|
expect(competitions_page.get_by_test_id("team-0")).to_have_text(team_name)
|
||||||
|
expect(competitions_page.get_by_test_id("status-0")).to_have_text("Open")
|
||||||
|
|
||||||
|
expect(competitions_page.get_by_test_id("earned-0")).to_have_text("100,000,000")
|
||||||
|
expect(competitions_page.get_by_test_id("games-0")).to_have_text("1")
|
||||||
|
|
||||||
|
# TODO still odd that this is 0
|
||||||
|
expect(competitions_page.get_by_test_id("volume-0")).to_have_text("-")
|
||||||
|
|
||||||
|
#TODO def test_games(competitions_page: Page):
|
||||||
|
#TODO currently no games appear which i think is a bug
|
90
apps/trading/lib/hooks/Team.graphql
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
fragment TeamFields on Team {
|
||||||
|
teamId
|
||||||
|
referrer
|
||||||
|
name
|
||||||
|
teamUrl
|
||||||
|
avatarUrl
|
||||||
|
createdAt
|
||||||
|
createdAtEpoch
|
||||||
|
closed
|
||||||
|
allowList
|
||||||
|
}
|
||||||
|
|
||||||
|
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, $aggregationEpochs: Int) {
|
||||||
|
teams(teamId: $teamId) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...TeamFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
partyTeams: teams(partyId: $partyId) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...TeamFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...TeamStatsFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
teamReferees(teamId: $teamId) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...TeamRefereeFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
games(entityScope: ENTITY_SCOPE_TEAMS) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...TeamGameFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
apps/trading/lib/hooks/Teams.graphql
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
13
apps/trading/lib/hooks/TeamsStatistics.graphql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
query TeamsStatistics($teamId: ID, $aggregationEpochs: Int) {
|
||||||
|
teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
teamId
|
||||||
|
totalQuantumVolume
|
||||||
|
totalQuantumRewards
|
||||||
|
totalGamesPlayed
|
||||||
|
gamesPlayed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
154
apps/trading/lib/hooks/__generated__/Team.ts
generated
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
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, allowList: Array<string> };
|
||||||
|
|
||||||
|
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: Types.DispatchMetric, 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: Types.DispatchMetric, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> };
|
||||||
|
|
||||||
|
export type TeamQueryVariables = Types.Exact<{
|
||||||
|
teamId: Types.Scalars['ID'];
|
||||||
|
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
|
||||||
|
aggregationEpochs?: Types.InputMaybe<Types.Scalars['Int']>;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
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, allowList: Array<string> } }> } | 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, allowList: Array<string> } }> } | 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: Types.DispatchMetric, 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
|
||||||
|
allowList
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
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, $aggregationEpochs: Int) {
|
||||||
|
teams(teamId: $teamId) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...TeamFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
partyTeams: teams(partyId: $partyId) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...TeamFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
|
||||||
|
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'
|
||||||
|
* aggregationEpochs: // value for 'aggregationEpochs'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
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>;
|
61
apps/trading/lib/hooks/__generated__/Teams.ts
generated
Normal file
@ -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>;
|
58
apps/trading/lib/hooks/__generated__/TeamsStatistics.ts
generated
Normal file
@ -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>;
|
37
apps/trading/lib/hooks/use-games.ts
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
const games = compact(data?.transfersConnection?.edges?.map((n) => n?.node))
|
||||||
|
.map((n) => n as TransferNode)
|
||||||
|
.filter((node) => {
|
||||||
|
const active = onlyActive ? isActiveReward(node, currentEpoch) : true;
|
||||||
|
return active && isScopedToTeams(node);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: games,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
};
|
18
apps/trading/lib/hooks/use-page-title.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { titlefy } from '@vegaprotocol/utils';
|
||||||
|
import { usePageTitleStore } from '../../stores';
|
||||||
|
|
||||||
|
export const usePageTitle = (title: string | string[]) => {
|
||||||
|
const { updateTitle } = usePageTitleStore((store) => ({
|
||||||
|
updateTitle: store.updateTitle,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const memotitle = useMemo(
|
||||||
|
() => titlefy(Array.isArray(title) ? title : [title]),
|
||||||
|
[title]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateTitle(memotitle);
|
||||||
|
}, [updateTitle, memotitle]);
|
||||||
|
};
|
33
apps/trading/lib/hooks/use-referral-set-transaction.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
useSimpleTransaction,
|
||||||
|
type Options,
|
||||||
|
type CreateReferralSet,
|
||||||
|
type UpdateReferralSet,
|
||||||
|
} from '@vegaprotocol/wallet';
|
||||||
|
import { useStakeAvailable } from './use-stake-available';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages state for creating a referral set or team
|
||||||
|
*/
|
||||||
|
export const useReferralSetTransaction = (opts?: Options) => {
|
||||||
|
const { stakeAvailable, requiredStake, isEligible } = useStakeAvailable();
|
||||||
|
|
||||||
|
const { status, result, error, send } = useSimpleTransaction({
|
||||||
|
onSuccess: opts?.onSuccess,
|
||||||
|
onError: opts?.onError,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = (tx: CreateReferralSet | UpdateReferralSet) => {
|
||||||
|
send(tx);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
err: error ? error : null,
|
||||||
|
code: result ? result.id : null,
|
||||||
|
status,
|
||||||
|
stakeAvailable,
|
||||||
|
requiredStake,
|
||||||
|
onSubmit,
|
||||||
|
isEligible,
|
||||||
|
};
|
||||||
|
};
|
73
apps/trading/lib/hooks/use-team.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import compact from 'lodash/compact';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import {
|
||||||
|
useTeamQuery,
|
||||||
|
type TeamFieldsFragment,
|
||||||
|
type TeamStatsFieldsFragment,
|
||||||
|
type TeamRefereeFieldsFragment,
|
||||||
|
type TeamEntityFragment,
|
||||||
|
} from './__generated__/Team';
|
||||||
|
import { DEFAULT_AGGREGATION_EPOCHS } from './use-teams';
|
||||||
|
|
||||||
|
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, partyId?: string) => {
|
||||||
|
const { data, loading, error, refetch } = useTeamQuery({
|
||||||
|
variables: {
|
||||||
|
teamId: teamId || '',
|
||||||
|
partyId,
|
||||||
|
aggregationEpochs: DEFAULT_AGGREGATION_EPOCHS,
|
||||||
|
},
|
||||||
|
skip: !teamId,
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
|
||||||
|
const partyTeam = data?.partyTeams?.edges?.length
|
||||||
|
? data.partyTeams.edges[0].node
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
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,
|
||||||
|
entities: edge.node.entities,
|
||||||
|
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,
|
||||||
|
refetch,
|
||||||
|
stats: teamStatsEdge?.node,
|
||||||
|
team: teamEdge?.node,
|
||||||
|
members,
|
||||||
|
games,
|
||||||
|
partyTeam,
|
||||||
|
};
|
||||||
|
};
|
72
apps/trading/lib/hooks/use-teams.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
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';
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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({
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: statsData,
|
||||||
|
loading: statsLoading,
|
||||||
|
error: statsError,
|
||||||
|
} = useTeamsStatisticsQuery({
|
||||||
|
variables: {
|
||||||
|
aggregationEpochs,
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
@ -16,7 +16,12 @@ export const Routes = {
|
|||||||
REFERRALS: '/referrals',
|
REFERRALS: '/referrals',
|
||||||
REFERRALS_APPLY_CODE: '/referrals/apply-code',
|
REFERRALS_APPLY_CODE: '/referrals/apply-code',
|
||||||
REFERRALS_CREATE_CODE: '/referrals/create-code',
|
REFERRALS_CREATE_CODE: '/referrals/create-code',
|
||||||
TEAMS: '/teams',
|
COMPETITIONS: '/competitions',
|
||||||
|
COMPETITIONS_TEAMS: '/competitions/teams',
|
||||||
|
COMPETITIONS_TEAM: '/competitions/teams/:teamId',
|
||||||
|
COMPETITIONS_CREATE_TEAM: '/competitions/teams/create',
|
||||||
|
COMPETITIONS_CREATE_TEAM_SOLO: '/competitions/teams/create?solo=true',
|
||||||
|
COMPETITIONS_UPDATE_TEAM: '/competitions/teams/:teamId/update',
|
||||||
FEES: '/fees',
|
FEES: '/fees',
|
||||||
REWARDS: '/rewards',
|
REWARDS: '/rewards',
|
||||||
} as const;
|
} as const;
|
||||||
@ -41,7 +46,14 @@ export const Links: ConsoleLinks = {
|
|||||||
REFERRALS: () => Routes.REFERRALS,
|
REFERRALS: () => Routes.REFERRALS,
|
||||||
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
|
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
|
||||||
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
|
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
|
||||||
TEAMS: () => Routes.TEAMS,
|
COMPETITIONS: () => Routes.COMPETITIONS,
|
||||||
|
COMPETITIONS_TEAMS: () => Routes.COMPETITIONS_TEAMS,
|
||||||
|
COMPETITIONS_TEAM: (teamId: string) =>
|
||||||
|
Routes.COMPETITIONS_TEAM.replace(':teamId', teamId),
|
||||||
|
COMPETITIONS_CREATE_TEAM: () => Routes.COMPETITIONS_CREATE_TEAM,
|
||||||
|
COMPETITIONS_CREATE_TEAM_SOLO: () => Routes.COMPETITIONS_CREATE_TEAM_SOLO,
|
||||||
|
COMPETITIONS_UPDATE_TEAM: (teamId: string) =>
|
||||||
|
Routes.COMPETITIONS_UPDATE_TEAM.replace(':teamId', teamId),
|
||||||
FEES: () => Routes.FEES,
|
FEES: () => Routes.FEES,
|
||||||
REWARDS: () => Routes.REWARDS,
|
REWARDS: () => Routes.REWARDS,
|
||||||
};
|
};
|
||||||
|
@ -2,8 +2,8 @@ import type { RouteObject } from 'react-router-dom';
|
|||||||
import { Navigate, useRoutes } from 'react-router-dom';
|
import { Navigate, useRoutes } from 'react-router-dom';
|
||||||
import { lazy, Suspense } from 'react';
|
import { lazy, Suspense } from 'react';
|
||||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
import { LayoutWithSidebar } from '../components/layouts';
|
import { LayoutWithSidebar, LayoutCentered } from '../components/layouts';
|
||||||
import { LayoutCentered } from '../components/layouts/layout-centered';
|
import { LayoutWithSky } from '../components/layouts-inner';
|
||||||
import { Home } from '../client-pages/home';
|
import { Home } from '../client-pages/home';
|
||||||
import { Liquidity } from '../client-pages/liquidity';
|
import { Liquidity } from '../client-pages/liquidity';
|
||||||
import { MarketsPage } from '../client-pages/markets';
|
import { MarketsPage } from '../client-pages/markets';
|
||||||
@ -14,9 +14,7 @@ import { Withdraw } from '../client-pages/withdraw';
|
|||||||
import { Transfer } from '../client-pages/transfer';
|
import { Transfer } from '../client-pages/transfer';
|
||||||
import { Fees } from '../client-pages/fees';
|
import { Fees } from '../client-pages/fees';
|
||||||
import { Rewards } from '../client-pages/rewards';
|
import { Rewards } from '../client-pages/rewards';
|
||||||
import { Teams } from '../client-pages/teams';
|
|
||||||
import { Routes as AppRoutes } from '../lib/links';
|
import { Routes as AppRoutes } from '../lib/links';
|
||||||
import { LayoutWithSky } from '../client-pages/referrals/layout';
|
|
||||||
import { Referrals } from '../client-pages/referrals/referrals';
|
import { Referrals } from '../client-pages/referrals/referrals';
|
||||||
import { ReferralStatistics } from '../client-pages/referrals/referral-statistics';
|
import { ReferralStatistics } from '../client-pages/referrals/referral-statistics';
|
||||||
import { ApplyCodeFormContainer } from '../client-pages/referrals/apply-code-form';
|
import { ApplyCodeFormContainer } from '../client-pages/referrals/apply-code-form';
|
||||||
@ -30,6 +28,11 @@ import { PortfolioSidebar } from '../client-pages/portfolio/portfolio-sidebar';
|
|||||||
import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar';
|
import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar';
|
||||||
import { MarketsSidebar } from '../client-pages/markets/markets-sidebar';
|
import { MarketsSidebar } from '../client-pages/markets/markets-sidebar';
|
||||||
import { useT } from '../lib/use-t';
|
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';
|
||||||
|
import { CompetitionsCreateTeam } from '../client-pages/competitions/competitions-create-team';
|
||||||
|
import { CompetitionsUpdateTeam } from '../client-pages/competitions/competitions-update-team';
|
||||||
|
|
||||||
// These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM
|
// 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
|
// Using dynamic imports is a workaround for this until pennant is published as ESM
|
||||||
@ -47,7 +50,7 @@ const NotFound = () => {
|
|||||||
|
|
||||||
export const useRouterConfig = (): RouteObject[] => {
|
export const useRouterConfig = (): RouteObject[] => {
|
||||||
const featureFlags = useFeatureFlags((state) => state.flags);
|
const featureFlags = useFeatureFlags((state) => state.flags);
|
||||||
return compact([
|
const routeConfig = compact([
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
element: <Home />,
|
element: <Home />,
|
||||||
@ -95,8 +98,34 @@ export const useRouterConfig = (): RouteObject[] => {
|
|||||||
: undefined,
|
: undefined,
|
||||||
featureFlags.TEAM_COMPETITION
|
featureFlags.TEAM_COMPETITION
|
||||||
? {
|
? {
|
||||||
path: AppRoutes.TEAMS,
|
path: AppRoutes.COMPETITIONS,
|
||||||
element: <Teams />,
|
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
|
||||||
|
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 />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: AppRoutes.COMPETITIONS_CREATE_TEAM,
|
||||||
|
element: <CompetitionsCreateTeam />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: AppRoutes.COMPETITIONS_UPDATE_TEAM,
|
||||||
|
element: <CompetitionsUpdateTeam />,
|
||||||
|
},
|
||||||
|
],
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
{
|
{
|
||||||
@ -189,6 +218,8 @@ export const useRouterConfig = (): RouteObject[] => {
|
|||||||
element: <NotFound />,
|
element: <NotFound />,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
return routeConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ClientRouter = () => {
|
export const ClientRouter = () => {
|
||||||
|
BIN
apps/trading/public/cover.png
Normal file
After Width: | Height: | Size: 668 KiB |
BIN
apps/trading/public/team-avatars/01.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
apps/trading/public/team-avatars/02.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
apps/trading/public/team-avatars/03.png
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
apps/trading/public/team-avatars/04.png
Normal file
After Width: | Height: | Size: 99 KiB |
BIN
apps/trading/public/team-avatars/05.png
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
apps/trading/public/team-avatars/06.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
apps/trading/public/team-avatars/07.png
Normal file
After Width: | Height: | Size: 92 KiB |
BIN
apps/trading/public/team-avatars/08.png
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
apps/trading/public/team-avatars/09.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
apps/trading/public/team-avatars/10.png
Normal file
After Width: | Height: | Size: 89 KiB |
BIN
apps/trading/public/team-avatars/11.png
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
apps/trading/public/team-avatars/12.png
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
apps/trading/public/team-avatars/13.png
Normal file
After Width: | Height: | Size: 108 KiB |
BIN
apps/trading/public/team-avatars/14.png
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
apps/trading/public/team-avatars/15.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
apps/trading/public/team-avatars/16.png
Normal file
After Width: | Height: | Size: 101 KiB |
BIN
apps/trading/public/team-avatars/17.png
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
apps/trading/public/team-avatars/18.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
apps/trading/public/team-avatars/19.png
Normal file
After Width: | Height: | Size: 97 KiB |
BIN
apps/trading/public/team-avatars/20.png
Normal file
After Width: | Height: | Size: 98 KiB |
@ -162,6 +162,7 @@ export const useProtocolUpgradeProposalLink = () => {
|
|||||||
export const EXPLORER_TX = '/txs/:hash';
|
export const EXPLORER_TX = '/txs/:hash';
|
||||||
export const EXPLORER_ORACLE = '/oracles/:id';
|
export const EXPLORER_ORACLE = '/oracles/:id';
|
||||||
export const EXPLORER_MARKET = '/markets/:id';
|
export const EXPLORER_MARKET = '/markets/:id';
|
||||||
|
export const EXPLORER_PARTIES = '/parties/:id';
|
||||||
|
|
||||||
// Etherscan pages
|
// Etherscan pages
|
||||||
export const ETHERSCAN_ADDRESS = '/address/:hash';
|
export const ETHERSCAN_ADDRESS = '/address/:hash';
|
||||||
|
@ -7,24 +7,31 @@
|
|||||||
"<0>No running Desktop App/CLI detected. Open your app now to connect or enter a</0> <1>custom wallet location</1>": "<0>No running Desktop App/CLI detected. Open your app now to connect or enter a</0> <1>custom wallet location</1>",
|
"<0>No running Desktop App/CLI detected. Open your app now to connect or enter a</0> <1>custom wallet location</1>": "<0>No running Desktop App/CLI detected. Open your app now to connect or enter a</0> <1>custom wallet location</1>",
|
||||||
"A percentage of commission earned by the referrer": "A percentage of commission earned by the referrer",
|
"A percentage of commission earned by the referrer": "A percentage of commission earned by the referrer",
|
||||||
"A successor to this market has been proposed": "A successor to this market has been proposed",
|
"A successor to this market has been proposed": "A successor to this market has been proposed",
|
||||||
|
"As a team creator, you cannot switch teams": "As a team creator, you cannot switch teams",
|
||||||
"About the referral program": "About the referral program",
|
"About the referral program": "About the referral program",
|
||||||
"Active": "Active",
|
"Active": "Active",
|
||||||
"Activity Streak": "Activity Streak",
|
"Activity Streak": "Activity Streak",
|
||||||
"All": "All",
|
"All": "All",
|
||||||
|
"All teams": "All teams",
|
||||||
|
"All teams are eligible": "All teams are eligible",
|
||||||
|
"Amount earned": "Amount earned",
|
||||||
"An unknown error occurred.": "An unknown error occurred.",
|
"An unknown error occurred.": "An unknown error occurred.",
|
||||||
"Anonymous": "Anonymous",
|
"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",
|
"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",
|
||||||
"Assessed over": "Assessed over",
|
"Assessed over": "Assessed over",
|
||||||
|
"Are you sure you want to join team: {{team}}": "Are you sure you want to join team: {{team}}",
|
||||||
"Asset (1)": "Asset (1)",
|
"Asset (1)": "Asset (1)",
|
||||||
"Assets": "Assets",
|
"Assets": "Assets",
|
||||||
"Available to withdraw this epoch": "Available to withdraw this epoch",
|
"Available to withdraw this epoch": "Available to withdraw this epoch",
|
||||||
"Average position": "Average position",
|
"Average position": "Average position",
|
||||||
|
"Avatar URL": "Avatar URL",
|
||||||
"Base commission rate": "Base commission rate",
|
"Base commission rate": "Base commission rate",
|
||||||
"Base rate": "Base rate",
|
"Base rate": "Base rate",
|
||||||
"Best bid": "Best bid",
|
"Best bid": "Best bid",
|
||||||
"Best offer": "Best offer",
|
"Best offer": "Best offer",
|
||||||
"Browse": "Browse",
|
"Browse": "Browse",
|
||||||
"By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>": "By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>",
|
"By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>": "By using the Vega Console, you acknowledge that you have read and understood the <0>Vega Console Disclaimer</0>",
|
||||||
|
"Cancel": "Cancel",
|
||||||
"Change (24h)": "Change (24h)",
|
"Change (24h)": "Change (24h)",
|
||||||
"Changes have been proposed for this market. <0>View proposals</0>": "Changes have been proposed for this market. <0>View proposals</0>",
|
"Changes have been proposed for this market. <0>View proposals</0>": "Changes have been proposed for this market. <0>View proposals</0>",
|
||||||
"Chart": "Chart",
|
"Chart": "Chart",
|
||||||
@ -37,9 +44,12 @@
|
|||||||
"Code must be be valid hex": "Code must be be valid hex",
|
"Code must be be valid hex": "Code must be be valid hex",
|
||||||
"Collateral": "Collateral",
|
"Collateral": "Collateral",
|
||||||
"Conduct your own due diligence and consult your financial advisor before making any investment decisions.": "Conduct your own due diligence and consult your financial advisor before making any investment decisions.",
|
"Conduct your own due diligence and consult your financial advisor before making any investment decisions.": "Conduct your own due diligence and consult your financial advisor before making any investment decisions.",
|
||||||
|
"Confrim": "Confrim",
|
||||||
"Confirm in wallet...": "Confirm in wallet...",
|
"Confirm in wallet...": "Confirm in wallet...",
|
||||||
|
"Confirming transaction...": "Confirming transaction...",
|
||||||
"Connect": "Connect",
|
"Connect": "Connect",
|
||||||
"Connect wallet": "Connect wallet",
|
"Connect wallet": "Connect wallet",
|
||||||
|
"Connect your wallet to join the team": "Connect your wallet to join the team",
|
||||||
"Connected node": "Connected node",
|
"Connected node": "Connected node",
|
||||||
"Console": "Console",
|
"Console": "Console",
|
||||||
"Continue sharing data": "Continue sharing data",
|
"Continue sharing data": "Continue sharing data",
|
||||||
@ -50,6 +60,10 @@
|
|||||||
"Could not initialize app": "Could not initialize app",
|
"Could not initialize app": "Could not initialize app",
|
||||||
"Countdown": "Countdown",
|
"Countdown": "Countdown",
|
||||||
"Create a referral code": "Create a referral code",
|
"Create a referral code": "Create a referral code",
|
||||||
|
"Create": "Create",
|
||||||
|
"Create a team": "Create a team",
|
||||||
|
"Create a simple referral code to enjoy the referrer commission outlined in the current referral program": "Create a simple referral code to enjoy the referrer commission outlined in the current referral program",
|
||||||
|
"Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards": "Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards",
|
||||||
"Current tier": "Current tier",
|
"Current tier": "Current tier",
|
||||||
"DISCLAIMER_P1": "Vega is a decentralised peer-to-peer protocol that can be used to trade derivatives with cryptoassets. The Vega Protocol is an implementation layer (layer one) protocol made of free, public, open-source or source-available software. Use of the Vega Protocol involves various risks, including but not limited to, losses while digital assets are supplied to the Vega Protocol and losses due to the fluctuation of prices of assets.",
|
"DISCLAIMER_P1": "Vega is a decentralised peer-to-peer protocol that can be used to trade derivatives with cryptoassets. The Vega Protocol is an implementation layer (layer one) protocol made of free, public, open-source or source-available software. Use of the Vega Protocol involves various risks, including but not limited to, losses while digital assets are supplied to the Vega Protocol and losses due to the fluctuation of prices of assets.",
|
||||||
"DISCLAIMER_P2": "Before using the Vega Protocol, review the relevant documentation at docs.vega.xyz to make sure that you understand how it works. Conduct your own due diligence and consult your financial advisor before making any investment decisions.",
|
"DISCLAIMER_P2": "Before using the Vega Protocol, review the relevant documentation at docs.vega.xyz to make sure that you understand how it works. Conduct your own due diligence and consult your financial advisor before making any investment decisions.",
|
||||||
@ -73,10 +87,14 @@
|
|||||||
"Docs": "Docs",
|
"Docs": "Docs",
|
||||||
"Earn commission & stake rewards": "Earn commission & stake rewards",
|
"Earn commission & stake rewards": "Earn commission & stake rewards",
|
||||||
"Earned by me": "Earned by me",
|
"Earned by me": "Earned by me",
|
||||||
|
"Eligible teams": "Eligible teams",
|
||||||
"Enactment date reached and usual auction exit checks pass": "Enactment date reached and usual auction exit checks pass",
|
"Enactment date reached and usual auction exit checks pass": "Enactment date reached and usual auction exit checks pass",
|
||||||
"Ends in": "Ends in",
|
"Ends in": "Ends in",
|
||||||
"Entity scope": "Entity scope",
|
"Entity scope": "Entity scope",
|
||||||
|
"{{entity}} scope": "{{entity}} scope",
|
||||||
"Environment not configured": "Environment not configured",
|
"Environment not configured": "Environment not configured",
|
||||||
|
"Epoch": "Epoch",
|
||||||
|
"epochs in referral set": "epochs in referral set",
|
||||||
"Epochs in set": "Epochs in set",
|
"Epochs in set": "Epochs in set",
|
||||||
"Epochs to next tier": "Epochs to next tier",
|
"Epochs to next tier": "Epochs to next tier",
|
||||||
"Expected in {{distance}}": "Expected in {{distance}}",
|
"Expected in {{distance}}": "Expected in {{distance}}",
|
||||||
@ -84,6 +102,7 @@
|
|||||||
"Experiment for free with virtual assets on <0>Fairground Testnet</0>": "Experiment for free with virtual assets on <0>Fairground Testnet</0>",
|
"Experiment for free with virtual assets on <0>Fairground Testnet</0>": "Experiment for free with virtual assets on <0>Fairground Testnet</0>",
|
||||||
"Expiry": "Expiry",
|
"Expiry": "Expiry",
|
||||||
"Explore": "Explore",
|
"Explore": "Explore",
|
||||||
|
"Favorite game": "Favorite game",
|
||||||
"Fees": "Fees",
|
"Fees": "Fees",
|
||||||
"Fees paid": "Fees paid",
|
"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",
|
"Fees work like a CEX with no per-transaction gas for orders": "Fees work like a CEX with no per-transaction gas for orders",
|
||||||
@ -100,12 +119,14 @@
|
|||||||
"Funding payments": "Funding payments",
|
"Funding payments": "Funding payments",
|
||||||
"Funding rate": "Funding rate",
|
"Funding rate": "Funding rate",
|
||||||
"Futures": "Futures",
|
"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 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",
|
"Generate code": "Generate code",
|
||||||
"Get rewards for providing liquidity. Get rewards for providing liquidity.": "Get rewards for providing liquidity. Get rewards for providing liquidity.",
|
"Get rewards for providing liquidity.": "Get rewards for providing liquidity.",
|
||||||
"Get started": "Get started",
|
"Get started": "Get started",
|
||||||
"Give Feedback": "Give Feedback",
|
"Give Feedback": "Give Feedback",
|
||||||
"Go back and try again": "Go back and try again",
|
"Go back and try again": "Go back and try again",
|
||||||
|
"Got to competitions": "Go to competitions",
|
||||||
"Governance": "Governance",
|
"Governance": "Governance",
|
||||||
"Governance vote for this market has been rejected": "Governance vote for this market has been rejected",
|
"Governance vote for this market has been rejected": "Governance vote for this market has been rejected",
|
||||||
"Governance vote for this market is valid and has been accepted": "Governance vote for this market is valid and has been accepted",
|
"Governance vote for this market is valid and has been accepted": "Governance vote for this market is valid and has been accepted",
|
||||||
@ -127,10 +148,18 @@
|
|||||||
"Inactive": "Inactive",
|
"Inactive": "Inactive",
|
||||||
"Index Price": "Index Price",
|
"Index Price": "Index Price",
|
||||||
"Indicators": "Indicators",
|
"Indicators": "Indicators",
|
||||||
|
"Invalid image URL": "Invalid image URL",
|
||||||
|
"Invalid URL": "Invalid URL",
|
||||||
"Individual": "Individual",
|
"Individual": "Individual",
|
||||||
"Infrastructure": "Infrastructure",
|
"Infrastructure": "Infrastructure",
|
||||||
"Interval: {{interval}}": "Interval: {{interval}}",
|
"Interval: {{interval}}": "Interval: {{interval}}",
|
||||||
"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.",
|
"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 team": "Join team",
|
||||||
|
"Joined": "Joined",
|
||||||
|
"Joined at": "Joined at",
|
||||||
|
"Joined epoch": "Joined epoch",
|
||||||
|
"gameCount_one": "Last game result",
|
||||||
|
"gameCount_other": "Last {{count}} game results",
|
||||||
"Learn about providing liquidity": "Learn about providing liquidity",
|
"Learn about providing liquidity": "Learn about providing liquidity",
|
||||||
"Learn more": "Learn more",
|
"Learn more": "Learn more",
|
||||||
"Ledger entries": "Ledger entries",
|
"Ledger entries": "Ledger entries",
|
||||||
@ -141,6 +170,7 @@
|
|||||||
"Low fees and no cost to place orders": "Low fees and no cost to place orders",
|
"Low fees and no cost to place orders": "Low fees and no cost to place orders",
|
||||||
"Mainnet status & incidents": "Mainnet status & incidents",
|
"Mainnet status & incidents": "Mainnet status & incidents",
|
||||||
"Make withdrawal": "Make withdrawal",
|
"Make withdrawal": "Make withdrawal",
|
||||||
|
"Make team private": "Make team private",
|
||||||
"Maker": "Maker",
|
"Maker": "Maker",
|
||||||
"Mark Price": "Mark Price",
|
"Mark Price": "Mark Price",
|
||||||
"Mark price": "Mark price",
|
"Mark price": "Mark price",
|
||||||
@ -149,6 +179,8 @@
|
|||||||
"Market specification": "Market specification",
|
"Market specification": "Market specification",
|
||||||
"Market triggers cancellation or governance vote has passed to cancel": "Market triggers cancellation or governance vote has passed to cancel",
|
"Market triggers cancellation or governance vote has passed to cancel": "Market triggers cancellation or governance vote has passed to cancel",
|
||||||
"Markets": "Markets",
|
"Markets": "Markets",
|
||||||
|
"Members": "Members",
|
||||||
|
"Members ({{count}})": "Members ({{count}})",
|
||||||
"Menu": "Menu",
|
"Menu": "Menu",
|
||||||
"Metamask Snap <0>quick start</0>": "Metamask Snap <0>quick start</0>",
|
"Metamask Snap <0>quick start</0>": "Metamask Snap <0>quick start</0>",
|
||||||
"Min. epochs": "Min. epochs",
|
"Min. epochs": "Min. epochs",
|
||||||
@ -157,16 +189,18 @@
|
|||||||
"My liquidity provision": "My liquidity provision",
|
"My liquidity provision": "My liquidity provision",
|
||||||
"My trading fees": "My trading fees",
|
"My trading fees": "My trading fees",
|
||||||
"Name": "Name",
|
"Name": "Name",
|
||||||
"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 closed orders": "No closed orders",
|
"No closed orders": "No closed orders",
|
||||||
"No data": "No data",
|
"No data": "No data",
|
||||||
"No deposits": "No deposits",
|
"No deposits": "No deposits",
|
||||||
"No funding history data": "No funding history data",
|
"No funding history data": "No funding history data",
|
||||||
"No future markets.": "No future markets.",
|
"No future markets.": "No future markets.",
|
||||||
|
"No games": "No games",
|
||||||
"No ledger entries to export": "No ledger entries to export",
|
"No ledger entries to export": "No ledger entries to export",
|
||||||
"No market": "No market",
|
"No market": "No market",
|
||||||
"No markets": "No markets",
|
"No markets": "No markets",
|
||||||
"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 open orders": "No open orders",
|
||||||
"No orders": "No orders",
|
"No orders": "No orders",
|
||||||
"No party accepts any liability for any losses whatsoever.": "No party accepts any liability for any losses whatsoever.",
|
"No party accepts any liability for any losses whatsoever.": "No party accepts any liability for any losses whatsoever.",
|
||||||
@ -179,6 +213,8 @@
|
|||||||
"No third party has access to your funds.": "No third party has access to your funds.",
|
"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 volume discount program active": "No volume discount program active",
|
||||||
"No withdrawals": "No withdrawals",
|
"No withdrawals": "No withdrawals",
|
||||||
|
"No. of participating members": "No. of participating members",
|
||||||
|
"No. of participating teams": "No. of participating teams",
|
||||||
"Node: {{VEGA_URL}} is unsuitable": "Node: {{VEGA_URL}} is unsuitable",
|
"Node: {{VEGA_URL}} is unsuitable": "Node: {{VEGA_URL}} is unsuitable",
|
||||||
"Non-custodial and pseudonymous": "Non-custodial and pseudonymous",
|
"Non-custodial and pseudonymous": "Non-custodial and pseudonymous",
|
||||||
"None": "None",
|
"None": "None",
|
||||||
@ -192,11 +228,16 @@
|
|||||||
"Order": "Order",
|
"Order": "Order",
|
||||||
"Orderbook": "Orderbook",
|
"Orderbook": "Orderbook",
|
||||||
"Orders": "Orders",
|
"Orders": "Orders",
|
||||||
|
"Owner": "Owner",
|
||||||
"PRNT": "PRNT",
|
"PRNT": "PRNT",
|
||||||
"Page not found": "Page not found",
|
"Page not found": "Page not found",
|
||||||
"Parent of a market": "Parent of a market",
|
"Parent of a market": "Parent of a market",
|
||||||
"Pennant": "Pennant",
|
"Pennant": "Pennant",
|
||||||
"Perpetuals": "Perpetuals",
|
"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 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",
|
"Please connect Vega wallet": "Please connect Vega wallet",
|
||||||
"Portfolio": "Portfolio",
|
"Portfolio": "Portfolio",
|
||||||
@ -206,12 +247,19 @@
|
|||||||
"Propose a new market": "Propose a new market",
|
"Propose a new market": "Propose a new market",
|
||||||
"Proposed final price is {{price}} {{assetSymbol}}.": "Proposed final price is {{price}} {{assetSymbol}}.",
|
"Proposed final price is {{price}} {{assetSymbol}}.": "Proposed final price is {{price}} {{assetSymbol}}.",
|
||||||
"Proposed markets": "Proposed markets",
|
"Proposed markets": "Proposed markets",
|
||||||
|
"Provide a link so users can learn more about your team": "Provide a link so users can learn more about your team",
|
||||||
|
"Provide a URL to a hosted image": "Provide a URL to a hosted image",
|
||||||
"Providing liquidity": "Providing liquidity",
|
"Providing liquidity": "Providing liquidity",
|
||||||
|
"Public key allow list": "Public key allow list",
|
||||||
"Purpose built proof of stake blockchain": "Purpose built proof of stake blockchain",
|
"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",
|
"Read the terms": "Read the terms",
|
||||||
"Ready to trade": "Ready to trade",
|
"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>",
|
"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",
|
"Redeem rewards": "Redeem rewards",
|
||||||
|
"Referee": "Referee",
|
||||||
"Referral benefits": "Referral benefits",
|
"Referral benefits": "Referral benefits",
|
||||||
"Referral discount": "Referral discount",
|
"Referral discount": "Referral discount",
|
||||||
"Referrals": "Referrals",
|
"Referrals": "Referrals",
|
||||||
@ -220,6 +268,7 @@
|
|||||||
"Referrers earn commission based on a percentage of the taker fees their referees pay": "Referrers earn commission based on a percentage of the taker fees their referees pay",
|
"Referrers earn commission based on a percentage of the taker fees their referees pay": "Referrers earn commission based on a percentage of the taker fees their referees pay",
|
||||||
"Referrers generate a code assigned to their key via an on chain transaction": "Referrers generate a code assigned to their key via an on chain transaction",
|
"Referrers generate a code assigned to their key via an on chain transaction": "Referrers generate a code assigned to their key via an on chain transaction",
|
||||||
"Rejected": "Rejected",
|
"Rejected": "Rejected",
|
||||||
|
"Required": "Required",
|
||||||
"Required epochs": "Required epochs",
|
"Required epochs": "Required epochs",
|
||||||
"Required for next tier": "Required for next tier",
|
"Required for next tier": "Required for next tier",
|
||||||
"Reset Columns": "Reset Columns",
|
"Reset Columns": "Reset Columns",
|
||||||
@ -260,8 +309,15 @@
|
|||||||
"Successors to this market have been proposed": "Successors to this market have been proposed",
|
"Successors to this market have been proposed": "Successors to this market have been proposed",
|
||||||
"Supplied stake": "Supplied stake",
|
"Supplied stake": "Supplied stake",
|
||||||
"Suspended due to price or liquidity monitoring trigger": "Suspended due to price or liquidity monitoring trigger",
|
"Suspended due to price or liquidity monitoring trigger": "Suspended due to price or liquidity monitoring trigger",
|
||||||
|
"Switch team": "Switch team",
|
||||||
|
"Switching team will move you from '{{fromTeam}}' to '{{toTeam}}' at the end of the epoch. Are you sure?": "Switching team will move you from '{{fromTeam}}' to '{{toTeam}}' at the end of the epoch. Are you sure?",
|
||||||
"Target stake": "Target stake",
|
"Target stake": "Target stake",
|
||||||
"Team": "Team",
|
"Team": "Team",
|
||||||
|
"Team name": "Team name",
|
||||||
|
"Team creation transaction successful": "Team creation transaction successful",
|
||||||
|
"Team joined": "Team joined",
|
||||||
|
"Team switch successful. You will switch team at the end of the epoch.": "Team switch successful. You will switch team at the end of the epoch.",
|
||||||
|
|
||||||
"The amount of fees paid to liquidity providers across the whole market during the last epoch {{epoch}}.": "The amount of fees paid to liquidity providers across the whole market during the last epoch {{epoch}}.",
|
"The amount of fees paid to liquidity providers across the whole market during the last epoch {{epoch}}.": "The amount of fees paid to liquidity providers across the whole market during the last epoch {{epoch}}.",
|
||||||
"The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee": "The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee",
|
"The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee": "The commission is taken from the infrastructure fee, maker fee, and liquidity provider fee, not from the referee",
|
||||||
"The external time weighted average price (TWAP) received from the data source defined in the data sourcing specification.": "The external time weighted average price (TWAP) received from the data source defined in the data sourcing specification.",
|
"The external time weighted average price (TWAP) received from the data source defined in the data sourcing specification.": "The external time weighted average price (TWAP) received from the data source defined in the data sourcing specification.",
|
||||||
@ -283,10 +339,17 @@
|
|||||||
"Tier {{userTier}}": "Tier {{userTier}}",
|
"Tier {{userTier}}": "Tier {{userTier}}",
|
||||||
"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.",
|
"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",
|
"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 discount": "Total discount",
|
||||||
"Total distributed": "Total distributed",
|
"Total distributed": "Total distributed",
|
||||||
"Total fee after discount": "Total fee after discount",
|
"Total fee after discount": "Total fee after discount",
|
||||||
"Total fee before discount": "Total fee before 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>)",
|
||||||
"Trader": "Trader",
|
"Trader": "Trader",
|
||||||
"Trades": "Trades",
|
"Trades": "Trades",
|
||||||
"Trading": "Trading",
|
"Trading": "Trading",
|
||||||
@ -297,11 +360,14 @@
|
|||||||
"Trading on market {{name}} will stop on {{date}}": "Trading on market {{name}} will stop on {{date}}",
|
"Trading on market {{name}} will stop on {{date}}": "Trading on market {{name}} will stop on {{date}}",
|
||||||
"TradingView": "TradingView",
|
"TradingView": "TradingView",
|
||||||
"Transfer": "Transfer",
|
"Transfer": "Transfer",
|
||||||
|
"Type": "Type",
|
||||||
"Unknown": "Unknown",
|
"Unknown": "Unknown",
|
||||||
"Unknown settlement date": "Unknown settlement date",
|
"Unknown settlement date": "Unknown settlement date",
|
||||||
|
"URL": "URL",
|
||||||
|
"Use a comma separated list to allow only specific public keys to join the team": "Use a comma separated list to allow only specific public keys to join the team",
|
||||||
|
"Vega chart": "Vega chart",
|
||||||
"Vega Reward pot": "Vega Reward pot",
|
"Vega Reward pot": "Vega Reward pot",
|
||||||
"Vega Wallet <0>full featured<0>": "Vega Wallet <0>full featured<0>",
|
"Vega Wallet <0>full featured<0>": "Vega Wallet <0>full featured<0>",
|
||||||
"Vega chart": "Vega chart",
|
|
||||||
"Vesting": "Vesting",
|
"Vesting": "Vesting",
|
||||||
"Vesting multiplier": "Vesting multiplier",
|
"Vesting multiplier": "Vesting multiplier",
|
||||||
"Vesting {{assetSymbol}}": "Vesting {{assetSymbol}}",
|
"Vesting {{assetSymbol}}": "Vesting {{assetSymbol}}",
|
||||||
@ -314,6 +380,7 @@
|
|||||||
"View proposals": "View proposals",
|
"View proposals": "View proposals",
|
||||||
"View settlement asset details": "View settlement asset details",
|
"View settlement asset details": "View settlement asset details",
|
||||||
"View successor market": "View successor market",
|
"View successor market": "View successor market",
|
||||||
|
"View team": "View team",
|
||||||
"Volume": "Volume",
|
"Volume": "Volume",
|
||||||
"Volume (24h)": "Volume (24h)",
|
"Volume (24h)": "Volume (24h)",
|
||||||
"Volume discount": "Volume discount",
|
"Volume discount": "Volume discount",
|
||||||
@ -337,7 +404,6 @@
|
|||||||
"checkOutProposalsAndVote_one": "Check out the terms of the proposal and vote:",
|
"checkOutProposalsAndVote_one": "Check out the terms of the proposal and vote:",
|
||||||
"checkOutProposalsAndVote_other": "Check out the terms of the proposals and vote:",
|
"checkOutProposalsAndVote_other": "Check out the terms of the proposals and vote:",
|
||||||
"epochStreak_one": "{{count}} epoch streak",
|
"epochStreak_one": "{{count}} epoch streak",
|
||||||
"epochs in referral set": "epochs in referral set",
|
|
||||||
"epochsStreak": "{{count}} epochs streak",
|
"epochsStreak": "{{count}} epochs streak",
|
||||||
"minTradingVolume": "Min. trading volume (last {{count}} epochs)",
|
"minTradingVolume": "Min. trading volume (last {{count}} epochs)",
|
||||||
"minTradingVolume_one": "Min. trading volume (last {{count}} epoch)",
|
"minTradingVolume_one": "Min. trading volume (last {{count}} epoch)",
|
||||||
@ -347,22 +413,8 @@
|
|||||||
"myVolume_other": "My volume (last {{count}} epochs)",
|
"myVolume_other": "My volume (last {{count}} epochs)",
|
||||||
"numberEpochs": "{{count}} epochs",
|
"numberEpochs": "{{count}} epochs",
|
||||||
"numberEpochs_one": "{{count}} epoch",
|
"numberEpochs_one": "{{count}} epoch",
|
||||||
"numberEpochs_other": "{{count}} epochs",
|
"Rewards paid out": "Rewards paid out",
|
||||||
"pastEpochs": "Past {{count}} epochs",
|
"{{reward}}x": "{{reward}}x",
|
||||||
"pastEpochs_one": "Past {{count}} epoch",
|
|
||||||
"pastEpochs_other": "Past {{count}} epochs",
|
|
||||||
"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",
|
|
||||||
"referralStatisticsCommission": "Commission earned in <0>qUSD</0> (<1>last {{count}} epochs</1>)",
|
|
||||||
"referralStatisticsCommission_one": "Commission earned in <0>qUSD</0> (<1>last {{count}} epoch</1>)",
|
|
||||||
"referralStatisticsCommission_other": "Commission earned in <0>qUSD</0> (<1>last {{count}} epochs</1>)",
|
|
||||||
"runningNotionalOverEpochs": "Combined running notional over the {{count}} epochs",
|
|
||||||
"runningNotionalOverEpochs_one": "Combined running notional over the {{count}} epoch",
|
|
||||||
"runningNotionalOverEpochs_other": "Combined running notional over the {{count}} epochs",
|
|
||||||
"to": "to",
|
|
||||||
"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>)",
|
|
||||||
"userActive": "{{active}} trader: {{count}} epochs so far",
|
"userActive": "{{active}} trader: {{count}} epochs so far",
|
||||||
"userInactive": "{{active}} trader: {{count}} epochs so far, you will lose your streak in {{remaining}} epochs!",
|
"userInactive": "{{active}} trader: {{count}} epochs so far, you will lose your streak in {{remaining}} epochs!",
|
||||||
"volumeLastEpochs": "Volume (last {{count}} epochs)",
|
"volumeLastEpochs": "Volume (last {{count}} epochs)",
|
||||||
@ -375,6 +427,5 @@
|
|||||||
"{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot",
|
"{{assetSymbol}} Reward pot": "{{assetSymbol}} Reward pot",
|
||||||
"{{checkedAssets}} Assets": "{{checkedAssets}} Assets",
|
"{{checkedAssets}} Assets": "{{checkedAssets}} Assets",
|
||||||
"{{distance}} ago": "{{distance}} ago",
|
"{{distance}} ago": "{{distance}} ago",
|
||||||
"{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision",
|
"{{instrumentCode}} liquidity provision": "{{instrumentCode}} liquidity provision"
|
||||||
"{{reward}}x": "{{reward}}x"
|
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
"Expired on {{date}}": "Expired on {{date}}",
|
"Expired on {{date}}": "Expired on {{date}}",
|
||||||
"Invalid Ethereum address": "Invalid Ethereum address",
|
"Invalid Ethereum address": "Invalid Ethereum address",
|
||||||
"Invalid Vega key": "Invalid Vega key",
|
"Invalid Vega key": "Invalid Vega key",
|
||||||
|
"Invalid URL": "Invalid URL",
|
||||||
"Mark": "Mark",
|
"Mark": "Mark",
|
||||||
"Must be valid JSON": "Must be valid JSON",
|
"Must be valid JSON": "Must be valid JSON",
|
||||||
"Not time-based": "Not time-based",
|
"Not time-based": "Not time-based",
|
||||||
|
@ -48,6 +48,8 @@
|
|||||||
"Supported browsers": "Supported browsers",
|
"Supported browsers": "Supported browsers",
|
||||||
"The user rejected the wallet connection": "The user rejected the wallet connection",
|
"The user rejected the wallet connection": "The user rejected the wallet connection",
|
||||||
"To complete your wallet connection, set your wallet network in your app to \"{{appChainId}}\".": "To complete your wallet connection, set your wallet network in your app to \"{{appChainId}}\".",
|
"To complete your wallet connection, set your wallet network in your app to \"{{appChainId}}\".": "To complete your wallet connection, set your wallet network in your app to \"{{appChainId}}\".",
|
||||||
|
"Transaction could not be sent": "Transaction could not be sent",
|
||||||
|
"Transaction was not successful": "Transaction was not successful",
|
||||||
"Try again": "Try again",
|
"Try again": "Try again",
|
||||||
"Understand the risk": "Understand the risk",
|
"Understand the risk": "Understand the risk",
|
||||||
"Use the Desktop App/CLI": "Use the Desktop App/CLI",
|
"Use the Desktop App/CLI": "Use the Desktop App/CLI",
|
||||||
@ -57,6 +59,7 @@
|
|||||||
"Verifying chain": "Verifying chain",
|
"Verifying chain": "Verifying chain",
|
||||||
"View as party": "View as party",
|
"View as party": "View as party",
|
||||||
"VIEW AS VEGA USER": "VIEW AS VEGA USER",
|
"VIEW AS VEGA USER": "VIEW AS VEGA USER",
|
||||||
|
"Wallet rejected transaction": "Wallet rejected transaction",
|
||||||
"Wrong Network": "Wrong Network",
|
"Wrong Network": "Wrong Network",
|
||||||
"Wrong network": "Wrong network",
|
"Wrong network": "Wrong network",
|
||||||
"your browser": "your browser"
|
"your browser": "your browser"
|
||||||
|
24
libs/types/src/__generated__/types.ts
generated
@ -1879,12 +1879,8 @@ export type LiquidityFeeSettings = {
|
|||||||
/** Configuration of a market liquidity monitoring parameters */
|
/** Configuration of a market liquidity monitoring parameters */
|
||||||
export type LiquidityMonitoringParameters = {
|
export type LiquidityMonitoringParameters = {
|
||||||
__typename?: 'LiquidityMonitoringParameters';
|
__typename?: 'LiquidityMonitoringParameters';
|
||||||
/** Specifies by how many seconds an auction should be extended if leaving the auction were to trigger a liquidity auction */
|
|
||||||
auctionExtensionSecs: Scalars['Int'];
|
|
||||||
/** Specifies parameters related to target stake calculation */
|
/** Specifies parameters related to target stake calculation */
|
||||||
targetStakeParameters: TargetStakeParameters;
|
targetStakeParameters: TargetStakeParameters;
|
||||||
/** Specifies the triggering ratio for entering liquidity auction */
|
|
||||||
triggeringRatio: Scalars['String'];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** A special order type for liquidity providers */
|
/** A special order type for liquidity providers */
|
||||||
@ -4002,10 +3998,10 @@ export type Perpetual = {
|
|||||||
fundingRateScalingFactor: Scalars['String'];
|
fundingRateScalingFactor: Scalars['String'];
|
||||||
/** Upper bound for the funding-rate such that the funding-rate will never be higher than this value */
|
/** Upper bound for the funding-rate such that the funding-rate will never be higher than this value */
|
||||||
fundingRateUpperBound: Scalars['String'];
|
fundingRateUpperBound: Scalars['String'];
|
||||||
/** Optional configuration driving the index price calculation for perpetual product */
|
|
||||||
indexPriceConfig?: Maybe<CompositePriceConfiguration>;
|
|
||||||
/** Continuously compounded interest rate used in funding rate calculation, in the range [-1, 1] */
|
/** Continuously compounded interest rate used in funding rate calculation, in the range [-1, 1] */
|
||||||
interestRate: Scalars['String'];
|
interestRate: Scalars['String'];
|
||||||
|
/** Optional configuration driving the internal composite price calculation for perpetual product */
|
||||||
|
internalCompositePriceConfig?: Maybe<CompositePriceConfiguration>;
|
||||||
/** Controls how much the upcoming funding payment liability contributes to party's margin, in the range [0, 1] */
|
/** Controls how much the upcoming funding payment liability contributes to party's margin, in the range [0, 1] */
|
||||||
marginFundingFactor: Scalars['String'];
|
marginFundingFactor: Scalars['String'];
|
||||||
/** Quote name of the instrument */
|
/** Quote name of the instrument */
|
||||||
@ -4023,14 +4019,14 @@ export type PerpetualData = {
|
|||||||
fundingPayment?: Maybe<Scalars['String']>;
|
fundingPayment?: Maybe<Scalars['String']>;
|
||||||
/** Percentage difference between the time-weighted average price of the external and internal data point. */
|
/** Percentage difference between the time-weighted average price of the external and internal data point. */
|
||||||
fundingRate?: Maybe<Scalars['String']>;
|
fundingRate?: Maybe<Scalars['String']>;
|
||||||
/** The index price used for external VWAP calculation */
|
/** Internal composite price used as input to the internal VWAP */
|
||||||
indexPrice: Scalars['String'];
|
internalCompositePrice: Scalars['String'];
|
||||||
/** The methodology used to calculated index price for perps */
|
/** The methodology used to calculated internal composite price for perpetual markets */
|
||||||
indexPriceType: CompositePriceType;
|
internalCompositePriceType: CompositePriceType;
|
||||||
/** Time-weighted average price calculated from data points for this period from the internal data source. */
|
/** Time-weighted average price calculated from data points for this period from the internal data source. */
|
||||||
internalTwap?: Maybe<Scalars['String']>;
|
internalTwap?: Maybe<Scalars['String']>;
|
||||||
/** RFC3339Nano time indicating the next time index price will be calculated for perps where applicable */
|
/** RFC3339Nano time indicating the next time internal composite price will be calculated for perpetual markets, where applicable */
|
||||||
nextIndexPriceCalc: Scalars['String'];
|
nextInternalCompositePriceCalc: Scalars['String'];
|
||||||
/** Funding period sequence number */
|
/** Funding period sequence number */
|
||||||
seqNum: Scalars['Int'];
|
seqNum: Scalars['Int'];
|
||||||
/** Time at which the funding period started */
|
/** Time at which the funding period started */
|
||||||
@ -5507,6 +5503,8 @@ export type ReferralSet = {
|
|||||||
id: Scalars['ID'];
|
id: Scalars['ID'];
|
||||||
/** Party that created the set. */
|
/** Party that created the set. */
|
||||||
referrer: Scalars['ID'];
|
referrer: Scalars['ID'];
|
||||||
|
/** Current number of members in the referral set. */
|
||||||
|
totalMembers: Scalars['Int'];
|
||||||
/** Timestamp as RFC3339Nano when the referral set was updated. */
|
/** Timestamp as RFC3339Nano when the referral set was updated. */
|
||||||
updatedAt: Scalars['Timestamp'];
|
updatedAt: Scalars['Timestamp'];
|
||||||
};
|
};
|
||||||
@ -6333,6 +6331,8 @@ export type Team = {
|
|||||||
teamId: Scalars['ID'];
|
teamId: Scalars['ID'];
|
||||||
/** Link to the team's homepage. */
|
/** Link to the team's homepage. */
|
||||||
teamUrl: Scalars['String'];
|
teamUrl: Scalars['String'];
|
||||||
|
/** Current number of members in the team. */
|
||||||
|
totalMembers: Scalars['Int'];
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Connection type for retrieving cursor-based paginated team data */
|
/** Connection type for retrieving cursor-based paginated team data */
|
||||||
|
@ -3,6 +3,7 @@ import type {
|
|||||||
EntityScope,
|
EntityScope,
|
||||||
GovernanceTransferKind,
|
GovernanceTransferKind,
|
||||||
GovernanceTransferType,
|
GovernanceTransferType,
|
||||||
|
IndividualScope,
|
||||||
PeggedReference,
|
PeggedReference,
|
||||||
ProposalChange,
|
ProposalChange,
|
||||||
TransferStatus,
|
TransferStatus,
|
||||||
@ -700,6 +701,20 @@ export const EntityScopeLabelMapping: { [e in EntityScope]: string } = {
|
|||||||
ENTITY_SCOPE_TEAMS: 'Team',
|
ENTITY_SCOPE_TEAMS: 'Team',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const IndividualScopeMapping: { [e in IndividualScope]: string } = {
|
||||||
|
INDIVIDUAL_SCOPE_ALL: 'All',
|
||||||
|
INDIVIDUAL_SCOPE_IN_TEAM: 'In team',
|
||||||
|
INDIVIDUAL_SCOPE_NOT_IN_TEAM: 'Not in team',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IndividualScopeDescriptionMapping: {
|
||||||
|
[e in IndividualScope]: string;
|
||||||
|
} = {
|
||||||
|
INDIVIDUAL_SCOPE_ALL: 'All parties are eligble',
|
||||||
|
INDIVIDUAL_SCOPE_IN_TEAM: 'Parties in teams are eligible',
|
||||||
|
INDIVIDUAL_SCOPE_NOT_IN_TEAM: 'Only parties not in teams are eligible',
|
||||||
|
};
|
||||||
|
|
||||||
export enum DistributionStrategyMapping {
|
export enum DistributionStrategyMapping {
|
||||||
/** 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 */
|
||||||
DISTRIBUTION_STRATEGY_PRO_RATA = 'Pro rata',
|
DISTRIBUTION_STRATEGY_PRO_RATA = 'Pro rata',
|
||||||
|
@ -8,13 +8,15 @@ export type VegaIconSize = 8 | 10 | 12 | 13 | 14 | 16 | 18 | 20 | 24 | 28 | 32;
|
|||||||
export interface VegaIconProps {
|
export interface VegaIconProps {
|
||||||
name: VegaIconNames;
|
name: VegaIconNames;
|
||||||
size?: VegaIconSize;
|
size?: VegaIconSize;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VegaIcon = ({ size = 16, name }: VegaIconProps) => {
|
export const VegaIcon = ({ size = 16, name, className }: VegaIconProps) => {
|
||||||
const effectiveClassName = classNames(
|
const effectiveClassName = classNames(
|
||||||
'inline-block',
|
'inline-block',
|
||||||
'align-text-bottom',
|
'align-text-bottom',
|
||||||
'fill-current stroke-none'
|
'fill-current stroke-none',
|
||||||
|
className
|
||||||
);
|
);
|
||||||
const Element = VegaIconNameMap[name];
|
const Element = VegaIconNameMap[name];
|
||||||
return (
|
return (
|
||||||
|
@ -3,12 +3,14 @@ import type { ReactNode } from 'react';
|
|||||||
|
|
||||||
export interface SplashProps {
|
export interface SplashProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
|
className?: classNames.Argument;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Splash = ({ children }: SplashProps) => {
|
export const Splash = ({ children, className }: SplashProps) => {
|
||||||
const splashClasses = classNames(
|
const splashClasses = classNames(
|
||||||
'w-full h-full text-xs text-center text-gray-800 dark:text-gray-200',
|
'w-full h-full text-xs text-center text-gray-800 dark:text-gray-200',
|
||||||
'flex items-center justify-center'
|
'flex items-center justify-center',
|
||||||
|
className
|
||||||
);
|
);
|
||||||
return <div className={splashClasses}>{children}</div>;
|
return <div className={splashClasses}>{children}</div>;
|
||||||
};
|
};
|
||||||
|
@ -43,7 +43,7 @@ export const TradingFormGroup = ({
|
|||||||
<label htmlFor={labelFor} className={labelClasses}>
|
<label htmlFor={labelFor} className={labelClasses}>
|
||||||
{label}
|
{label}
|
||||||
{labelDescription && (
|
{labelDescription && (
|
||||||
<div className="font-light mt-1">{labelDescription}</div>
|
<div className="font-light mt-1 text-muted">{labelDescription}</div>
|
||||||
)}
|
)}
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
quantumDecimalPlaces,
|
quantumDecimalPlaces,
|
||||||
toDecimal,
|
toDecimal,
|
||||||
toNumberParts,
|
toNumberParts,
|
||||||
|
formatNumberRounded,
|
||||||
} from './number';
|
} from './number';
|
||||||
|
|
||||||
describe('number utils', () => {
|
describe('number utils', () => {
|
||||||
@ -254,3 +255,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'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -191,7 +191,10 @@ export const isNumeric = (
|
|||||||
* Format a number greater than 1 million with m for million, b for billion
|
* Format a number greater than 1 million with m for million, b for billion
|
||||||
* and t for trillion
|
* and t for trillion
|
||||||
*/
|
*/
|
||||||
export const formatNumberRounded = (num: BigNumber) => {
|
export const formatNumberRounded = (
|
||||||
|
num: BigNumber,
|
||||||
|
limit: '1e12' | '1e9' | '1e6' | '1e3' = '1e6'
|
||||||
|
) => {
|
||||||
let value = '';
|
let value = '';
|
||||||
|
|
||||||
const format = (divisor: string) => {
|
const format = (divisor: string) => {
|
||||||
@ -201,15 +204,29 @@ export const formatNumberRounded = (num: BigNumber) => {
|
|||||||
|
|
||||||
if (num.isGreaterThan(new BigNumber('1e14'))) {
|
if (num.isGreaterThan(new BigNumber('1e14'))) {
|
||||||
value = '>100t';
|
value = '>100t';
|
||||||
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e12'))) {
|
} else if (
|
||||||
|
num.isGreaterThanOrEqualTo(limit) &&
|
||||||
|
num.isGreaterThanOrEqualTo(new BigNumber('1e12'))
|
||||||
|
) {
|
||||||
// Trillion
|
// Trillion
|
||||||
value = `${format('1e12')}t`;
|
value = `${format('1e12')}t`;
|
||||||
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e9'))) {
|
} else if (
|
||||||
|
num.isGreaterThanOrEqualTo(limit) &&
|
||||||
|
num.isGreaterThanOrEqualTo(new BigNumber('1e9'))
|
||||||
|
) {
|
||||||
// Billion
|
// Billion
|
||||||
value = `${format('1e9')}b`;
|
value = `${format('1e9')}b`;
|
||||||
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e6'))) {
|
} else if (
|
||||||
|
num.isGreaterThanOrEqualTo(limit) &&
|
||||||
|
num.isGreaterThanOrEqualTo(new BigNumber('1e6'))
|
||||||
|
) {
|
||||||
// Million
|
// Million
|
||||||
value = `${format('1e6')}m`;
|
value = `${format('1e6')}m`;
|
||||||
|
} else if (
|
||||||
|
num.isGreaterThanOrEqualTo(limit) &&
|
||||||
|
num.isGreaterThanOrEqualTo(new BigNumber('1e3'))
|
||||||
|
) {
|
||||||
|
value = `${format('1e3')}k`;
|
||||||
} else {
|
} else {
|
||||||
value = formatNumber(num);
|
value = formatNumber(num);
|
||||||
}
|
}
|
||||||
|
@ -29,11 +29,14 @@ export const useEthereumAddress = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const VEGA_ID_REGEX = /^[A-Fa-f0-9]{64}$/i;
|
export const VEGA_ID_REGEX = /^[A-Fa-f0-9]{64}$/i;
|
||||||
|
export const isValidVegaPublicKey = (value: string) => {
|
||||||
|
return VEGA_ID_REGEX.test(value);
|
||||||
|
};
|
||||||
export const useVegaPublicKey = () => {
|
export const useVegaPublicKey = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
return useCallback(
|
return useCallback(
|
||||||
(value: string) => {
|
(value: string) => {
|
||||||
if (!VEGA_ID_REGEX.test(value)) {
|
if (!isValidVegaPublicKey(value)) {
|
||||||
return t('Invalid Vega key');
|
return t('Invalid Vega key');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@ -91,3 +94,21 @@ export const useValidateJson = () => {
|
|||||||
[t]
|
[t]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const URL_REGEX =
|
||||||
|
/^(https?:\/\/)?([a-zA-Z0-9.-]+(\.[a-zA-Z]{2,})+)(:[0-9]{1,5})?(\/[^\s]*)?$/;
|
||||||
|
const isValidUrl = (value: string) => {
|
||||||
|
return URL_REGEX.test(value);
|
||||||
|
};
|
||||||
|
export const useValidateUrl = () => {
|
||||||
|
const t = useT();
|
||||||
|
return useCallback(
|
||||||
|
(value: string) => {
|
||||||
|
if (!isValidUrl(value)) {
|
||||||
|
return t('Invalid URL');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
[t]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
17
libs/wallet/src/SimpleTransaction.graphql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
fragment SimpleTransactionFields on TransactionResult {
|
||||||
|
partyId
|
||||||
|
hash
|
||||||
|
status
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription SimpleTransaction($partyId: ID!) {
|
||||||
|
busEvents(partyId: $partyId, batchSize: 0, types: [TransactionResult]) {
|
||||||
|
type
|
||||||
|
event {
|
||||||
|
... on TransactionResult {
|
||||||
|
...SimpleTransactionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
libs/wallet/src/__generated__/SimpleTransaction.ts
generated
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import * as Types from '@vegaprotocol/types';
|
||||||
|
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import * as Apollo from '@apollo/client';
|
||||||
|
const defaultOptions = {} as const;
|
||||||
|
export type SimpleTransactionFieldsFragment = { __typename?: 'TransactionResult', partyId: string, hash: string, status: boolean, error?: string | null };
|
||||||
|
|
||||||
|
export type SimpleTransactionSubscriptionVariables = Types.Exact<{
|
||||||
|
partyId: Types.Scalars['ID'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type SimpleTransactionSubscription = { __typename?: 'Subscription', busEvents?: Array<{ __typename?: 'BusEvent', type: Types.BusEventType, event: { __typename?: 'Deposit' } | { __typename?: 'TimeUpdate' } | { __typename?: 'TransactionResult', partyId: string, hash: string, status: boolean, error?: string | null } | { __typename?: 'Withdrawal' } }> | null };
|
||||||
|
|
||||||
|
export const SimpleTransactionFieldsFragmentDoc = gql`
|
||||||
|
fragment SimpleTransactionFields on TransactionResult {
|
||||||
|
partyId
|
||||||
|
hash
|
||||||
|
status
|
||||||
|
error
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export const SimpleTransactionDocument = gql`
|
||||||
|
subscription SimpleTransaction($partyId: ID!) {
|
||||||
|
busEvents(partyId: $partyId, batchSize: 0, types: [TransactionResult]) {
|
||||||
|
type
|
||||||
|
event {
|
||||||
|
... on TransactionResult {
|
||||||
|
...SimpleTransactionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${SimpleTransactionFieldsFragmentDoc}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useSimpleTransactionSubscription__
|
||||||
|
*
|
||||||
|
* To run a query within a React component, call `useSimpleTransactionSubscription` and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useSimpleTransactionSubscription` 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 subscription, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const { data, loading, error } = useSimpleTransactionSubscription({
|
||||||
|
* variables: {
|
||||||
|
* partyId: // value for 'partyId'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useSimpleTransactionSubscription(baseOptions: Apollo.SubscriptionHookOptions<SimpleTransactionSubscription, SimpleTransactionSubscriptionVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useSubscription<SimpleTransactionSubscription, SimpleTransactionSubscriptionVariables>(SimpleTransactionDocument, options);
|
||||||
|
}
|
||||||
|
export type SimpleTransactionSubscriptionHookResult = ReturnType<typeof useSimpleTransactionSubscription>;
|
||||||
|
export type SimpleTransactionSubscriptionResult = Apollo.SubscriptionResult<SimpleTransactionSubscription>;
|
@ -439,6 +439,12 @@ export type ApplyReferralCode = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type JoinTeam = {
|
||||||
|
joinTeam: {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type CreateReferralSet = {
|
export type CreateReferralSet = {
|
||||||
createReferralSet: {
|
createReferralSet: {
|
||||||
isTeam: boolean;
|
isTeam: boolean;
|
||||||
@ -447,6 +453,21 @@ export type CreateReferralSet = {
|
|||||||
teamUrl?: string;
|
teamUrl?: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
closed: boolean;
|
closed: boolean;
|
||||||
|
allowList: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateReferralSet = {
|
||||||
|
updateReferralSet: {
|
||||||
|
id: string;
|
||||||
|
isTeam: boolean;
|
||||||
|
team?: {
|
||||||
|
name: string;
|
||||||
|
teamUrl?: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
closed: boolean;
|
||||||
|
allowList: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -481,7 +502,9 @@ export type Transaction =
|
|||||||
| TransferBody
|
| TransferBody
|
||||||
| LiquidityProvisionSubmission
|
| LiquidityProvisionSubmission
|
||||||
| ApplyReferralCode
|
| ApplyReferralCode
|
||||||
| CreateReferralSet;
|
| JoinTeam
|
||||||
|
| CreateReferralSet
|
||||||
|
| UpdateReferralSet;
|
||||||
|
|
||||||
export const isMarginModeUpdateTransaction = (
|
export const isMarginModeUpdateTransaction = (
|
||||||
transaction: Transaction
|
transaction: Transaction
|
||||||
|
@ -8,3 +8,9 @@ export * from './connect-dialog';
|
|||||||
export * from './utils';
|
export * from './utils';
|
||||||
export * from './storage';
|
export * from './storage';
|
||||||
export * from './use-chain-id';
|
export * from './use-chain-id';
|
||||||
|
export {
|
||||||
|
useSimpleTransaction,
|
||||||
|
type Status,
|
||||||
|
type Result,
|
||||||
|
type Options,
|
||||||
|
} from './use-simple-transaction';
|
||||||
|
118
libs/wallet/src/use-simple-transaction.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { useVegaWallet } from './use-vega-wallet';
|
||||||
|
import { type Transaction } from './connectors';
|
||||||
|
import {
|
||||||
|
useSimpleTransactionSubscription,
|
||||||
|
type SimpleTransactionFieldsFragment,
|
||||||
|
} from './__generated__/SimpleTransaction';
|
||||||
|
import { useT } from './use-t';
|
||||||
|
import { determineId } from './utils';
|
||||||
|
|
||||||
|
export type Status = 'idle' | 'requested' | 'pending' | 'confirmed';
|
||||||
|
|
||||||
|
export type Result = {
|
||||||
|
txHash: string;
|
||||||
|
signature: string;
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Options = {
|
||||||
|
onSuccess?: (result: Result) => void;
|
||||||
|
onError?: (msg: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useSimpleTransaction = (opts?: Options) => {
|
||||||
|
const t = useT();
|
||||||
|
const { pubKey, isReadOnly, sendTx } = useVegaWallet();
|
||||||
|
|
||||||
|
const [status, setStatus] = useState<Status>('idle');
|
||||||
|
const [result, setResult] = useState<Result>();
|
||||||
|
const [error, setError] = useState<string>();
|
||||||
|
|
||||||
|
const send = async (tx: Transaction) => {
|
||||||
|
if (!pubKey) {
|
||||||
|
throw new Error('no pubKey');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isReadOnly) {
|
||||||
|
throw new Error('cant submit in read only mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('requested');
|
||||||
|
setError(undefined);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await sendTx(pubKey, tx);
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
throw new Error(t('Transaction could not be sent'));
|
||||||
|
}
|
||||||
|
|
||||||
|
setStatus('pending');
|
||||||
|
setResult({
|
||||||
|
txHash: res?.transactionHash.toLowerCase(),
|
||||||
|
signature: res.signature,
|
||||||
|
id: determineId(res.signature),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
if (err.message.includes('user rejected')) {
|
||||||
|
setStatus('idle');
|
||||||
|
} else {
|
||||||
|
setError(err.message);
|
||||||
|
setStatus('idle');
|
||||||
|
opts?.onError?.(err.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const msg = t('Wallet rejected transaction');
|
||||||
|
setError(msg);
|
||||||
|
setStatus('idle');
|
||||||
|
opts?.onError?.(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useSimpleTransactionSubscription({
|
||||||
|
variables: { partyId: pubKey || '' },
|
||||||
|
skip: !pubKey || !result,
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
|
onData: ({ data }) => {
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('simple transaction query started before result');
|
||||||
|
}
|
||||||
|
|
||||||
|
const e = data.data?.busEvents?.find((event) => {
|
||||||
|
if (
|
||||||
|
event.event.__typename === 'TransactionResult' &&
|
||||||
|
event.event.hash.toLowerCase() === result?.txHash
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!e) return;
|
||||||
|
|
||||||
|
// Force type narrowing
|
||||||
|
const event = e.event as SimpleTransactionFieldsFragment;
|
||||||
|
|
||||||
|
if (event.status && !event.error) {
|
||||||
|
setStatus('confirmed');
|
||||||
|
opts?.onSuccess?.(result);
|
||||||
|
} else {
|
||||||
|
const msg = event?.error || t('Transaction was not successful');
|
||||||
|
setError(msg);
|
||||||
|
setStatus('idle');
|
||||||
|
opts?.onError?.(msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
status,
|
||||||
|
send,
|
||||||
|
};
|
||||||
|
};
|