Compare commits

...
Author SHA1 Message Date
Matthew Russell 02597faeba feat: use new limit param for number formatting 2024-01-13 18:45:49 -05:00
Matthew Russell b216312a3e feat: update formatNumberRounded to accept a limit 2024-01-13 18:45:49 -05:00
Matthew Russell ab04c68e1b feat: add formatting for stat values 2024-01-13 18:45:49 -05:00
Matthew Russell 6049cb1dc7 chore: use built in team creation methods 2024-01-13 18:45:49 -05:00
Matthew Russell 9a04045d32 feat: responsive adjustments 2024-01-13 18:45:49 -05:00
Matthew Russell fa99dfde79 fix: missing i18n 2024-01-13 18:45:49 -05:00
Matthew Russell 8b826bce45 fix: use correct bg color for gradient 2024-01-13 18:45:49 -05:00
Matthew Russell 0f5d28c32a fix: incorrect member count 2024-01-13 18:45:49 -05:00
Matthew Russell 204adb4910 feat: add i18n 2024-01-13 18:45:49 -05:00
Matthew Russell 38edc24e02 feat: add favorite game and last 5 logic 2024-01-13 18:45:49 -05:00
Matthew Russell c476706cdf feat: add games list data 2024-01-13 18:45:48 -05:00
Matthew Russell f523366a64 feat: wire up games played 2024-01-13 18:44:55 -05:00
Matthew Russell 07285973d1 feat: wire up data, remove team pnl, improve key creation 2024-01-13 18:44:55 -05:00
Matthew Russell 72611fbb31 feat: add members table 2024-01-13 18:44:55 -05:00
Matthew Russell dc99dd592d feat: add members table 2024-01-13 18:44:55 -05:00
Matthew Russell be5d98c011 test: add team creation in sim 2024-01-13 18:44:55 -05:00
Matthew Russell 88e45271b1 feat: wire up avatar 2024-01-13 18:44:55 -05:00
Matthew Russell bec78a4c1a feat: add teams query 2024-01-13 18:44:54 -05:00
Matthew Russell b89b901f71 feat: richer cell content 2024-01-13 18:43:45 -05:00
Matthew Russell a5eb460d98 feat: add trading table component 2024-01-13 18:43:45 -05:00
Matthew Russell da43624478 feat: add favorite game and fix bg cover 2024-01-13 18:43:45 -05:00
Matthew Russell 6e6207e7f9 feat: update path to be nested under competitions 2024-01-13 18:43:45 -05:00
Matthew Russell 3957c2d911 feat: add cover bg and tooltips for stats 2024-01-13 18:43:45 -05:00
Matthew Russell 11010525df feat: add joined state 2024-01-13 18:43:45 -05:00
Matthew Russell c1d3d83045 feat: add basic table 2024-01-13 18:43:45 -05:00
Matthew Russell 9c7adb812c feat: add route and initial page layout 2024-01-13 18:43:45 -05:00
18 changed files with 849 additions and 23 deletions
@@ -0,0 +1,89 @@
fragment TeamFields on Team {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
}
fragment TeamStatsFields on TeamStatistics {
teamId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
quantumRewards {
epoch
total_quantum_rewards
}
gamesPlayed
}
fragment TeamRefereeFields on TeamReferee {
teamId
referee
joinedAt
joinedAtEpoch
}
fragment TeamEntity on TeamGameEntity {
rank
volume
rewardMetric
rewardEarned
totalRewardsEarned
team {
teamId
}
}
fragment TeamGameFields on Game {
id
epoch
numberOfParticipants
entities {
... on TeamGameEntity {
...TeamEntity
}
}
}
query Team($teamId: ID!, $partyId: ID) {
teams(teamId: $teamId) {
edges {
node {
...TeamFields
}
}
}
partyTeams: teams(partyId: $partyId) {
edges {
node {
...TeamFields
}
}
}
teamsStatistics(teamId: $teamId) {
edges {
node {
...TeamStatsFields
}
}
}
teamReferees(teamId: $teamId) {
edges {
node {
...TeamRefereeFields
}
}
}
games(entityScope: ENTITY_SCOPE_TEAMS) {
edges {
node {
...TeamGameFields
}
}
}
}
+151
View File
@@ -0,0 +1,151 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type TeamFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean };
export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> };
export type TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
export type TeamEntityFragment = { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } };
export type TeamGameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> };
export type TeamQueryVariables = Types.Exact<{
teamId: Types.Scalars['ID'];
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
export type TeamQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null, partyTeams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> } }> } | null, teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null, games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> } } | null> | null } };
export const TeamFieldsFragmentDoc = gql`
fragment TeamFields on Team {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
}
`;
export const TeamStatsFieldsFragmentDoc = gql`
fragment TeamStatsFields on TeamStatistics {
teamId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
quantumRewards {
epoch
total_quantum_rewards
}
gamesPlayed
}
`;
export const TeamRefereeFieldsFragmentDoc = gql`
fragment TeamRefereeFields on TeamReferee {
teamId
referee
joinedAt
joinedAtEpoch
}
`;
export const TeamEntityFragmentDoc = gql`
fragment TeamEntity on TeamGameEntity {
rank
volume
rewardMetric
rewardEarned
totalRewardsEarned
team {
teamId
}
}
`;
export const TeamGameFieldsFragmentDoc = gql`
fragment TeamGameFields on Game {
id
epoch
numberOfParticipants
entities {
... on TeamGameEntity {
...TeamEntity
}
}
}
${TeamEntityFragmentDoc}`;
export const TeamDocument = gql`
query Team($teamId: ID!, $partyId: ID) {
teams(teamId: $teamId) {
edges {
node {
...TeamFields
}
}
}
partyTeams: teams(partyId: $partyId) {
edges {
node {
...TeamFields
}
}
}
teamsStatistics(teamId: $teamId) {
edges {
node {
...TeamStatsFields
}
}
}
teamReferees(teamId: $teamId) {
edges {
node {
...TeamRefereeFields
}
}
}
games(entityScope: ENTITY_SCOPE_TEAMS) {
edges {
node {
...TeamGameFields
}
}
}
}
${TeamFieldsFragmentDoc}
${TeamStatsFieldsFragmentDoc}
${TeamRefereeFieldsFragmentDoc}
${TeamGameFieldsFragmentDoc}`;
/**
* __useTeamQuery__
*
* To run a query within a React component, call `useTeamQuery` and pass it any options that fit your needs.
* When your component renders, `useTeamQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useTeamQuery({
* variables: {
* teamId: // value for 'teamId'
* partyId: // value for 'partyId'
* },
* });
*/
export function useTeamQuery(baseOptions: Apollo.QueryHookOptions<TeamQuery, TeamQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TeamQuery, TeamQueryVariables>(TeamDocument, options);
}
export function useTeamLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamQuery, TeamQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TeamQuery, TeamQueryVariables>(TeamDocument, options);
}
export type TeamQueryHookResult = ReturnType<typeof useTeamQuery>;
export type TeamLazyQueryHookResult = ReturnType<typeof useTeamLazyQuery>;
export type TeamQueryResult = Apollo.QueryResult<TeamQuery, TeamQueryVariables>;
+1
View File
@@ -0,0 +1 @@
export { Team } from './team';
+395
View File
@@ -0,0 +1,395 @@
import { useState, type ReactNode, type ButtonHTMLAttributes } from 'react';
import { Link, useParams } from 'react-router-dom';
import orderBy from 'lodash/orderBy';
import countBy from 'lodash/countBy';
import {
TradingButton as Button,
Intent,
Pill,
VegaIcon,
VegaIconNames,
Tooltip,
Splash,
truncateMiddle,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
import { Table } from '../../components/table';
import { formatNumberRounded, getDateTimeFormat } from '@vegaprotocol/utils';
import {
useTeam,
type Team as TeamType,
type TeamStats,
type Member,
type TeamGame,
} from './use-team';
import { DApp, EXPLORER_PARTIES, useLinks } from '@vegaprotocol/environment';
import BigNumber from 'bignumber.js';
export const Team = () => {
const t = useT();
const { teamId } = useParams<{ teamId: string }>();
const { team, stats, partyInTeam, members, games } = useTeam(teamId);
if (!team) {
return (
<Splash>
<p>{t('Page not found')}</p>
</Splash>
);
}
return (
<TeamPage
team={team}
stats={stats}
partyInTeam={partyInTeam}
members={members}
games={games}
/>
);
};
export const TeamPage = ({
team,
stats,
partyInTeam,
members,
games,
}: {
team: TeamType;
stats?: TeamStats;
partyInTeam: boolean;
members?: Member[];
games?: TeamGame[];
}) => {
const t = useT();
const [showGames, setShowGames] = useState(true);
return (
<div className="relative h-full overflow-y-auto">
<div className="absolute top-0 left-0 w-full h-[40%] -z-10 bg-[40%_0px] bg-cover bg-no-repeat bg-local bg-[url(/cover.png)]">
<div className="absolute top-o left-0 w-full h-full bg-gradient-to-t from-white dark:from-vega-cdark-900 to-transparent from-20% to-60%" />
</div>
<div className="flex flex-col gap-4 lg:gap-6 container p-4 mx-auto">
<header className="flex gap-3 lg:gap-4 pt-5 lg:pt-10">
<TeamAvatar imgUrl={team.avatarUrl} />
<div className="flex flex-col items-start gap-1 lg:gap-3">
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
{team.name}
</h1>
<JoinButton joined={partyInTeam} />
</div>
</header>
<StatSection>
<StatList>
<Stat value={members ? members.length : 0} label={t('Members')} />
<Stat
value={stats ? stats.totalGamesPlayed : 0}
label={t('Total games')}
tooltip={t('Total number of games this team has participated in')}
/>
<StatSectionSeparator />
<Stat
value={
stats
? formatNumberRounded(
new BigNumber(stats.totalQuantumVolume),
'1e3'
)
: 0
}
label={t('Total volume')}
/>
<Stat
value={
stats
? formatNumberRounded(
new BigNumber(stats.totalQuantumRewards),
'1e3'
)
: 0
}
label={t('Rewards paid')}
tooltip={'Total amount of rewards paid out to this team in qUSD'}
/>
</StatList>
</StatSection>
{games && games.length ? (
<StatSection>
<FavoriteGame games={games} />
<StatSectionSeparator />
<LatestResults games={games} />
</StatSection>
) : null}
<section>
<div className="flex gap-4 lg:gap-8 mb-4 border-b border-default">
<ToggleButton active={showGames} onClick={() => setShowGames(true)}>
{t('Games ({{count}})', { count: games ? games.length : 0 })}
</ToggleButton>
<ToggleButton
active={!showGames}
onClick={() => setShowGames(false)}
>
{t('Members ({{count}})', {
count: members ? members.length : 0,
})}
</ToggleButton>
</div>
{showGames ? <Games games={games} /> : <Members members={members} />}
</section>
</div>
</div>
);
};
const Games = ({ games }: { games?: TeamGame[] }) => {
const t = useT();
if (!games?.length) {
return <p>{t('No games')}</p>;
}
return (
<Table
columns={[
{ name: 'rank', displayName: t('Rank') },
{
name: 'epoch',
displayName: t('Epoch'),
headerClassName: 'hidden md:block',
className: 'hidden md:block',
},
{ name: 'type', displayName: t('Type') },
{ name: 'amount', displayName: t('Amount earned') },
{
name: 'teams',
displayName: t('No. of participating teams'),
headerClassName: 'hidden md:block',
className: 'hidden md:block',
},
{ name: 'status', displayName: t('Status') },
]}
data={games.map((game) => ({
rank: game.team.rank,
epoch: game.epoch,
type: game.team.rewardMetric,
amount: game.team.totalRewardsEarned,
teams: game.numberOfParticipants,
}))}
noCollapse={true}
/>
);
};
const TeamAvatar = ({ imgUrl }: { imgUrl: string }) => {
// TODO: add fallback avatars
return (
// eslint-disable-next-line @next/next/no-img-element
<img
src={imgUrl}
alt="Team avatar"
className="rounded-full w-20 h-20 lg:w-[112px] lg:h-[112px] bg-vega-clight-700 dark:bg-vega-cdark-700 shrink-0"
/>
);
};
const Members = ({ members }: { members?: Member[] }) => {
const t = useT();
if (!members?.length) {
return <p>{t('No members')}</p>;
}
const data = orderBy(
members.map((m) => ({
referee: <RefereeCell pubkey={m.referee} />,
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
joinedAtEpoch: Number(m.joinedAtEpoch),
explorerLink: <RefereeLink pubkey={m.referee} />,
})),
'joinedAtEpoch',
'desc'
);
return (
<Table
columns={[
{ name: 'referee', displayName: t('Referee') },
{
name: 'joinedAt',
displayName: t('Joined at'),
},
{
name: 'joinedAtEpoch',
displayName: t('Joined epoch'),
headerClassName: 'text-right',
className: 'text-right',
},
{
name: 'explorerLink',
displayName: '',
headerClassName: 'hidden md:block',
className: 'hidden md:block text-right',
},
]}
data={data}
noCollapse={true}
/>
);
};
const RefereeCell = ({ pubkey }: { pubkey: string }) => {
return <span title={pubkey}>{truncateMiddle(pubkey)}</span>;
};
const RefereeLink = ({ pubkey }: { pubkey: string }) => {
const t = useT();
const linkCreator = useLinks(DApp.Explorer);
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
return (
<Link to={link} className="underline underline-offset-4">
{t('View on explorer')}
</Link>
);
};
const JoinButton = ({ joined }: { joined: boolean }) => {
const t = useT();
if (joined) {
return (
<Button intent={Intent.None} disabled={true}>
<span className="flex items-center gap-2">
{t('Joined')}{' '}
<span className="text-vega-green-600 dark:text-vega-green">
<VegaIcon name={VegaIconNames.TICK} />
</span>
</span>
</Button>
);
}
return <Button intent={Intent.Primary}>{t('Join this team')}</Button>;
};
const LatestResults = ({ games }: { games: TeamGame[] }) => {
const t = useT();
const latestGames = games.slice(0, 5);
return (
<dl>
<dt className="text-muted text-sm">
{t('Last {{count}} game results', { count: latestGames.length })}
</dt>
<dd className="flex gap-1">
{latestGames.map((game) => {
return (
<Pill key={game.id} className="text-sm">
{t('place', { count: game.team.rank, ordinal: true })}
</Pill>
);
})}
</dd>
</dl>
);
};
const FavoriteGame = ({ games }: { games: TeamGame[] }) => {
const t = useT();
const rewardMetrics = games.map((game) => game.team.rewardMetric);
const count = countBy(rewardMetrics);
let favoriteMetric = '';
let mostOccurances = 0;
for (const key in count) {
if (count[key] > mostOccurances) {
favoriteMetric = key;
mostOccurances = count[key];
}
}
if (!favoriteMetric) return null;
return (
<dl>
<dt className="text-muted text-sm">{t('Favorite game')}</dt>
<dd>
<Pill className="flex-inline items-center gap-2 bg-transparent text-sm">
<VegaIcon
name={VegaIconNames.STAR}
className="text-vega-yellow-400"
/>{' '}
{favoriteMetric}
</Pill>
</dd>
</dl>
);
};
const ToggleButton = ({
active,
...props
}: ButtonHTMLAttributes<HTMLButtonElement> & { active: boolean }) => {
return (
<button
{...props}
className={classNames('relative top-px uppercase border-b-2 py-4', {
'text-muted border-transparent': !active,
'border-vega-yellow': active,
})}
/>
);
};
const StatSection = ({ children }: { children: ReactNode }) => {
return (
<section className="flex flex-col lg:flex-row gap-2 lg:gap-8">
{children}
</section>
);
};
const StatSectionSeparator = () => {
return <div className="hidden md:block border-r border-default" />;
};
const StatList = ({ children }: { children: ReactNode }) => {
return (
<dl className="grid grid-cols-[min-content_min-content] md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
{children}
</dl>
);
};
const Stat = ({
value,
label,
tooltip,
}: {
value: ReactNode;
label: ReactNode;
tooltip?: string;
}) => {
return (
<div>
<dd className="text-3xl lg:text-4xl">{value}</dd>
<dt className="text-sm text-muted">
{tooltip ? (
<Tooltip description={tooltip} underline={false}>
<span className="flex items-center gap-2">
{label}
<VegaIcon name={VegaIconNames.INFO} size={12} />
</span>
</Tooltip>
) : (
label
)}
</dt>
</div>
);
};
@@ -0,0 +1,64 @@
import compact from 'lodash/compact';
import orderBy from 'lodash/orderBy';
import {
useTeamQuery,
type TeamFieldsFragment,
type TeamStatsFieldsFragment,
type TeamRefereeFieldsFragment,
type TeamEntityFragment,
} from './__generated__/Team';
import { useVegaWallet } from '@vegaprotocol/wallet';
export type Team = TeamFieldsFragment;
export type TeamStats = TeamStatsFieldsFragment;
export type Member = TeamRefereeFieldsFragment;
export type TeamEntity = TeamEntityFragment;
export type TeamGame = ReturnType<typeof useTeam>['games'][number];
export const useTeam = (teamId?: string) => {
const { pubKey } = useVegaWallet();
const { data, loading, error } = useTeamQuery({
variables: { teamId: teamId || '', partyId: pubKey },
skip: !teamId,
});
const teamEdge = data?.teams?.edges.find((e) => e.node.teamId === teamId);
const partyTeamEdge = data?.partyTeams?.edges[0];
const teamStatsEdge = data?.teamsStatistics?.edges.find(
(e) => e.node.teamId === teamId
);
const members = data?.teamReferees?.edges
.filter((e) => e.node.teamId === teamId)
.map((e) => e.node);
// Find games where the current team participated in
const gamesWithTeam = compact(data?.games.edges).map((edge) => {
const team = edge.node.entities.find((e) => {
if (e.__typename !== 'TeamGameEntity') return false;
if (e.team.teamId !== teamId) return false;
return true;
});
if (!team) return null;
return {
id: edge.node.id,
epoch: edge.node.epoch,
numberOfParticipants: edge.node.numberOfParticipants,
team: team as TeamEntity, // TS can't infer that all the game entities are teams
};
});
const games = orderBy(compact(gamesWithTeam), 'epoch', 'desc');
return {
data,
loading,
error,
stats: teamStatsEdge?.node,
team: teamEdge?.node,
members,
games,
partyInTeam: Boolean(partyTeamEdge),
};
};
-1
View File
@@ -1 +0,0 @@
export { Teams } from './teams';
@@ -1,7 +0,0 @@
export const Teams = () => {
return (
<div>
<h1>Teams</h1>
</div>
);
};
+4 -2
View File
@@ -11,6 +11,7 @@ type TableColumnDefinition = {
name: string;
tooltip?: string;
className?: string;
headerClassName?: string;
testId?: string;
};
@@ -41,13 +42,14 @@ export const Table = forwardRef<
const header = (
<thead className={classNames({ 'max-md:hidden': !noCollapse })}>
<tr>
{columns.map(({ displayName, name, tooltip }) => (
{columns.map(({ displayName, name, tooltip, headerClassName }) => (
<th
key={name}
col-id={name}
className={classNames(
'px-5 py-3 text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
INNER_BORDER_STYLE
INNER_BORDER_STYLE,
headerClassName
)}
>
<span className="flex flex-row items-center gap-2">
+1 -1
View File
@@ -1,3 +1,3 @@
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
VEGA_VERSION=v0.73.10
LOCAL_SERVER=false
LOCAL_SERVER=true
@@ -0,0 +1,58 @@
import pytest
from playwright.sync_api import expect, Page
from vega_sim.null_service import VegaServiceNull
from actions.utils import next_epoch
from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_teams(vega: VegaServiceNull, page: Page):
# submit_transaction will fail with spam statistics error if block height is 0
# so go forward at least one block
vega.wait_fn(1)
for key in [PARTY_A, PARTY_B, PARTY_C, PARTY_D]:
vega.wallet.create_key(key.name)
team_name = create_team(vega)
next_epoch(vega)
# would be better to derive the team id from the signature if possible but
# vega.submit_transaction does not return anything
teams = vega.list_teams()
# list_teams actually returns a dictionary {"team_id": Team}
team_id = list(teams.keys())[0]
vega.apply_referral_code(PARTY_B.name, team_id)
# go to next epoch so we can check joinedAt and joinedAtEpoch appropriately
next_epoch(vega)
vega.apply_referral_code(PARTY_C.name, team_id)
next_epoch(vega)
vega.apply_referral_code(PARTY_D.name, team_id)
vega.wait_fn(1)
vega.forward("10s")
vega.wait_for_total_catchup()
page.goto(f"/#/competitions/team/{team_id}")
expect(page.get_by_role('heading', level=1)).to_have_text(team_name)
expect(page.get_by_text('Members (3)')).to_be_visible()
def create_team(vega: VegaServiceNull):
team_name = "Foobar"
vega.create_referral_set(
key_name=PARTY_A.name,
name=team_name,
team_url="https://vega.xyz",
avatar_url="http://placekitten.com/200/200",
closed=False
)
return team_name
+2 -2
View File
@@ -16,7 +16,7 @@ export const Routes = {
REFERRALS: '/referrals',
REFERRALS_APPLY_CODE: '/referrals/apply-code',
REFERRALS_CREATE_CODE: '/referrals/create-code',
TEAMS: '/teams',
TEAM: '/competitions/team/:teamId',
FEES: '/fees',
REWARDS: '/rewards',
} as const;
@@ -41,7 +41,7 @@ export const Links: ConsoleLinks = {
REFERRALS: () => Routes.REFERRALS,
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
TEAMS: () => Routes.TEAMS,
TEAM: (teamId: string) => trimEnd(Routes.TEAM.replace(':teamId', teamId)),
FEES: () => Routes.FEES,
REWARDS: () => Routes.REWARDS,
};
+3 -3
View File
@@ -14,7 +14,7 @@ import { Withdraw } from '../client-pages/withdraw';
import { Transfer } from '../client-pages/transfer';
import { Fees } from '../client-pages/fees';
import { Rewards } from '../client-pages/rewards';
import { Teams } from '../client-pages/teams';
import { Team } from '../client-pages/team';
import { Routes as AppRoutes } from '../lib/links';
import { LayoutWithSky } from '../client-pages/referrals/layout';
import { Referrals } from '../client-pages/referrals/referrals';
@@ -95,8 +95,8 @@ export const useRouterConfig = (): RouteObject[] => {
: undefined,
featureFlags.TEAM_COMPETITION
? {
path: AppRoutes.TEAMS,
element: <Teams />,
path: AppRoutes.TEAM,
element: <Team />,
}
: undefined,
{
Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

+1
View File
@@ -161,6 +161,7 @@ export const useProtocolUpgradeProposalLink = () => {
export const EXPLORER_TX = '/txs/:hash';
export const EXPLORER_ORACLE = '/oracles/:id';
export const EXPLORER_MARKET = '/markets/:id';
export const EXPLORER_PARTIES = '/parties/:id';
// Etherscan pages
export const ETHERSCAN_ADDRESS = '/address/:hash';
+27 -1
View File
@@ -14,6 +14,7 @@
"About the referral program": "About the referral program",
"Active": "Active",
"All": "All",
"Amount earned": "Amount earned",
"An unknown error occurred.": "An unknown error occurred.",
"Anonymous": "Anonymous",
"Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction": "Anyone with the referral link can apply it to their key(s) of choice via an on chain transaction",
@@ -79,6 +80,7 @@
"Earned by me": "Earned by me",
"Enactment date reached and usual auction exit checks pass": "Enactment date reached and usual auction exit checks pass",
"Environment not configured": "Environment not configured",
"Epoch": "Epoch",
"epochs in referral set": "epochs in referral set",
"Epochs in set": "Epochs in set",
"Epochs to next tier": "Epochs to next tier",
@@ -87,6 +89,7 @@
"Experiment for free with virtual assets on <0>Fairground Testnet</0>": "Experiment for free with virtual assets on <0>Fairground Testnet</0>",
"Expiry": "Expiry",
"Explore": "Explore",
"Favorite game": "Favorite game",
"Fees": "Fees",
"Fees paid": "Fees paid",
"Fees work like a CEX with no per-transaction gas for orders": "Fees work like a CEX with no per-transaction gas for orders",
@@ -103,6 +106,7 @@
"Funding Rate": "Funding Rate",
"Funding rate": "Funding rate",
"Futures": "Futures",
"Games ({{count}})": "Games ({{count}})",
"Generate a referral code to share with your friends and start earning commission.": "Generate a referral code to share with your friends and start earning commission.",
"Generate code": "Generate code",
"Get started": "Get started",
@@ -132,6 +136,11 @@
"INTERVAL_I6H": "6H",
"INTERVAL_I1D": "1D",
"Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.": "Invite friends and earn rewards from the trading fees they pay. Stake those rewards to earn multipliers on future rewards.",
"Join this team": "Join this team",
"Joined": "Joined",
"Joined at": "Joined at",
"Joined epoch": "Joined epoch",
"Last {{count}} game results": "Last {{count}} game results",
"Learn about providing liquidity": "Learn about providing liquidity",
"Learn more": "Learn more",
"Ledger entries": "Ledger entries",
@@ -150,6 +159,8 @@
"Market specification": "Market specification",
"Market triggers cancellation or governance vote has passed to cancel": "Market triggers cancellation or governance vote has passed to cancel",
"Markets": "Markets",
"Members": "Members",
"Members ({{count}})": "Members ({{count}})",
"Menu": "Menu",
"Metamask Snap <0>quick start</0>": "Metamask Snap <0>quick start</0>",
"Min. epochs": "Min. epochs",
@@ -169,10 +180,12 @@
"No deposits": "No deposits",
"No funding history data": "No funding history data",
"No future markets.": "No future markets.",
"No games": "No games",
"No ledger entries to export": "No ledger entries to export",
"No market": "No market",
"No markets": "No markets",
"No markets.": "No markets.",
"No members": "No members",
"No MetaMask version that supports snaps detected. Learn more about <0>MetaMask Snaps</0>": "No MetaMask version that supports snaps detected. Learn more about <0>MetaMask Snaps</0>",
"No open orders": "No open orders",
"No orders": "No orders",
@@ -185,6 +198,7 @@
"No third party has access to your funds.": "No third party has access to your funds.",
"No volume discount program active": "No volume discount program active",
"No withdrawals": "No withdrawals",
"No. of participating teams": "No. of participating teams",
"Node: {{VEGA_URL}} is unsuitable": "Node: {{VEGA_URL}} is unsuitable",
"Non-custodial and pseudonymous": "Non-custodial and pseudonymous",
"None": "None",
@@ -204,6 +218,10 @@
"pastEpochs_other": "Past {{count}} epochs",
"Pennant": "Pennant",
"Perpetuals": "Perpetuals",
"place_ordinal_one": "{{count}}st",
"place_ordinal_two": "{{count}}nd",
"place_ordinal_few": "{{count}}rd",
"place_ordinal_other": "{{count}}th",
"Please choose another market from the <0>market list</0>": "Please choose another market from the <0>market list</0>",
"Please connect Vega wallet": "Please connect Vega wallet",
"Portfolio": "Portfolio",
@@ -218,10 +236,12 @@
"Purpose built proof of stake blockchain": "Purpose built proof of stake blockchain",
"qUSD": "qUSD",
"qUSD provides a rough USD equivalent of balances across all assets using the value of \"Quantum\" for that asset": "qUSD provides a rough USD equivalent of balances across all assets using the value of \"Quantum\" for that asset",
"Rank": "Rank",
"Read the terms": "Read the terms",
"Ready to trade": "Ready to trade",
"Ready to trade with real funds? <0>Switch to Mainnet</0>": "Ready to trade with real funds? <0>Switch to Mainnet</0>",
"Redeem rewards": "Redeem rewards",
"Referee": "Referee",
"Referral benefits": "Referral benefits",
"Referral discount": "Referral discount",
"Referrals": "Referrals",
@@ -293,10 +313,14 @@
"to": "to",
"To protect the network from spam, you must have at least {{requiredFunds}} qUSD of any asset on the network to proceed.": "To protect the network from spam, you must have at least {{requiredFunds}} qUSD of any asset on the network to proceed.",
"Toast location": "Toast location",
"Total amount of rewards paid out to this team in qUSD": "Total amount of rewards paid out to this team in qUSD",
"Total discount": "Total discount",
"Total distributed": "Total distributed",
"Total fee after discount": "Total fee after discount",
"Total fee before discount": "Total fee before discount",
"Total games": "Total games",
"Total number of games this team has participated in": "Total number of games this team has participated in",
"Total volume": "Total volume",
"totalCommission": "Total commission (<0>last {{count}} epochs</0>)",
"totalCommission_one": "Total commission (<0>last {{count}} epoch</0>)",
"totalCommission_other": "Total commission (<0>last {{count}} epochs</0>)",
@@ -310,6 +334,7 @@
"Trading on market {{name}} may stop. There are open proposals to close this market": "Trading on market {{name}} may stop. There are open proposals to close this market",
"Trading on market {{name}} will stop on {{date}}": "Trading on market {{name}} will stop on {{date}}",
"Transfer": "Transfer",
"Type": "Type",
"Unknown": "Unknown",
"Unknown settlement date": "Unknown settlement date",
"Vega chart": "Vega chart",
@@ -365,10 +390,11 @@
"Rewards funded using the pro-rata strategy should be distributed pro-rata by each entity's reward metric scaled by any active multipliers that party has": " Rewards funded using the pro-rata strategy should be distributed pro-rata by each entity's reward metric scaled by any active multipliers that party has",
"Tier {{tier}}": "Tier {{tier}}",
"Reward {{reward}}x": "Reward {{reward}}x",
"Rewards paid": "Rewards paid",
"Reward bonus": "Reward bonus",
"Vesting {{vesting}}x": "Vesting {{vesting}}x",
"Tier {{userTier}}": "Tier {{userTier}}",
"{{reward}}x": "{{reward}}x",
"Reward bonus": "Reward bonus",
"Activity Streak": "Activity Streak",
"userActive": "{{active}} trader: {{count}} epochs so far",
"(Tier {{tier}} as of last epoch)": "(Tier {{tier}} as of last epoch)",
@@ -8,13 +8,15 @@ export type VegaIconSize = 8 | 10 | 12 | 13 | 14 | 16 | 18 | 20 | 24 | 28 | 32;
export interface VegaIconProps {
name: VegaIconNames;
size?: VegaIconSize;
className?: string;
}
export const VegaIcon = ({ size = 16, name }: VegaIconProps) => {
export const VegaIcon = ({ size = 16, name, className }: VegaIconProps) => {
const effectiveClassName = classNames(
'inline-block',
'align-text-bottom',
'fill-current stroke-none'
'fill-current stroke-none',
className
);
const Element = VegaIconNameMap[name];
return (
+28
View File
@@ -12,6 +12,7 @@ import {
quantumDecimalPlaces,
toDecimal,
toNumberParts,
formatNumberRounded,
} from './number';
describe('number utils', () => {
@@ -246,3 +247,30 @@ describe('getUnlimitedThreshold', () => {
}
);
});
describe('formatNumberRounded', () => {
it('rounds number with symbol', () => {
expect(formatNumberRounded(new BigNumber(1))).toBe('1');
expect(formatNumberRounded(new BigNumber(1_000))).toBe('1,000');
expect(formatNumberRounded(new BigNumber(1_000_000))).toBe('1m');
expect(formatNumberRounded(new BigNumber(1_000_000_000))).toBe('1b');
expect(formatNumberRounded(new BigNumber(1_000_000_000_000))).toBe('1t');
});
it('respects the limit parameter', () => {
expect(formatNumberRounded(new BigNumber(1_000), '1e3')).toBe('1k');
expect(formatNumberRounded(new BigNumber(1_000_000), '1e9')).toBe(
'1,000,000'
);
expect(formatNumberRounded(new BigNumber(9_999_999), '1e9')).toBe(
'9,999,999'
);
expect(formatNumberRounded(new BigNumber(1_000_000_000), '1e9')).toBe('1b');
expect(formatNumberRounded(new BigNumber(1_000_000_000), '1e12')).toBe(
'1,000,000,000'
);
expect(formatNumberRounded(new BigNumber(1_000_000_000_000), '1e9')).toBe(
'1t'
);
});
});
+21 -4
View File
@@ -184,7 +184,10 @@ export const isNumeric = (
* Format a number greater than 1 million with m for million, b for billion
* and t for trillion
*/
export const formatNumberRounded = (num: BigNumber) => {
export const formatNumberRounded = (
num: BigNumber,
limit: '1e12' | '1e9' | '1e6' | '1e3' = '1e6'
) => {
let value = '';
const format = (divisor: string) => {
@@ -194,15 +197,29 @@ export const formatNumberRounded = (num: BigNumber) => {
if (num.isGreaterThan(new BigNumber('1e14'))) {
value = '>100t';
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e12'))) {
} else if (
num.isGreaterThanOrEqualTo(limit) &&
num.isGreaterThanOrEqualTo(new BigNumber('1e12'))
) {
// Trillion
value = `${format('1e12')}t`;
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e9'))) {
} else if (
num.isGreaterThanOrEqualTo(limit) &&
num.isGreaterThanOrEqualTo(new BigNumber('1e9'))
) {
// Billion
value = `${format('1e9')}b`;
} else if (num.isGreaterThanOrEqualTo(new BigNumber('1e6'))) {
} else if (
num.isGreaterThanOrEqualTo(limit) &&
num.isGreaterThanOrEqualTo(new BigNumber('1e6'))
) {
// Million
value = `${format('1e6')}m`;
} else if (
num.isGreaterThanOrEqualTo(limit) &&
num.isGreaterThanOrEqualTo(new BigNumber('1e3'))
) {
value = `${format('1e3')}k`;
} else {
value = formatNumber(num);
}