42a98b6a35
Co-authored-by: bwallacee <ben@vega.xyz>
256 lines
7.1 KiB
TypeScript
256 lines
7.1 KiB
TypeScript
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 { data, team, partyTeam, stats, members, games, loading, refetch } =
|
|
useTeam(teamId, pubKey || undefined);
|
|
|
|
// only show spinner on first load so when users join teams its smoother
|
|
if (!data && 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>
|
|
<div className="flex gap-2">
|
|
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
|
|
<UpdateTeamButton team={team} />
|
|
</div>
|
|
</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} isCreator={m.isCreator} />,
|
|
rewards: formatNumber(m.totalQuantumRewards),
|
|
volume: formatNumber(m.totalQuantumVolume),
|
|
gamesPlayed: formatNumber(m.totalGamesPlayed),
|
|
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
|
|
joinedAtEpoch: Number(m.joinedAtEpoch),
|
|
})),
|
|
'joinedAtEpoch',
|
|
'desc'
|
|
);
|
|
|
|
return (
|
|
<Table
|
|
columns={[
|
|
{ name: 'referee', displayName: t('Member ID') },
|
|
{ name: 'rewards', displayName: t('Rewards earned') },
|
|
{ name: 'volume', displayName: t('Total volume') },
|
|
{ name: 'gamesPlayed', displayName: t('Games played') },
|
|
{
|
|
name: 'joinedAt',
|
|
displayName: t('Joined at'),
|
|
},
|
|
{
|
|
name: 'joinedAtEpoch',
|
|
displayName: t('Joined epoch'),
|
|
},
|
|
]}
|
|
data={data}
|
|
noCollapse={true}
|
|
/>
|
|
);
|
|
};
|
|
|
|
const RefereeLink = ({
|
|
pubkey,
|
|
isCreator,
|
|
}: {
|
|
pubkey: string;
|
|
isCreator: boolean;
|
|
}) => {
|
|
const t = useT();
|
|
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>{' '}
|
|
<span className="text-muted text-xs">{isCreator ? t('Owner') : ''}</span>
|
|
</>
|
|
);
|
|
};
|
|
|
|
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,
|
|
})}
|
|
/>
|
|
);
|
|
};
|