Compare commits

..
Author SHA1 Message Date
Matthew Russell 6178faf36f fix: incorrect argument for renderComponent helper 2024-01-18 10:02:39 -05:00
Matthew Russell af6b691405 fix: reset tx state to idle if an error is thrown 2024-01-17 18:55:18 -05:00
Matthew Russell 3bd9fb8ce1 fix: align header text with data 2024-01-17 18:47:51 -05:00
Matthew Russell 0af61ea04b fix: collapsing header on members table 2024-01-17 18:47:12 -05:00
Matthew Russell 22e9d8a6ae chore: align competitions grid card cta 2024-01-17 18:40:02 -05:00
Matthew Russell 49b16b6654 chore: move layouts into appropriate dirs 2024-01-17 17:05:48 -05:00
Matthew Russell a706633192 chore: left align no games text 2024-01-17 17:05:48 -05:00
Matthew Russell 8fa64e93fb feat(trading): join team (#5629) 2024-01-17 22:04:51 +00:00
Matthew Russell 6b5fa440fb feat(trading): create team (#5610) 2024-01-17 10:44:14 -05:00
asiaznik 0894854d3e chore: fallback team avatars 2024-01-17 10:44:14 -05:00
asiaznik e59b2e049f chore: team avatar sizes 2024-01-17 10:44:14 -05:00
asiaznik e0f04ab1b4 chore: sky layout, blurred layout 2024-01-17 10:44:14 -05:00
asiaznik 4484bdf0e0 chore: married with epic branch 2024-01-17 10:44:14 -05:00
asiaznik 316c31ee5d chore(trading): competitions home page
feat(trading): competitions
2024-01-17 10:44:14 -05:00
Matthew Russell 398fda6245 Feat/5486 team page (#5620) 2024-01-17 10:44:13 -05:00
Matthew Russell 47aff2a24d chore: tidy handling of proposal union type 2024-01-17 10:43:47 -05:00
Matthew Russell cadf9509e7 chore: handle proposal union type 2024-01-17 10:43:47 -05:00
Matthew Russell 85489a6a6e chore: fix ts errors in trading app 2024-01-17 10:43:47 -05:00
Matthew Russell e00d933c84 chore: fix ts errors in candles-chart 2024-01-17 10:43:47 -05:00
Matthew Russell 799a95e7a1 chore: fix type errors in market lib 2024-01-17 10:43:47 -05:00
Matthew Russell 01c03377e2 chore: fix type errors in proposals lib 2024-01-17 10:43:47 -05:00
Matthew Russell 3ef3ac9cd2 chore: fix global type mappings 2024-01-17 10:43:47 -05:00
Matthew Russell 18e461ecea chore: fix type issues 2024-01-17 10:43:47 -05:00
Matthew Russell 7fa1f1ec5c chore: use extract to narrow type 2024-01-17 10:43:47 -05:00
Matthew Russell 4e13ad52a9 chore: regen types 2024-01-17 10:43:45 -05:00
199 changed files with 5895 additions and 2653 deletions
@@ -1,9 +1,11 @@
query ExplorerProposal($id: ID!) {
proposal(id: $id) {
id
rationale {
title
description
... on Proposal {
id
rationale {
title
description
}
}
}
}
@@ -8,16 +8,18 @@ export type ExplorerProposalQueryVariables = Types.Exact<{
}>;
export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null };
export type ExplorerProposalQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, rationale: { __typename?: 'ProposalRationale', title: string, description: string } } | null };
export const ExplorerProposalDocument = gql`
query ExplorerProposal($id: ID!) {
proposal(id: $id) {
id
rationale {
title
description
... on Proposal {
id
rationale {
title
description
}
}
}
}
@@ -1,7 +1,11 @@
import { useExplorerProposalQuery } from './__generated__/Proposal';
import {
useExplorerProposalQuery,
type ExplorerProposalQuery,
} from './__generated__/Proposal';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ENV } from '../../../config/env';
import Hash from '../hash';
export type ProposalLinkProps = {
id: string;
text?: string;
@@ -16,8 +20,13 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
variables: { id },
});
const proposal = data?.proposal as Extract<
ExplorerProposalQuery['proposal'],
{ __typename?: 'Proposal' }
>;
const base = ENV.dataSources.governanceUrl;
const label = data?.proposal?.rationale.title || id;
const label = proposal?.rationale.title || id;
return (
<ExternalLink href={`${base}/proposals/${id}`}>
@@ -1,7 +1,9 @@
query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) {
id
state
rejectionReason
... on Proposal {
id
state
rejectionReason
}
}
}
@@ -8,15 +8,17 @@ export type ExplorerProposalStatusQueryVariables = Types.Exact<{
}>;
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
export const ExplorerProposalStatusDocument = gql`
query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) {
id
state
rejectionReason
... on Proposal {
id
state
rejectionReason
}
}
}
`;
@@ -14,16 +14,18 @@ export function format(date: string | undefined, def: string) {
return new Date().toLocaleDateString() || def;
}
export function getDate(
data: ExplorerProposalStatusQuery | undefined,
terms: Terms
): string {
type Proposal = Extract<
ExplorerProposalStatusQuery['proposal'],
{ __typename?: 'Proposal' }
>;
export function getDate(proposal: Proposal | undefined, terms: Terms): string {
const DEFAULT = t('Unknown');
if (!data?.proposal?.state) {
if (!proposal?.state) {
return DEFAULT;
}
switch (data.proposal.state) {
switch (proposal.state) {
case 'STATE_DECLINED':
return `${t('Rejected on')}: ${format(terms.closingTimestamp, DEFAULT)}`;
case 'STATE_ENACTED':
@@ -62,9 +64,11 @@ export const ProposalDate = ({ terms, id }: ProposalDateProps) => {
},
});
const proposal = data?.proposal as Proposal;
return (
<Lozenge className="font-sans text-xs float-right">
{getDate(data, terms)}
{getDate(proposal, terms)}
</Lozenge>
);
};
@@ -2,17 +2,8 @@ import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { IconProps } from '@vegaprotocol/ui-toolkit';
import { useExplorerProposalStatusQuery } from './__generated__/Proposal';
import type { ExplorerProposalStatusQuery } from './__generated__/Proposal';
import type * as Apollo from '@apollo/client';
import type * as Types from '@vegaprotocol/types';
import { t } from '@vegaprotocol/i18n';
type ProposalQueryResult = Apollo.QueryResult<
ExplorerProposalStatusQuery,
Types.Exact<{
id: string;
}>
>;
interface ProposalStatusIconProps {
id: string;
}
@@ -29,29 +20,38 @@ type IconAndLabel = {
* @param data a data result from useExplorerProposalStatusQuery
* @returns Icon name
*/
export function getIconAndLabelForStatus(
res: ProposalQueryResult
): IconAndLabel {
export function useIconAndLabelForStatus(id: string): IconAndLabel {
const { data, loading, error } = useExplorerProposalStatusQuery({
variables: {
id,
},
});
const proposal = data?.proposal as Extract<
ExplorerProposalStatusQuery['proposal'],
{ __typename?: 'Proposal' }
>;
const DEFAULT: IconAndLabel = {
icon: 'error',
label: t('Proposal state unknown'),
};
if (res.loading) {
if (loading) {
return {
icon: 'more',
label: t('Loading data'),
};
}
if (!res?.data?.proposal || res.error) {
if (!data?.proposal || error) {
return {
icon: 'error',
label: res.error?.message || DEFAULT.label,
label: error?.message || DEFAULT.label,
};
}
switch (res.data.proposal.state) {
switch (proposal.state) {
case 'STATE_DECLINED':
return {
icon: 'stop',
@@ -99,13 +99,7 @@ export function getIconAndLabelForStatus(
/**
*/
export const ProposalStatusIcon = ({ id }: ProposalStatusIconProps) => {
const { icon, label } = getIconAndLabelForStatus(
useExplorerProposalStatusQuery({
variables: {
id,
},
})
);
const { icon, label } = useIconAndLabelForStatus(id);
return (
<div className="float-left mr-3">
@@ -1,54 +0,0 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerReferralCodeOwnerQueryVariables = Types.Exact<{
id: Types.Scalars['ID'];
}>;
export type ExplorerReferralCodeOwnerQuery = { __typename?: 'Query', referralSets: { __typename?: 'ReferralSetConnection', edges: Array<{ __typename?: 'ReferralSetEdge', node: { __typename?: 'ReferralSet', createdAt: any, updatedAt: any, referrer: string } } | null> } };
export const ExplorerReferralCodeOwnerDocument = gql`
query ExplorerReferralCodeOwner($id: ID!) {
referralSets(id: $id) {
edges {
node {
createdAt
updatedAt
referrer
}
}
}
}
`;
/**
* __useExplorerReferralCodeOwnerQuery__
*
* To run a query within a React component, call `useExplorerReferralCodeOwnerQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerReferralCodeOwnerQuery` 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 } = useExplorerReferralCodeOwnerQuery({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useExplorerReferralCodeOwnerQuery(baseOptions: Apollo.QueryHookOptions<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>(ExplorerReferralCodeOwnerDocument, options);
}
export function useExplorerReferralCodeOwnerLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>(ExplorerReferralCodeOwnerDocument, options);
}
export type ExplorerReferralCodeOwnerQueryHookResult = ReturnType<typeof useExplorerReferralCodeOwnerQuery>;
export type ExplorerReferralCodeOwnerLazyQueryHookResult = ReturnType<typeof useExplorerReferralCodeOwnerLazyQuery>;
export type ExplorerReferralCodeOwnerQueryResult = Apollo.QueryResult<ExplorerReferralCodeOwnerQuery, ExplorerReferralCodeOwnerQueryVariables>;
@@ -1,11 +0,0 @@
query ExplorerReferralCodeOwner($id: ID!) {
referralSets(id: $id) {
edges {
node {
createdAt
updatedAt
referrer
}
}
}
}
@@ -1,95 +0,0 @@
import { render, waitFor } from '@testing-library/react';
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { MemoryRouter } from 'react-router-dom';
import { ReferralCodeOwner } from './referral-code-owner';
import type { ReferralCodeOwnerProps } from './referral-code-owner';
import { ExplorerReferralCodeOwnerDocument } from './__generated__/code-owner';
const renderComponent = (
props: ReferralCodeOwnerProps,
mocks: MockedResponse[]
) => {
return render(
<MockedProvider mocks={mocks}>
<MemoryRouter>
<ReferralCodeOwner {...props} />
</MemoryRouter>
</MockedProvider>
);
};
describe('ReferralCodeOwner', () => {
it('should render loading state', () => {
const mocks = [
{
request: {
query: ExplorerReferralCodeOwnerDocument,
variables: {
id: 'ABC123',
},
},
},
];
const { getByText } = renderComponent({ code: 'ABC123' }, mocks);
expect(getByText('Loading...')).toBeInTheDocument();
});
it('should render error state', async () => {
const errorMessage = 'Error fetching referrer: ABC123';
const mocks = [
{
request: {
query: ExplorerReferralCodeOwnerDocument,
variables: {
id: 'ABC123',
},
},
error: new Error('nope'),
},
];
const { getByText } = renderComponent({ code: 'ABC123' }, mocks);
await waitFor(() => {
expect(getByText(errorMessage)).toBeInTheDocument();
});
});
it('should render link to referring party', async () => {
const referrerId = 'DEF789';
const mocks = [
{
request: {
query: ExplorerReferralCodeOwnerDocument,
variables: {
id: 'ABC123',
},
},
result: {
data: {
referralSets: {
edges: [
{
node: {
__typename: 'ReferralSet',
referrer: referrerId,
createdAt: '123',
updatedAt: '456',
},
},
],
},
},
},
},
];
const { getByText } = renderComponent({ code: 'ABC123' }, mocks);
await waitFor(() => {
expect(getByText(referrerId)).toBeInTheDocument();
});
});
});
@@ -1,26 +0,0 @@
import { TableCell } from '../../../table';
import { useExplorerReferralCodeOwnerQuery } from './__generated__/code-owner';
import { PartyLink } from '../../../links';
export interface ReferralCodeOwnerProps {
code: string;
}
/**
* Render the owner of a referral code
*/
export const ReferralCodeOwner = ({ code }: ReferralCodeOwnerProps) => {
const { data, error, loading } = useExplorerReferralCodeOwnerQuery({
variables: {
id: code,
},
});
const referrer = data?.referralSets.edges[0]?.node.referrer || '';
return (
<TableCell>
{loading && 'Loading...'}
{error && `Error fetching referrer: ${code}`}
{referrer.length > 0 && <PartyLink id={referrer} />}
</TableCell>
);
};
@@ -1,93 +0,0 @@
import { render } from '@testing-library/react';
import { ReferralTeam } from './team';
import type { CreateReferralSet } from './team';
import { MockedProvider } from '@apollo/client/testing';
describe('ReferralTeam', () => {
const team = {
name: 'Test Team',
teamUrl: 'https://example.com/team',
avatarUrl: 'https://example.com/avatar',
closed: false,
};
const mockTx: CreateReferralSet = {
team,
};
const mockId = '123456';
const mockCreator = 'JohnDoe';
it('should render the team name', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Test Team')).toBeInTheDocument();
});
it('should render the team ID', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Id')).toBeInTheDocument();
expect(getByText(mockId)).toBeInTheDocument();
});
it('should render the creator', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Creator')).toBeInTheDocument();
expect(getByText(mockCreator)).toBeInTheDocument();
});
it('should render the team URL', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Team URL')).toBeInTheDocument();
expect(getByText(team.teamUrl)).toBeInTheDocument();
});
it('should render the avatar URL', () => {
const { getByText } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByText('Avatar')).toBeInTheDocument();
expect(getByText(team.avatarUrl)).toBeInTheDocument();
});
it('should render the open status as a tick if closed is falsy', () => {
const { getByTestId } = render(
<MockedProvider>
<ReferralTeam tx={mockTx} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByTestId('open-yes')).toBeInTheDocument();
});
it('should render the open status as a cross if it is truthy', () => {
const m = {
team: {
closed: true,
},
};
const { getByTestId } = render(
<MockedProvider>
<ReferralTeam tx={m} id={mockId} creator={mockCreator} />
</MockedProvider>
);
expect(getByTestId('open-no')).toBeInTheDocument();
});
});
@@ -1,73 +0,0 @@
import {
VegaIcon,
Icon,
KeyValueTable,
KeyValueTableRow,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { components } from '../../../../../types/explorer';
import Hash from '../../../links/hash';
import { t } from 'i18next';
import { PartyLink } from '../../../links';
export type CreateReferralSet = components['schemas']['v1CreateReferralSet'];
export type ReferralTeam = CreateReferralSet['team'];
export interface ReferralTeamProps {
tx: CreateReferralSet;
id: string;
creator: string;
}
/**
* Renders the details for a team in a CreateReferralSet or UpdateReferralSet transaction.
*
* Intentionally does not render the avatar image or link to the team url.
*/
export const ReferralTeam = ({ tx, id, creator }: ReferralTeamProps) => {
if (!tx.team) {
return null;
}
return (
<section>
<div className="inline-block mr-2 leading-none">
<VegaIcon name={VegaIconNames.TEAM} />
</div>
{tx.team.name && (
<h3 className="inline-block leading-loose">{tx.team.name}</h3>
)}
<div className="min-w-fit max-w-2xl block">
<KeyValueTable>
<KeyValueTableRow>
{t('Id')}
<Hash text={id} truncate={false} />
</KeyValueTableRow>
<KeyValueTableRow>
{t('Creator')}
<PartyLink id={creator} truncate={false} />
</KeyValueTableRow>
{tx.team.teamUrl && (
<KeyValueTableRow>
{t('Team URL')}
<Hash text={tx.team.teamUrl} truncate={false} />
</KeyValueTableRow>
)}
{tx.team.avatarUrl && (
<KeyValueTableRow>
{t('Avatar')}
<Hash text={tx.team.avatarUrl} truncate={false} />
</KeyValueTableRow>
)}
<KeyValueTableRow>
{t('Open')}
<span data-testid={!tx.team.closed ? 'open-yes' : 'open-no'}>
{!tx.team.closed ? <Icon name="tick" /> : <Icon name="cross" />}
</span>
</KeyValueTableRow>
</KeyValueTable>
</div>
</section>
);
};
@@ -27,8 +27,7 @@ export const sharedHeaderProps = {
className: 'align-top',
};
// The incoming type field is usually the right thing to show. Exceptions are listed here
const LabelOverrides: Record<BlockExplorerTransactionResult['type'], string> = {
const Labels: Record<BlockExplorerTransactionResult['type'], string> = {
'Stop Orders Submission': 'Stop Order',
'Stop Orders Cancellation': 'Cancel Stop Order',
};
@@ -51,7 +50,7 @@ export const TxDetailsShared = ({
const time: string = blockData?.result.block.header.time || '';
const height: string = blockData?.result.block.header.height || txData.block;
const type = LabelOverrides[txData.type] || txData.type;
const type = Labels[txData.type] || txData.type;
return (
<>
@@ -1,47 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { ReferralCodeOwner } from './referrals/referral-code-owner';
interface TxDetailsApplyReferralCodeProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* The signature can be turned in to an id with txSignatureToDeterministicId but
*/
export const TxDetailsApplyReferralCode = ({
txData,
pubKey,
blockData,
}: TxDetailsApplyReferralCodeProps) => {
if (!txData || !txData.command.applyReferralCode || !txData.signature) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const referralCode = txData.command.applyReferralCode.id;
return (
<div>
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{t('Applied Code')}</TableCell>
<TableCell>{referralCode}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Referrer')}</TableCell>
<ReferralCodeOwner code={referralCode} />
</TableRow>
</TableWithTbody>
</div>
);
};
@@ -1,52 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import { ReferralTeam } from './referrals/team';
interface TxDetailsCreateReferralProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* The signature can be turned in to an id with txSignatureToDeterministicId but
*/
export const TxDetailsCreateReferralSet = ({
txData,
pubKey,
blockData,
}: TxDetailsCreateReferralProps) => {
if (!txData || !txData.command.createReferralSet || !txData.signature) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const id = txSignatureToDeterministicId(txData.signature.value);
const isTeam = txData.command.createReferralSet.isTeam || false;
return (
<div>
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{isTeam ? t('Team ID') : t('Referral code')}</TableCell>
<TableCell>{id}</TableCell>
</TableRow>
</TableWithTbody>
<ReferralTeam
tx={txData.command.createReferralSet}
id={id}
creator={txData.submitter}
/>
</div>
);
};
@@ -28,10 +28,6 @@ import { TxProposal } from './tx-proposal';
import { TxDetailsTransfer } from './tx-transfer';
import { TxDetailsStopOrderSubmission } from './tx-stop-order-submission';
import { TxDetailsLiquiditySubmission } from './tx-liquidity-submission';
import { TxDetailsCreateReferralSet } from './tx-create-referral-set';
import { TxDetailsApplyReferralCode } from './tx-apply-referral-code';
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
import { TxDetailsJoinTeam } from './tx-join-team';
interface TxDetailsWrapperProps {
txData: BlockExplorerTransactionResult | undefined;
@@ -125,14 +121,6 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
return TxDetailsStopOrderSubmission;
case 'Transfer Funds':
return TxDetailsTransfer;
case 'Create Referral Set':
return TxDetailsCreateReferralSet;
case 'Update Referral Set':
return TxDetailsUpdateReferralSet;
case 'Apply Referral Code':
return TxDetailsApplyReferralCode;
case 'Join Team':
return TxDetailsJoinTeam;
default:
return TxDetailsGeneric;
}
@@ -1,47 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { ReferralCodeOwner } from './referrals/referral-code-owner';
interface TxDetailsJoinTeamProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* The signature can be turned in to an id with txSignatureToDeterministicId but
*/
export const TxDetailsJoinTeam = ({
txData,
pubKey,
blockData,
}: TxDetailsJoinTeamProps) => {
if (!txData || !txData.command.joinTeam || !txData.signature) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const team = txData.command.joinTeam.id;
return (
<div>
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{t('Team')}</TableCell>
<TableCell>{team}</TableCell>
</TableRow>
<TableRow modifier="bordered">
<TableCell>{t('Referrer')}</TableCell>
<ReferralCodeOwner code={team} />
</TableRow>
</TableWithTbody>
</div>
);
};
@@ -1,54 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
import { TxDetailsShared } from './shared/tx-details-shared';
import { TableCell, TableRow, TableWithTbody } from '../../table';
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
import { ReferralTeam } from './referrals/team';
interface TxDetailsUpdateReferralProps {
txData: BlockExplorerTransactionResult | undefined;
pubKey: string | undefined;
blockData: TendermintBlocksResponse | undefined;
}
/**
* A copy of create referral set, effectively.
* Updating a referral set without a team doesn't make sense,
* but is valid, so this component ignores sense.
*/
export const TxDetailsUpdateReferralSet = ({
txData,
pubKey,
blockData,
}: TxDetailsUpdateReferralProps) => {
if (!txData || !txData.command.updateReferralSet || !txData.signature) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
const id = txSignatureToDeterministicId(txData.signature.value);
const isTeam = txData.command.updateReferralSet.isTeam || false;
return (
<div>
<TableWithTbody className="mb-8" allowWrap={true}>
<TxDetailsShared
txData={txData}
pubKey={pubKey}
blockData={blockData}
/>
<TableRow modifier="bordered">
<TableCell>{isTeam ? t('Team ID') : t('Referral code')}</TableCell>
<TableCell>{id}</TableCell>
</TableRow>
</TableWithTbody>
<ReferralTeam
tx={txData.command.updateReferralSet}
id={id}
creator={txData.submitter}
/>
</div>
);
};
@@ -28,7 +28,6 @@ export type FilterOption =
| 'Delegate'
| 'Ethereum Key Rotate Submission'
| 'Issue Signatures'
| 'Join Team'
| 'Key Rotate Submission'
| 'Liquidity Provision Order'
| 'Node Signature'
@@ -48,46 +47,45 @@ export type FilterOption =
| 'Vote on Proposal'
| 'Withdraw';
export const filterOptions: Record<string, FilterOption[]> = {
'Market Instructions': [
'Amend LiquidityProvision Order',
'Amend Order',
'Batch Market Instructions',
'Cancel LiquidityProvision Order',
'Cancel Order',
'Liquidity Provision Order',
'Stop Orders Submission',
'Stop Orders Cancellation',
'Submit Order',
],
'Transfers and Withdrawals': [
'Transfer Funds',
'Cancel Transfer Funds',
'Withdraw',
],
Governance: ['Delegate', 'Undelegate', 'Vote on Proposal', 'Proposal'],
Referrals: [
'Apply Referral Code',
'Create Referral Set',
'Join Team',
'Update Referral Set',
],
'External Data': ['Chain Event', 'Submit Oracle Data'],
Validators: [
'Ethereum Key Rotate Submission',
'Issue Signatures',
'Key Rotate Submission',
'Node Signature',
'Node Vote',
'Protocol Upgrade',
'Register new Node',
'State Variable Proposal',
'Validator Heartbeat',
],
};
// Alphabetised list of transaction types to appear at the top level
export const PrimaryFilterOptions: FilterOption[] = [
'Amend LiquidityProvision Order',
'Amend Order',
'Batch Market Instructions',
'Cancel LiquidityProvision Order',
'Cancel Order',
'Cancel Transfer Funds',
'Delegate',
'Liquidity Provision Order',
'Proposal',
'Stop Orders Submission',
'Stop Orders Cancellation',
'Submit Oracle Data',
'Submit Order',
'Transfer Funds',
'Undelegate',
'Vote on Proposal',
'Withdraw',
];
export const AllFilterOptions: FilterOption[] =
Object.values(filterOptions).flat();
// Alphabetised list of transaction types to nest under a 'More...' submenu
export const SecondaryFilterOptions: FilterOption[] = [
'Chain Event',
'Ethereum Key Rotate Submission',
'Issue Signatures',
'Key Rotate Submission',
'Node Signature',
'Node Vote',
'Protocol Upgrade',
'Register new Node',
'State Variable Proposal',
'Validator Heartbeat',
];
export const AllFilterOptions: FilterOption[] = [
...PrimaryFilterOptions,
...SecondaryFilterOptions,
];
export interface TxFilterProps {
filters: Set<FilterOption>;
@@ -124,33 +122,46 @@ export const TxsFilter = ({ filters, setFilters }: TxFilterProps) => {
<DropdownMenuSeparator />
</>
)}
{Object.entries(filterOptions).map(([key, value]) => (
<DropdownMenuSub key={key}>
<DropdownMenuSubTrigger>
{t(key)}
<Icon name="chevron-right" />
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{value.map((f) => (
<DropdownMenuCheckboxItem
key={f}
checked={filters.has(f)}
onCheckedChange={(checked) => {
// NOTE: These act like radio buttons until the API supports multiple filters
setFilters(new Set([f]));
}}
id={`radio-${f}`}
>
{f}
<DropdownMenuItemIndicator>
<Icon name="tick-circle" className="inline" />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
{PrimaryFilterOptions.map((f) => (
<DropdownMenuCheckboxItem
key={f}
checked={filters.has(f)}
onCheckedChange={() => {
// NOTE: These act like radio buttons until the API supports multiple filters
setFilters(new Set([f]));
}}
id={`radio-${f}`}
>
{f}
<DropdownMenuItemIndicator>
<Icon name="tick-circle" />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
))}
<DropdownMenuSub>
<DropdownMenuSubTrigger>
{t('More Types')}
<Icon name="chevron-right" />
</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
{SecondaryFilterOptions.map((f) => (
<DropdownMenuCheckboxItem
key={f}
checked={filters.has(f)}
onCheckedChange={(checked) => {
// NOTE: These act like radio buttons until the API supports multiple filters
setFilters(new Set([f]));
}}
id={`radio-${f}`}
>
{f}
<DropdownMenuItemIndicator>
<Icon name="tick-circle" className="inline" />
</DropdownMenuItemIndicator>
</DropdownMenuCheckboxItem>
))}
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuContent>
</DropdownMenu>
);
@@ -48,8 +48,6 @@ const displayString: StringMap = {
StopOrdersSubmission: 'Stop',
StopOrdersCancellation: 'Cancel stop',
'Stop Orders Cancellation': 'Cancel stop',
'Apply Referral Code': 'Referral',
'Create Referral Set': 'Create referral',
};
export function getLabelForStopOrderType(
@@ -31,10 +31,6 @@ const Tx = () => {
}
}
if (!data || !data?.transaction) {
errorMessage = 'Transaction not found';
}
return (
<section>
<PageHeader
@@ -53,7 +49,7 @@ const Tx = () => {
<TxDetails
className="mb-28"
txData={data?.transaction}
pubKey={data?.transaction?.submitter}
pubKey={data?.transaction.submitter}
/>
</RenderFetched>
</section>
@@ -11,8 +11,8 @@ interface TxDetailsProps {
export const txDetailsTruncateLength = 30;
export const TxDetails = ({ txData, pubKey }: TxDetailsProps) => {
if (!txData || !pubKey) {
return <>{t('Transaction could not be found')}</>;
if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>;
}
return (
<section className="mb-10" key={txData.hash}>
+2 -2
View File
@@ -8,13 +8,13 @@ type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = T | U extends object
? (Without<T, U> & U) | (Without<U, T> & T)
: T | U;
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-explicit-any */
type OneOf<T extends any[]> = T extends [infer Only]
? Only
: T extends [infer A, infer B, ...infer Rest]
? OneOf<[XOR<A, B>, ...Rest]>
: never;
/* eslint-enable @typescript-eslint/no-explicit-any */
/* eslint-enable @typescript-eslint/no-explicit-any */
export interface paths {
'/info': {
@@ -47,7 +47,7 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
.and('contain.text', 'USDC (fake)');
});
it.skip('Unable to submit proposal with public key', function () {
it('Unable to submit proposal with public key', function () {
const expectedErrorTxt = `You are connected in a view only state for public key: ${vegaWalletPubKey}. In order to send transactions you must connect to a real wallet.`;
goToMakeNewProposal(governanceProposalType.RAW);
@@ -55,10 +55,7 @@ context('View functionality with public key', { tags: '@smoke' }, function () {
cy.getByTestId('dialog-content')
.first()
.within(() => {
cy.getByTestId('dialog-title').should(
'have.text',
'Transaction failed'
);
cy.get('h1').should('have.text', 'Transaction failed');
cy.getByTestId('Error').should('have.text', expectedErrorTxt);
});
});
@@ -112,7 +112,6 @@ export function createNewMarketProposalTxBody(): ProposalSubmissionBody {
performanceHysteresisEpochs: 2,
slaCompetitionFactor: '0.1',
},
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
quadraticSlippageFactor: '0',
instrument: {
name: 'Token test market',
@@ -242,7 +241,6 @@ export function createSuccessorMarketProposalTxBody(
decimalPlaces: '5',
positionDecimalPlaces: '5',
linearSlippageFactor: '0.001',
// FIXME: workaround because of https://github.com/vegaprotocol/vega/issues/10343
quadraticSlippageFactor: '0',
liquiditySlaParameters: {
priceRange: '0.5',
+2 -2
View File
@@ -31,7 +31,7 @@ import {
orderByUpgradeBlockHeight,
} from '../proposals/components/proposals-list/proposals-list';
import { BigNumber } from '../../lib/bignumber';
import type { ProposalQuery } from '../proposals/proposal/__generated__/Proposal';
import { type Proposal } from '../proposals/types';
const nodesToShow = 6;
@@ -39,7 +39,7 @@ const HomeProposals = ({
proposals,
protocolUpgradeProposals,
}: {
proposals: ProposalQuery['proposal'][];
proposals: Proposal[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
}) => {
const { t } = useTranslation();
@@ -1,16 +1,11 @@
import { useTranslation } from 'react-i18next';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalState } from '@vegaprotocol/types';
import { ProposalInfoLabel } from '../proposal-info-label';
import type { ReactNode } from 'react';
import type { ProposalInfoLabelVariant } from '../proposal-info-label';
import { type ReactNode } from 'react';
import { type ProposalInfoLabelVariant } from '../proposal-info-label';
import { type Proposal } from '../../types';
export const CurrentProposalState = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
export const CurrentProposalState = ({ proposal }: { proposal: Proposal }) => {
const { t } = useTranslation();
let proposalStatus: ReactNode;
let variant = 'tertiary' as ProposalInfoLabelVariant;
@@ -1,272 +0,0 @@
import type { MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react';
import { ProposalRejectionReason, ProposalState } from '@vegaprotocol/types';
import type { NetworkParamsQuery } from '@vegaprotocol/network-parameters';
import { NetworkParamsDocument } from '@vegaprotocol/network-parameters';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { CurrentProposalStatus } from './current-proposal-status';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
const networkParamsQueryMock: MockedResponse<NetworkParamsQuery> = {
request: {
query: NetworkParamsDocument,
},
result: {
data: {
networkParametersConnection: {
edges: [
{
node: {
__typename: 'NetworkParameter',
key: 'governance.proposal.updateNetParam.requiredMajority',
value: '0.00000001',
},
},
{
node: {
__typename: 'NetworkParameter',
key: 'governance.proposal.updateNetParam.requiredParticipation',
value: '0.000000001',
},
},
],
},
},
},
};
const renderComponent = ({
proposal,
}: {
proposal: ProposalQuery['proposal'];
}) => {
render(
<AppStateProvider>
<MockedProvider mocks={[networkParamsQueryMock]}>
<CurrentProposalStatus proposal={proposal} />
</MockedProvider>
</AppStateProvider>
);
};
beforeEach(() => {
jest.useFakeTimers();
jest.setSystemTime(60 * 60 * 1000);
});
afterEach(() => {
jest.useRealTimers();
});
it('Proposal open - renders will fail state if the proposal will fail', async () => {
const failedProposal = generateProposal({
votes: {
__typename: 'ProposalVotes',
yes: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
no: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
},
});
renderComponent({ proposal: failedProposal });
expect(await screen.findByText('Currently expected to')).toBeInTheDocument();
expect(await screen.findByText('fail.')).toBeInTheDocument();
});
it('Proposal open - renders will pass state if the proposal will pass', async () => {
const proposal = generateProposal();
renderComponent({ proposal });
expect(await screen.findByText('Currently expected to')).toBeInTheDocument();
expect(await screen.findByText('pass.')).toBeInTheDocument();
});
it('Proposal enacted - renders vote passed and time since enactment', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_ENACTED,
terms: {
enactmentDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(await screen.findByText('Vote passed.')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal passed - renders vote passed and time since vote closed', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
closingDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(await screen.findByText('Vote passed.')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal waiting for node vote - will pass - renders if the vote will pass and status', async () => {
const failedProposal = generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
votes: {
__typename: 'ProposalVotes',
yes: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
no: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
},
});
renderComponent({ proposal: failedProposal });
expect(
await screen.findByText('Waiting for nodes to validate asset.')
).toBeInTheDocument();
expect(await screen.findByText('Currently expected to')).toBeInTheDocument();
expect(await screen.findByText('fail.')).toBeInTheDocument();
});
it('Proposal waiting for node vote - will fail - renders if the vote will pass and status', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
});
renderComponent({ proposal });
expect(
await screen.findByText('Waiting for nodes to validate asset.')
).toBeInTheDocument();
expect(await screen.findByText('Currently expected to')).toBeInTheDocument();
expect(await screen.findByText('pass.')).toBeInTheDocument();
});
it('Proposal failed - renders vote failed reason and vote closed ago', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
errorDetails: 'foo',
terms: {
closingDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(await screen.findByText('foo')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal failed - renders rejection reason there are no error details', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
rejectionReason: ProposalRejectionReason.PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE,
terms: {
closingDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(
await screen.findByText('PROPOSAL_ERROR_CLOSE_TIME_TOO_LATE')
).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal failed - renders unknown reason if there are no error details or rejection reason', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
terms: {
closingDatetime: new Date(0).toISOString(),
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(await screen.findByText('unknown reason')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal failed - renders participation not met if participation is not met', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
terms: {
closingDatetime: new Date(0).toISOString(),
},
votes: {
__typename: 'ProposalVotes',
yes: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
no: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(await screen.findByText('Participation not met')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
it('Proposal failed - renders majority not met if majority is not met', async () => {
const proposal = generateProposal({
state: ProposalState.STATE_FAILED,
terms: {
closingDatetime: new Date(0).toISOString(),
},
votes: {
__typename: 'ProposalVotes',
yes: {
__typename: 'ProposalVoteSide',
totalNumber: '0',
totalTokens: '0',
totalEquityLikeShareWeight: '0',
},
no: {
__typename: 'ProposalVoteSide',
totalNumber: '1',
totalTokens: '25242474195500835440000',
totalEquityLikeShareWeight: '0',
},
},
});
renderComponent({ proposal });
expect(
await screen.findByText('Vote closed. Failed due to:')
).toBeInTheDocument();
expect(await screen.findByText('Majority not met')).toBeInTheDocument();
expect(await screen.findByText('about 1 hour ago')).toBeInTheDocument();
});
@@ -1,143 +0,0 @@
import type { ReactNode } from 'react';
import { formatDistanceToNow } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { ProposalState } from '@vegaprotocol/types';
import { useVoteInformation } from '../../hooks';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
export const StatusPass = ({ children }: { children: ReactNode }) => (
<span className="text-vega-green">{children}</span>
);
export const StatusFail = ({ children }: { children: ReactNode }) => (
<span className="text-danger">{children}</span>
);
const WillPass = ({
willPass,
children,
}: {
willPass: boolean;
children?: ReactNode;
}) => {
const { t } = useTranslation();
if (willPass) {
return (
<>
{children}
<StatusPass>{t('pass')}.</StatusPass>
<span className="ml-2">{t('finalOutcomeMayDiffer')}</span>
</>
);
} else {
return (
<>
{children}
<StatusFail>{t('fail')}.</StatusFail>
<span className="ml-2">{t('finalOutcomeMayDiffer')}</span>
</>
);
}
};
export const CurrentProposalStatus = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const { willPassByTokenVote, majorityMet, participationMet } =
useVoteInformation({
proposal,
});
const { t } = useTranslation();
const daysClosedAgo = formatDistanceToNow(
new Date(proposal?.terms.closingDatetime),
{ addSuffix: true }
);
const daysEnactedAgo =
proposal?.terms.enactmentDatetime &&
formatDistanceToNow(new Date(proposal.terms.enactmentDatetime), {
addSuffix: true,
});
if (proposal?.state === ProposalState.STATE_OPEN) {
return (
<WillPass willPass={willPassByTokenVote}>{t('currentlySetTo')}</WillPass>
);
}
if (
proposal?.state === ProposalState.STATE_FAILED ||
proposal?.state === ProposalState.STATE_DECLINED ||
proposal?.state === ProposalState.STATE_REJECTED
) {
if (!participationMet) {
return (
<>
<span>{t('voteFailedReason')}</span>
<StatusFail>{t('participationNotMet')}</StatusFail>
<span>&nbsp;{daysClosedAgo}</span>
</>
);
}
if (!majorityMet) {
return (
<>
<span>{t('voteFailedReason')}</span>
<StatusFail>{t('majorityNotMet')}</StatusFail>
<span>&nbsp;{daysClosedAgo}</span>
</>
);
}
return (
<>
<span>{t('voteFailedReason')}</span>
<StatusFail>
{proposal?.errorDetails ||
proposal?.rejectionReason ||
t('unknownReason')}
</StatusFail>
<span>&nbsp;{daysClosedAgo}</span>
</>
);
}
if (
proposal?.state === ProposalState.STATE_ENACTED ||
proposal?.state === ProposalState.STATE_PASSED
) {
return (
<>
<span>{t('votePassed')}</span>
<StatusPass>
&nbsp;
{proposal?.state === ProposalState.STATE_ENACTED
? t('Enacted')
: t('Passed')}
</StatusPass>
<span>
&nbsp;
{proposal?.state === ProposalState.STATE_ENACTED
? daysEnactedAgo
: daysClosedAgo}
</span>
</>
);
}
if (proposal?.state === ProposalState.STATE_WAITING_FOR_NODE_VOTE) {
return (
<WillPass willPass={willPassByTokenVote}>
<span>{t('WaitingForNodeVote')}</span>{' '}
<span>{t('currentlySetTo')}</span>
</WillPass>
);
}
return null;
};
@@ -1 +0,0 @@
export { CurrentProposalStatus } from './current-proposal-status';
@@ -6,11 +6,10 @@ import {
KeyValueTableRow,
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type Proposal } from '../../types';
interface ProposalChangeTableProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
}
export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
@@ -23,8 +23,8 @@ import { useFeatureFlags } from '@vegaprotocol/environment';
import { BrowserRouter } from 'react-router-dom';
import { VoteState } from '../vote-details/use-user-vote';
import { useNewTransferProposalDetails } from '@vegaprotocol/proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import { type MockedResponse } from '@apollo/client/testing';
import { type Proposal } from '../../types';
jest.mock('@vegaprotocol/proposals', () => ({
...jest.requireActual('@vegaprotocol/proposals'),
@@ -36,7 +36,7 @@ jest.mock('@vegaprotocol/proposals', () => ({
}));
const renderComponent = (
proposal: ProposalQuery['proposal'],
proposal: Proposal,
isListItem = true,
mocks: MockedResponse[] = [],
voteState?: VoteState
@@ -64,6 +64,7 @@ describe('Proposal header', () => {
it('Renders New market proposal', () => {
useFeatureFlags.setState({ flags: { SUCCESSOR_MARKETS: true } });
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New some market',
@@ -102,6 +103,7 @@ describe('Proposal header', () => {
it('Renders Update market proposal', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New market id',
@@ -124,12 +126,13 @@ describe('Proposal header', () => {
screen.queryByTestId('proposal-description')
).not.toBeInTheDocument();
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Update to market ID: MarketId'
'Market change: MarketId'
);
});
it('Renders New asset proposal - ERC20', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'New asset: Fake currency',
@@ -159,6 +162,7 @@ describe('Proposal header', () => {
it('Renders New asset proposal - BuiltInAsset', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
terms: {
change: {
@@ -184,6 +188,7 @@ describe('Proposal header', () => {
it('Renders Update network', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
rationale: {
title: 'Network parameter',
@@ -213,6 +218,7 @@ describe('Proposal header', () => {
it('Renders Freeform proposal - short rationale', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
id: 'short',
rationale: {
@@ -234,6 +240,7 @@ describe('Proposal header', () => {
it('Renders Freeform proposal - long rationale (105 chars) - listing', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
id: 'long',
rationale: {
@@ -259,6 +266,7 @@ describe('Proposal header', () => {
// Remove once proposals have rationale and re-enable above tests
it('Renders Freeform proposal - id for title', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
id: 'freeform id',
rationale: {
@@ -280,6 +288,7 @@ describe('Proposal header', () => {
it('Renders asset change proposal header', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
terms: {
change: {
@@ -297,6 +306,7 @@ describe('Proposal header', () => {
it("Renders unknown proposal if it's a different proposal type", () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
terms: {
change: {
@@ -313,6 +323,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Enacted', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_ENACTED,
terms: {
@@ -325,6 +336,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Passed', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_PASSED,
terms: {
@@ -338,6 +350,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Waiting for node vote', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
terms: {
@@ -352,6 +365,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Open', () => {
renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({
state: ProposalState.STATE_OPEN,
votes: {
@@ -1,15 +1,8 @@
import { useTranslation } from 'react-i18next';
import {
CopyWithTooltip,
Lozenge,
Tooltip,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { Lozenge, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { shorten } from '@vegaprotocol/utils';
import { Heading, SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type ReactNode } from 'react';
import { truncateMiddle } from '../../../../lib/truncate-middle';
import { CurrentProposalState } from '../current-proposal-state';
import { ProposalInfoLabel } from '../proposal-info-label';
@@ -18,24 +11,20 @@ import {
useNewTransferProposalDetails,
useSuccessorMarketProposalDetails,
} from '@vegaprotocol/proposals';
import {
CONSOLE_MARKET_PAGE,
DApp,
useFeatureFlags,
useLinks,
} from '@vegaprotocol/environment';
import { useFeatureFlags } from '@vegaprotocol/environment';
import Routes from '../../../routes';
import { Link } from 'react-router-dom';
import type { VoteState } from '../vote-details/use-user-vote';
import { type VoteState } from '../vote-details/use-user-vote';
import { VoteBreakdown } from '../vote-breakdown';
import { GovernanceTransferKindMapping } from '@vegaprotocol/types';
import { type Proposal } from '../../types';
export const ProposalHeader = ({
proposal,
isListItem = true,
voteState,
}: {
proposal: ProposalQuery['proposal'];
proposal: Proposal;
isListItem?: boolean;
voteState?: VoteState | null;
}) => {
@@ -43,8 +32,6 @@ export const ProposalHeader = ({
const { t } = useTranslation();
const change = proposal?.terms.change;
const consoleLink = useLinks(DApp.Console);
let details: ReactNode;
let proposalType = '';
let fallbackTitle = '';
@@ -53,7 +40,7 @@ export const ProposalHeader = ({
const titleContent = shorten(title ?? '', 100);
const getAsset = (proposal: ProposalQuery['proposal']) => {
const getAsset = (proposal: Proposal) => {
const terms = proposal?.terms;
if (
terms?.change.__typename === 'NewMarket' &&
@@ -119,33 +106,8 @@ export const ProposalHeader = ({
fallbackTitle = t('UpdateMarketProposal');
details = (
<>
<span>{t('UpdateToMarket')}:</span>{' '}
<span className="inline-flex items-start gap-2">
<span className="break-all">{change.marketId} </span>
<span className="inline-flex items-end gap-0">
<CopyWithTooltip
text={change.marketId}
description={t('copyToClipboard')}
>
<button className="inline-block px-1">
<VegaIcon size={20} name={VegaIconNames.COPY} />
</button>
</CopyWithTooltip>
<Tooltip description={t('OpenInConsole')} align="center">
<button
className="inline-block px-1"
onClick={() => {
const marketPageLink = consoleLink(
CONSOLE_MARKET_PAGE.replace(':marketId', change.marketId)
);
window.open(marketPageLink, '_blank');
}}
>
<VegaIcon size={20} name={VegaIconNames.OPEN_EXTERNAL} />
</button>
</Tooltip>
</span>
</span>
<span>{t('MarketChange')}:</span>{' '}
<span>{truncateMiddle(change.marketId)}</span>
</>
);
break;
@@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
@@ -14,9 +13,10 @@ import {
} from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { type Proposal } from '../../types';
interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal'];
proposal: Proposal | null;
}
export const formatEndOfProgramTimestamp = (value: string) => {
@@ -1,6 +1,4 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import { useCancelTransferProposalDetails } from '@vegaprotocol/proposals';
import {
KeyValueTable,
@@ -8,11 +6,12 @@ import {
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading';
import { type Proposal } from '../../types';
export const ProposalCancelTransferDetails = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
}) => {
const { t } = useTranslation();
const details = useCancelTransferProposalDetails(proposal?.id);
@@ -1,6 +1,4 @@
import { useState } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import { useTranslation } from 'react-i18next';
@@ -21,11 +19,12 @@ import {
addDecimalsFormatNumberQuantum,
formatDateWithLocalTimezone,
} from '@vegaprotocol/utils';
import { type Proposal } from '../../types';
export const ProposalTransferDetails = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
}) => {
const { t } = useTranslation();
const [show, setShow] = useState(false);
@@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
@@ -12,6 +11,7 @@ import {
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { type Proposal } from '../../types';
// These types are not generated as it's not known how dynamic these are
type VestingBenefitTier = {
@@ -43,7 +43,7 @@ export const formatVolumeDiscountFactor = (value: string) => {
};
interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal'];
proposal: Proposal | null;
}
/**
@@ -5,13 +5,13 @@ import {
RoundedWrapper,
} from '@vegaprotocol/ui-toolkit';
import { Row } from '@vegaprotocol/markets';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading';
import { type Proposal } from '../../types';
interface ProposalUpdateMarketStateProps {
proposal: ProposalQuery['proposal'];
proposal: Proposal | null;
}
export const ProposalUpdateMarketState = ({
@@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import {
KeyValueTable,
KeyValueTableRow,
@@ -12,9 +11,10 @@ import {
} from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js';
import { type Proposal } from '../../types';
interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal'];
proposal: Proposal | null;
}
export const formatVolumeDiscountFactor = (value: string) => {
@@ -1,13 +1,13 @@
import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
import type { VegaWalletConfig } from '@vegaprotocol/wallet';
import { type VegaWalletConfig } from '@vegaprotocol/wallet';
import { render, screen } from '@testing-library/react';
import { generateProposal } from '../../test-helpers/generate-proposals';
import { Proposal } from './proposal';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalState } from '@vegaprotocol/types';
import { mockNetworkParams } from '../../test-helpers/mocks';
import { type Proposal as IProposal } from '../../types';
jest.mock('@vegaprotocol/network-parameters', () => ({
...jest.requireActual('@vegaprotocol/network-parameters'),
@@ -50,14 +50,14 @@ const vegaWalletConfig: VegaWalletConfig = {
},
};
const renderComponent = (proposal: ProposalQuery['proposal']) => {
const renderComponent = (proposal: IProposal) => {
render(
<MemoryRouter>
<MockedProvider>
<VegaWalletProvider config={vegaWalletConfig}>
<Proposal
restData={{}}
proposal={proposal as ProposalQuery['proposal']}
proposal={proposal}
networkParams={mockNetworkParams}
/>
</VegaWalletProvider>
@@ -12,14 +12,13 @@ import { UserVote } from '../vote-details';
import { ListAsset } from '../list-asset';
import Routes from '../../../routes';
import { ProposalMarketData } from '../proposal-market-data';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MarketInfo } from '@vegaprotocol/markets';
import type { AssetQuery } from '@vegaprotocol/assets';
import { type MarketInfo } from '@vegaprotocol/markets';
import { type AssetQuery } from '@vegaprotocol/assets';
import { removePaginationWrapper } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import { ProposalMarketChanges } from '../proposal-market-changes';
import { ProposalUpdateMarketState } from '../proposal-update-market-state';
import type { NetworkParamsResult } from '@vegaprotocol/network-parameters';
import { type NetworkParamsResult } from '@vegaprotocol/network-parameters';
import { useVoteSubmit } from '@vegaprotocol/proposals';
import { useUserVote } from '../vote-details/use-user-vote';
import {
@@ -28,9 +27,10 @@ import {
} from '../proposal-transfer';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
import { type Proposal as IProposal } from '../../types';
export interface ProposalProps {
proposal: ProposalQuery['proposal'];
proposal: IProposal;
networkParams: Partial<NetworkParamsResult>;
marketData?: MarketInfo | null;
parentMarketData?: MarketInfo | null;
@@ -1,7 +1,7 @@
import { BrowserRouter as Router } from 'react-router-dom';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { VegaWalletContext } from '@vegaprotocol/wallet';
import type { MockedResponse } from '@apollo/client/testing';
import { type MockedResponse } from '@apollo/client/testing';
import { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react';
import { format } from 'date-fns';
@@ -18,10 +18,10 @@ import {
lastWeek,
nextWeek,
} from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type Proposal } from '../../types';
const renderComponent = (
proposal: ProposalQuery['proposal'],
proposal: Proposal,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
@@ -1,21 +1,20 @@
import { type ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@vegaprotocol/ui-toolkit';
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
import { useTranslation } from 'react-i18next';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import type { ReactNode } from 'react';
import {
ProposalRejectionReasonMapping,
ProposalState,
} from '@vegaprotocol/types';
import Routes from '../../../routes';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type Proposal } from '../../types';
export const ProposalsListItemDetails = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
}) => {
const { t } = useTranslation();
const state = proposal?.state;
@@ -2,10 +2,10 @@ import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import { ProposalsListItemDetails } from './proposals-list-item-details';
import { useUserVote } from '../vote-details/use-user-vote';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type Proposal } from '../../types';
interface ProposalsListItemProps {
proposal?: ProposalQuery['proposal'] | null;
proposal?: Proposal | null;
}
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
@@ -17,8 +17,8 @@ import {
lastMonth,
nextMonth,
} from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../../types';
const openProposalClosesNextMonth = generateProposal({
id: 'proposal1',
@@ -63,7 +63,7 @@ const closedProtocolUpgradeProposal = generateProtocolUpgradeProposal({
});
const renderComponent = (
proposals: ProposalQuery['proposal'][],
proposals: Proposal[],
protocolUpgradeProposals?: ProtocolUpgradeProposalFieldsFragment[]
) => (
<Router>
@@ -10,20 +10,20 @@ import Routes from '../../../routes';
import { Button, Toggle } from '@vegaprotocol/ui-toolkit';
import { Link } from 'react-router-dom';
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { ExternalLinks } from '@vegaprotocol/environment';
import { type ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../../types';
interface ProposalsListProps {
proposals: Array<ProposalQuery['proposal']>;
proposals: Proposal[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
lastBlockHeight?: string;
}
interface SortedProposalsProps {
open: ProposalQuery['proposal'][];
closed: ProposalQuery['proposal'][];
open: Proposal[];
closed: Proposal[];
}
interface SortedProtocolUpgradeProposalsProps {
@@ -31,7 +31,7 @@ interface SortedProtocolUpgradeProposalsProps {
closed: ProtocolUpgradeProposalFieldsFragment[];
}
export const orderByDate = (arr: ProposalQuery['proposal'][]) =>
export const orderByDate = (arr: Proposal[]) =>
orderBy(
arr,
[
@@ -91,14 +91,10 @@ export const ProposalsList = ({
);
return {
open:
initialSorting.open.length > 0
? orderByDate(initialSorting.open as ProposalQuery['proposal'][])
: [],
initialSorting.open.length > 0 ? orderByDate(initialSorting.open) : [],
closed:
initialSorting.closed.length > 0
? orderByDate(
initialSorting.closed as ProposalQuery['proposal'][]
).reverse()
? orderByDate(initialSorting.closed).reverse()
: [],
};
}, [proposals]);
@@ -125,9 +121,7 @@ export const ProposalsList = ({
};
}, [protocolUpgradeProposals, lastBlockHeight]);
const filterPredicate = (
p: ProposalFieldsFragment | ProposalQuery['proposal']
) =>
const filterPredicate = (p: ProposalFieldsFragment | Proposal) =>
p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString);
@@ -12,7 +12,7 @@ import {
nextWeek,
lastMonth,
} from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type Proposal } from '../../types';
const rejectedProposalClosesNextWeek = generateProposal({
id: 'rejected1',
@@ -35,7 +35,7 @@ const rejectedProposalClosedLastMonth = generateProposal({
},
});
const renderComponent = (proposals: ProposalQuery['proposal'][]) => (
const renderComponent = (proposals: Proposal[]) => (
<Router>
<MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider>
@@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next';
import { Heading } from '../../../../components/heading';
import { ProposalsListItem } from '../proposals-list-item';
import { ProposalsListFilter } from '../proposals-list-filter';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { type Proposal } from '../../types';
interface ProposalsListProps {
proposals: ProposalQuery['proposal'][];
proposals: Proposal[];
}
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
const { t } = useTranslation();
const [filterString, setFilterString] = useState('');
const filterPredicate = (p: ProposalQuery['proposal']) =>
const filterPredicate = (p: Proposal) =>
p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString);
@@ -9,8 +9,7 @@ import {
nextWeek,
} from '../../test-helpers/mocks';
import { CompactVotes, VoteBreakdown } from './vote-breakdown';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { MockedResponse } from '@apollo/client/testing';
import { type MockedResponse } from '@apollo/client/testing';
import {
generateNoVotes,
generateProposal,
@@ -18,7 +17,8 @@ import {
} from '../../test-helpers/generate-proposals';
import { ProposalState } from '@vegaprotocol/types';
import { BigNumber } from '../../../../lib/bignumber';
import type { AppState } from '../../../../contexts/app-state/app-state-context';
import { type AppState } from '../../../../contexts/app-state/app-state-context';
import { type Proposal } from '../../types';
const mockTotalSupply = new BigNumber(100);
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :)
@@ -41,7 +41,7 @@ jest.mock('../../../../contexts/app-state/app-state-context', () => ({
}));
const renderComponent = (
proposal: ProposalQuery['proposal'],
proposal: Proposal,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) =>
@@ -1,3 +1,4 @@
import { type ReactNode } from 'react';
import classNames from 'classnames';
import BigNumber from 'bignumber.js';
import { useTranslation } from 'react-i18next';
@@ -5,10 +6,8 @@ import { useVoteInformation } from '../../hooks';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types';
import type { ReactNode } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { CompactNumber } from '@vegaprotocol/react-helpers';
import { type Proposal } from '../../types';
export const CompactVotes = ({ number }: { number: BigNumber }) => (
<CompactNumber
@@ -20,7 +19,7 @@ export const CompactVotes = ({ number }: { number: BigNumber }) => (
);
interface VoteBreakdownProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
}
interface VoteProgressProps {
@@ -5,14 +5,13 @@ import { ProposalState } from '@vegaprotocol/types';
import { ConnectToVega } from '../../../../components/connect-to-vega';
import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading';
import type { VoteValue } from '@vegaprotocol/types';
import type { DialogProps, VegaTxState } from '@vegaprotocol/proposals';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import type { VoteState } from './use-user-vote';
import { type VoteValue } from '@vegaprotocol/types';
import { type DialogProps, type VegaTxState } from '@vegaprotocol/proposals';
import { type VoteState } from './use-user-vote';
import { type Proposal } from '../../types';
interface UserVoteProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined;
transaction: VegaTxState | null;
@@ -3,13 +3,12 @@ import {
useNetworkParams,
} from '@vegaprotocol/network-parameters';
import { BigNumber } from '../../../lib/bignumber';
import type { ProposalFieldsFragment } from '../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../proposal/__generated__/Proposal';
import { type Proposal } from '../types';
export const useProposalNetworkParams = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
proposal: Proposal;
}) => {
const { params } = useNetworkParams([
NetworkParams.governance_proposal_updateMarket_requiredMajority,
@@ -2,15 +2,10 @@ import { useMemo } from 'react';
import { useAppState } from '../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../lib/bignumber';
import { useProposalNetworkParams } from './use-proposal-network-params';
import type { ProposalFieldsFragment } from '../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../proposal/__generated__/Proposal';
import { addDecimal } from '@vegaprotocol/utils';
import { type Proposal } from '../types';
export const useVoteInformation = ({
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
const {
appState: { totalSupply, decimals },
} = useAppState();
@@ -86,228 +86,65 @@ query Proposal(
$includeUpdateReferralProgram: Boolean!
) {
proposal(id: $proposalId) {
id
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
... on Proposal {
id
}
errorDetails
...NewMarketProductField @include(if: $includeNewMarketProductField)
...UpdateMarketState @include(if: $includeUpdateMarketState)
...UpdateReferralProgram @include(if: $includeUpdateReferralProgram)
...UpdateVolumeDiscountProgram
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
decimalPlaces
metadata
riskParameters {
... on LogNormalRiskModel {
riskAversionParameter
tau
params {
mu
r
sigma
}
}
... on SimpleRiskModel {
params {
factorLong
factorShort
}
}
}
instrument {
name
code
product {
... on FutureProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
quoteName
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id
}
errorDetails
...NewMarketProductField @include(if: $includeNewMarketProductField)
...UpdateMarketState @include(if: $includeUpdateMarketState)
...UpdateReferralProgram @include(if: $includeUpdateReferralProgram)
...UpdateVolumeDiscountProgram
terms {
closingDatetime
enactmentDatetime
change {
... on NewMarket {
decimalPlaces
metadata
riskParameters {
... on LogNormalRiskModel {
riskAversionParameter
tau
params {
mu
r
sigma
}
}
... on PerpetualProduct {
settlementAsset {
id
name
symbol
decimals
quantum
... on SimpleRiskModel {
params {
factorLong
factorShort
}
quoteName
}
}
}
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
targetStakeParameters {
timeWindow
scalingFactor
}
}
positionDecimalPlaces
linearSlippageFactor
}
... on UpdateMarket {
marketId
updateMarketConfiguration {
instrument {
name
code
product {
... on UpdateFutureProduct {
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
... on FutureProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
# dataSourceSpecForTradingTermination {
# sourceType {
# ... on DataSourceDefinitionInternal {
# sourceType {
# ... on DataSourceSpecConfigurationTime {
# conditions {
# operator
# value
# }
# }
# }
# }
# ... on DataSourceDefinitionExternal {
# sourceType {
# ... on DataSourceSpecConfiguration {
# signers {
# signer {
# ... on PubKey {
# key
# }
# ... on ETHAddress {
# address
# }
# }
# }
# filters {
# key {
# name
# type
# }
# conditions {
# operator
# value
# }
# }
# }
# }
# }
# }
# }
quoteName
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
}
... on UpdatePerpetualProduct {
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
@@ -348,14 +185,19 @@ query Proposal(
}
}
}
dataSourceSpecBinding {
settlementDataProperty
settlementScheduleProperty
}
... on PerpetualProduct {
settlementAsset {
id
name
symbol
decimals
quantum
}
quoteName
}
}
}
metadata
priceMonitoringParameters {
triggers {
horizonSecs
@@ -369,71 +211,232 @@ query Proposal(
scalingFactor
}
}
riskParameters {
... on UpdateMarketSimpleRiskModel {
simple {
factorLong
factorShort
positionDecimalPlaces
linearSlippageFactor
quadraticSlippageFactor
}
... on UpdateMarket {
marketId
updateMarketConfiguration {
instrument {
code
product {
... on UpdateFutureProduct {
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
}
# dataSourceSpecForTradingTermination {
# sourceType {
# ... on DataSourceDefinitionInternal {
# sourceType {
# ... on DataSourceSpecConfigurationTime {
# conditions {
# operator
# value
# }
# }
# }
# }
# ... on DataSourceDefinitionExternal {
# sourceType {
# ... on DataSourceSpecConfiguration {
# signers {
# signer {
# ... on PubKey {
# key
# }
# ... on ETHAddress {
# address
# }
# }
# }
# filters {
# key {
# name
# type
# }
# conditions {
# operator
# value
# }
# }
# }
# }
# }
# }
# }
dataSourceSpecBinding {
settlementDataProperty
tradingTerminationProperty
}
}
... on UpdatePerpetualProduct {
quoteName
dataSourceSpecForSettlementData {
sourceType {
... on DataSourceDefinitionInternal {
sourceType {
... on DataSourceSpecConfigurationTime {
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
}
dataSourceSpecBinding {
settlementDataProperty
settlementScheduleProperty
}
}
}
}
... on UpdateMarketLogNormalRiskModel {
logNormal {
riskAversionParameter
tau
params {
r
sigma
mu
metadata
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
targetStakeParameters {
timeWindow
scalingFactor
}
}
riskParameters {
... on UpdateMarketSimpleRiskModel {
simple {
factorLong
factorShort
}
}
... on UpdateMarketLogNormalRiskModel {
logNormal {
riskAversionParameter
tau
params {
r
sigma
mu
}
}
}
}
}
}
}
... on NewAsset {
name
symbol
decimals
quantum
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
lifetimeLimit
withdrawThreshold
... on NewAsset {
name
symbol
decimals
quantum
source {
... on BuiltinAsset {
maxFaucetAmountMint
}
... on ERC20 {
contractAddress
lifetimeLimit
withdrawThreshold
}
}
}
}
... on UpdateNetworkParameter {
networkParameter {
key
value
... on UpdateNetworkParameter {
networkParameter {
key
value
}
}
}
... on UpdateAsset {
quantum
assetId
source {
... on UpdateERC20 {
lifetimeLimit
withdrawThreshold
... on UpdateAsset {
quantum
assetId
source {
... on UpdateERC20 {
lifetimeLimit
withdrawThreshold
}
}
}
}
}
}
votes {
yes {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
no {
totalTokens
totalNumber
totalEquityLikeShareWeight
votes {
yes {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
no {
totalTokens
totalNumber
totalEquityLikeShareWeight
}
}
}
}
File diff suppressed because one or more lines are too long
@@ -17,6 +17,7 @@ import {
import { useParentMarketIdQuery } from '@vegaprotocol/markets';
import { useFeatureFlags } from '@vegaprotocol/environment';
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
import { type Proposal as IProposal } from '../types';
export const ProposalContainer = () => {
const featureFlags = useFeatureFlags((state) => state.flags);
@@ -67,6 +68,8 @@ export const ProposalContainer = () => {
skip: !params.proposalId,
});
const proposal = data?.proposal as IProposal;
const successor = useSuccessorMarketProposalDetails(params.proposalId);
const isSuccessor = !!successor?.parentMarketId || !!successor.code;
@@ -79,12 +82,12 @@ export const ProposalContainer = () => {
},
} = useFetch(
`${ENV.rest}governance?proposalId=${
data?.proposal?.terms.change.__typename === 'UpdateMarket' &&
data?.proposal.terms.change.marketId
proposal?.terms.change.__typename === 'UpdateMarket' &&
proposal.terms.change.marketId
}`,
undefined,
true,
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
proposal?.terms.change.__typename !== 'UpdateMarket'
);
const {
@@ -97,7 +100,7 @@ export const ProposalContainer = () => {
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
undefined,
true,
data?.proposal?.terms.change.__typename !== 'UpdateMarket'
proposal?.terms.change.__typename !== 'UpdateMarket'
);
const {
@@ -108,8 +111,8 @@ export const ProposalContainer = () => {
dataProvider: marketInfoProvider,
skipUpdates: true,
variables: {
marketId: data?.proposal?.id || '',
skip: !data?.proposal?.id,
marketId: proposal?.id || '',
skip: !proposal?.id,
},
});
@@ -148,23 +151,22 @@ export const ProposalContainer = () => {
fetchPolicy: 'network-only',
variables: {
assetId:
(data?.proposal?.terms.change.__typename === 'NewAsset' &&
data?.proposal?.id) ||
(data?.proposal?.terms.change.__typename === 'UpdateAsset' &&
data.proposal.terms.change.assetId) ||
(proposal?.terms.change.__typename === 'NewAsset' && proposal?.id) ||
(proposal?.terms.change.__typename === 'UpdateAsset' &&
proposal.terms.change.assetId) ||
'',
},
skip: !['NewAsset', 'UpdateAsset'].includes(
data?.proposal?.terms?.change?.__typename || ''
proposal?.terms?.change?.__typename || ''
),
});
useEffect(() => {
if (
previouslyEnactedMarketProposalsRestData &&
data?.proposal?.terms.change.__typename === 'UpdateMarket'
proposal?.terms.change.__typename === 'UpdateMarket'
) {
const change = data?.proposal?.terms?.change as { marketId: string };
const change = proposal?.terms?.change as { marketId: string };
const filteredProposals =
// @ts-ignore rest data is not typed
@@ -188,8 +190,8 @@ export const ProposalContainer = () => {
}, [
previouslyEnactedMarketProposalsRestData,
params.proposalId,
data?.proposal?.terms.change.__typename,
data?.proposal?.terms.change,
proposal?.terms.change.__typename,
proposal?.terms.change,
]);
useEffect(() => {
@@ -242,7 +244,7 @@ export const ProposalContainer = () => {
>
{data?.proposal ? (
<Proposal
proposal={data.proposal}
proposal={proposal}
networkParams={networkParams}
restData={restData}
marketData={marketData}
@@ -8,6 +8,7 @@ import mergeWith from 'lodash/mergeWith';
import { type PartialDeep } from 'type-fest';
import { type ProposalQuery } from '../proposal/__generated__/Proposal';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../types';
export function generateProtocolUpgradeProposal(
override: PartialDeep<ProtocolUpgradeProposalFieldsFragment> = {}
@@ -43,8 +44,8 @@ export function generateProtocolUpgradeProposal(
}
export function generateProposal(
override: PartialDeep<ProposalQuery['proposal']> = {}
): ProposalQuery['proposal'] {
override: PartialDeep<Proposal> = {}
): Proposal {
const defaultProposal: ProposalQuery['proposal'] = {
__typename: 'Proposal',
id: faker.datatype.uuid(),
@@ -92,15 +93,16 @@ export function generateProposal(
},
};
return mergeWith<
ProposalQuery['proposal'],
PartialDeep<ProposalQuery['proposal']>
>(defaultProposal, override, (objValue, srcValue) => {
if (!isArray(objValue)) {
return;
return mergeWith<Proposal, PartialDeep<Proposal>>(
defaultProposal,
override,
(objValue, srcValue) => {
if (!isArray(objValue)) {
return;
}
return srcValue;
}
return srcValue;
});
);
}
type Vote = Pick<Schema.Vote, '__typename' | 'value' | 'party' | 'datetime'>;
@@ -0,0 +1,11 @@
import type { ProposalQuery } from './proposal/__generated__/Proposal';
/**
* The default Proposal type needs extracting from the ProposalNode union type
* as lots of fields on the original type don't exist on BatchProposal. Eventually
* we will support BatchProposal but for now we don't
*/
export type Proposal = Extract<
ProposalQuery['proposal'],
{ __typename?: 'Proposal' }
>;
+2 -7
View File
@@ -22,12 +22,7 @@ NX_WALLETCONNECT_PROJECT_ID=fe8091dc35738863e509fc4947525c72
NX_SUCCESSOR_MARKETS=true
NX_STOP_ORDERS=true
NX_ICEBERG_ORDERS=true
# NX_PRODUCT_PERPETUALS
NX_METAMASK_SNAPS=true
NX_REFERRALS=true
# NX_DISABLE_CLOSE_POSITION=false
NX_TENDERMINT_URL=https://be.vega.community
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
NX_TEAM_COMPETITION=true
@@ -0,0 +1,329 @@
import { useSearchParams } from 'react-router-dom';
import { Controller, useForm } from 'react-hook-form';
import {
Intent,
TextArea,
TradingAnchorButton,
TradingButton,
TradingCheckbox,
TradingFormGroup,
TradingInput,
TradingInputError,
} from '@vegaprotocol/ui-toolkit';
import {
useVegaWallet,
type CreateReferralSet,
type Status,
useVegaWalletDialogStore,
} from '@vegaprotocol/wallet';
import {
addDecimalsFormatNumber,
isValidVegaPublicKey,
URL_REGEX,
} from '@vegaprotocol/utils';
import { useT } from '../../lib/use-t';
import { useCreateReferralSet } from '../../lib/hooks/use-create-referral-set';
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';
interface FormFields {
name: string;
url: string;
avatarUrl: string;
private: boolean;
allowList: string;
}
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 } =
useCreateReferralSet({
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 (
<CreateTeamForm
onSubmit={onSubmit}
status={status}
err={err}
isSolo={isSolo}
/>
);
};
const CreateTeamForm = ({
status,
err,
isSolo,
onSubmit,
}: {
status: ReturnType<typeof useCreateReferralSet>['status'];
err: ReturnType<typeof useCreateReferralSet>['err'];
isSolo: boolean;
onSubmit: (tx: CreateReferralSet) => void;
}) => {
const t = useT();
const {
register,
handleSubmit,
control,
watch,
formState: { errors },
} = useForm<FormFields>({
defaultValues: {
private: isSolo,
},
});
const isPrivate = watch('private');
const createTeam = (fields: FormFields) => {
onSubmit({
createReferralSet: {
isTeam: true,
team: {
name: fields.name,
teamUrl: fields.url,
avatarUrl: fields.avatarUrl,
closed: fields.private,
allowList: fields.private ? parseAllowListText(fields.allowList) : [],
},
},
});
};
return (
<form onSubmit={handleSubmit(createTeam)}>
<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 status={status} />
</form>
);
};
const SubmitButton = ({ status }: { status: Status }) => {
const t = useT();
const disabled = status === 'pending' || status === 'requested';
let text = t('Create');
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,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 './hooks/use-games';
import { useCurrentEpochInfoQuery } from '../referrals/hooks/__generated__/Epoch';
import { Link, useNavigate } from 'react-router-dom';
import { Links } from '../../lib/links';
import {
CompetitionsAction,
CompetitionsActionsContainer,
} from '../../components/competitions/competitions-cta';
import { GamesContainer } from '../../components/competitions/games-container';
import { CompetitionsLeaderboard } from '../../components/competitions/competitions-leaderboard';
import { useTeams } from './hooks/use-teams';
import take from 'lodash/take';
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>
);
};
@@ -0,0 +1,382 @@
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 {
Pill,
VegaIcon,
VegaIconNames,
Tooltip,
Splash,
truncateMiddle,
Loader,
} from '@vegaprotocol/ui-toolkit';
import classNames from 'classnames';
import { useT } from '../../lib/use-t';
import { Table } from '../../components/table';
import { formatNumberRounded, getDateTimeFormat } from '@vegaprotocol/utils';
import {
useTeam,
type Team as TeamType,
type TeamStats,
type Member,
type TeamGame,
} from './hooks/use-team';
import { DApp, EXPLORER_PARTIES, useLinks } from '@vegaprotocol/environment';
import BigNumber from 'bignumber.js';
import { TeamAvatar } from '../../components/competitions/team-avatar';
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';
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?: TeamStats;
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">{team.name}</h1>
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
</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>
</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:block',
className: 'hidden md:block',
},
{ name: 'type', displayName: t('Type') },
{ name: 'amount', displayName: t('Amount earned') },
{
name: 'teams',
displayName: t('No. of participating teams'),
headerClassName: 'hidden md:block',
className: 'hidden md:block',
},
{ name: 'status', displayName: t('Status') },
]}
data={games.map((game) => ({
rank: game.team.rank,
epoch: game.epoch,
type: game.team.rewardMetric,
amount: game.team.totalRewardsEarned,
teams: game.numberOfParticipants,
}))}
noCollapse={true}
/>
);
};
const Members = ({ members }: { members?: Member[] }) => {
const t = useT();
if (!members?.length) {
return <p>{t('No members')}</p>;
}
const data = orderBy(
members.map((m) => ({
referee: <RefereeCell pubkey={m.referee} />,
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
joinedAtEpoch: Number(m.joinedAtEpoch),
explorerLink: <RefereeLink pubkey={m.referee} />,
})),
'joinedAtEpoch',
'desc'
);
return (
<Table
columns={[
{ name: 'referee', displayName: t('Referee') },
{
name: 'joinedAt',
displayName: t('Joined at'),
},
{
name: 'joinedAtEpoch',
displayName: t('Joined epoch'),
},
{
name: 'explorerLink',
displayName: <span className="invisible">Actions</span>, // ensure header doesn't collapse
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 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,236 @@
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 './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 teamsData = [
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// '8d81a2ba54c21cbb891e46a2a8debac508b33ee83e2d7c3b8a5bcd420753515e',
// name: 'cat lovers 2000',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// 'afe0f77322c9bc9ebe36faa8b0d846617018b86db78f33cadbe09dc15c95f408',
// name: 'dog lovers 2000',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// '580e52a0ef7f706898c6e0a3a34a6f870ef2a49e191408a3e1ed2c0a92e6e588',
// name: 'we like vega',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// '38c49f91670f536ce5ab6609899780cfb6bb9dcfc6c79842cbc348325e10c10f',
// name: 'pure gold',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// 'dfaaace4de83cab2443ebd7f75dc0d9a22a4f4a38f757b3adb5269aa930e0bdc',
// name: 'diamond hands',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// '1627a2ea10b07ed9cf08bfc7818bd76716493f5e6a5b5ffe4ed856ef34602a14',
// name: 'to the moon',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// 'd21f7b04985e5b5f510a53ee61c4d77c100374eafeffd754b5296d386b7454da',
// name: 'beyond cats',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// 'a40339f4cf31f4496e4d85ac7f56d5bbc9e257a337070fa361205c1f2fdaa0eb',
// name: 'cat lovers 2000',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// 'af059e7508a302cce9523edcbde52bf7f2e876f34854c6274a83aec4045b9657',
// name: 'cat lovers 2000',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// '3f06f0e0ec863e686212d6b100f03122c9997f890d304f15f55215ac32334e6f',
// name: 'cat lovers 2000',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// {
// referrer: '12345678909876543212345678765432345676543234567',
// avatarUrl: '',
// teamUrl: 'https://vega.xyz',
// closed: true,
// teamId:
// '0fdc948a548ccd29001c2411629065bc5e248b6535b59b4b089f07a577074bf9',
// name: 'cat lovers 2000',
// totalQuantumRewards: '1234567890',
// totalGamesPlayed: 12,
// totalQuantumVolume: '1234567890',
// gamesPlayed: [],
// createdAt: 123,
// createdAtEpoch: 123,
// },
// ];
const [filter, setFilter] = useState<string | null | undefined>(undefined);
return (
<ErrorBoundary>
<CompetitionsHeader title={t('Join a team')}>
<p className="text-lg mb-1">{t('Choose a team to get involved')}</p>
</CompetitionsHeader>
<div className="mb-6 flex justify-end">
<div className="w-40 h-10 relative">
<span className="absolute z-10 pointer-events-none opacity-90 top-[5px] left-[5px]">
<VegaIcon name={VegaIconNames.SEARCH} size={18} />
</span>
<Input
ref={inputRef}
className="opacity-90 text-right"
placeholder={t('Name')}
onKeyUp={() => {
const value = inputRef.current?.value;
if (value != filter) setFilter(value);
}}
/>
</div>
</div>
<div>
{teamsLoading ? (
<Loader size="small" />
) : (
<CompetitionsLeaderboard
data={teamsData.filter((td) => {
if (filter && filter.length > 0) {
const re = new RegExp(filter, 'i');
return re.test(td.name);
}
return true;
})}
/>
)}
</div>
</ErrorBoundary>
);
};
@@ -0,0 +1,89 @@
fragment TeamFields on Team {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
}
fragment TeamStatsFields on TeamStatistics {
teamId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
quantumRewards {
epoch
total_quantum_rewards
}
gamesPlayed
}
fragment TeamRefereeFields on TeamReferee {
teamId
referee
joinedAt
joinedAtEpoch
}
fragment TeamEntity on TeamGameEntity {
rank
volume
rewardMetric
rewardEarned
totalRewardsEarned
team {
teamId
}
}
fragment TeamGameFields on Game {
id
epoch
numberOfParticipants
entities {
... on TeamGameEntity {
...TeamEntity
}
}
}
query Team($teamId: ID!, $partyId: ID) {
teams(teamId: $teamId) {
edges {
node {
...TeamFields
}
}
}
partyTeams: teams(partyId: $partyId) {
edges {
node {
...TeamFields
}
}
}
teamsStatistics(teamId: $teamId) {
edges {
node {
...TeamStatsFields
}
}
}
teamReferees(teamId: $teamId) {
edges {
node {
...TeamRefereeFields
}
}
}
games(entityScope: ENTITY_SCOPE_TEAMS) {
edges {
node {
...TeamGameFields
}
}
}
}
@@ -0,0 +1,16 @@
query Teams($teamId: ID, $partyId: ID) {
teams(teamId: $teamId, partyId: $partyId) {
edges {
node {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
}
}
}
}
@@ -0,0 +1,13 @@
query TeamsStatistics($teamId: ID, $aggregationEpochs: Int) {
teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
edges {
node {
teamId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
gamesPlayed
}
}
}
}
@@ -0,0 +1,151 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type TeamFieldsFragment = { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean };
export type TeamStatsFieldsFragment = { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> };
export type TeamRefereeFieldsFragment = { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number };
export type TeamEntityFragment = { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } };
export type TeamGameFieldsFragment = { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> };
export type TeamQueryVariables = Types.Exact<{
teamId: Types.Scalars['ID'];
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
export type TeamQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null, partyTeams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null, teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string>, quantumRewards: Array<{ __typename?: 'QuantumRewardsPerEpoch', epoch: number, total_quantum_rewards: string }> } }> } | null, teamReferees?: { __typename?: 'TeamRefereeConnection', edges: Array<{ __typename?: 'TeamRefereeEdge', node: { __typename?: 'TeamReferee', teamId: string, referee: string, joinedAt: any, joinedAtEpoch: number } }> } | null, games: { __typename?: 'GamesConnection', edges?: Array<{ __typename?: 'GameEdge', node: { __typename?: 'Game', id: string, epoch: number, numberOfParticipants: number, entities: Array<{ __typename?: 'IndividualGameEntity' } | { __typename?: 'TeamGameEntity', rank: number, volume: string, rewardMetric: string, rewardEarned: string, totalRewardsEarned: string, team: { __typename?: 'TeamParticipation', teamId: string } }> } } | null> | null } };
export const TeamFieldsFragmentDoc = gql`
fragment TeamFields on Team {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
}
`;
export const TeamStatsFieldsFragmentDoc = gql`
fragment TeamStatsFields on TeamStatistics {
teamId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
quantumRewards {
epoch
total_quantum_rewards
}
gamesPlayed
}
`;
export const TeamRefereeFieldsFragmentDoc = gql`
fragment TeamRefereeFields on TeamReferee {
teamId
referee
joinedAt
joinedAtEpoch
}
`;
export const TeamEntityFragmentDoc = gql`
fragment TeamEntity on TeamGameEntity {
rank
volume
rewardMetric
rewardEarned
totalRewardsEarned
team {
teamId
}
}
`;
export const TeamGameFieldsFragmentDoc = gql`
fragment TeamGameFields on Game {
id
epoch
numberOfParticipants
entities {
... on TeamGameEntity {
...TeamEntity
}
}
}
${TeamEntityFragmentDoc}`;
export const TeamDocument = gql`
query Team($teamId: ID!, $partyId: ID) {
teams(teamId: $teamId) {
edges {
node {
...TeamFields
}
}
}
partyTeams: teams(partyId: $partyId) {
edges {
node {
...TeamFields
}
}
}
teamsStatistics(teamId: $teamId) {
edges {
node {
...TeamStatsFields
}
}
}
teamReferees(teamId: $teamId) {
edges {
node {
...TeamRefereeFields
}
}
}
games(entityScope: ENTITY_SCOPE_TEAMS) {
edges {
node {
...TeamGameFields
}
}
}
}
${TeamFieldsFragmentDoc}
${TeamStatsFieldsFragmentDoc}
${TeamRefereeFieldsFragmentDoc}
${TeamGameFieldsFragmentDoc}`;
/**
* __useTeamQuery__
*
* To run a query within a React component, call `useTeamQuery` and pass it any options that fit your needs.
* When your component renders, `useTeamQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useTeamQuery({
* variables: {
* teamId: // value for 'teamId'
* partyId: // value for 'partyId'
* },
* });
*/
export function useTeamQuery(baseOptions: Apollo.QueryHookOptions<TeamQuery, TeamQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TeamQuery, TeamQueryVariables>(TeamDocument, options);
}
export function useTeamLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamQuery, TeamQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TeamQuery, TeamQueryVariables>(TeamDocument, options);
}
export type TeamQueryHookResult = ReturnType<typeof useTeamQuery>;
export type TeamLazyQueryHookResult = ReturnType<typeof useTeamLazyQuery>;
export type TeamQueryResult = Apollo.QueryResult<TeamQuery, TeamQueryVariables>;
@@ -0,0 +1,61 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type TeamsQueryVariables = Types.Exact<{
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
partyId?: Types.InputMaybe<Types.Scalars['ID']>;
}>;
export type TeamsQuery = { __typename?: 'Query', teams?: { __typename?: 'TeamConnection', edges: Array<{ __typename?: 'TeamEdge', node: { __typename?: 'Team', teamId: string, referrer: string, name: string, teamUrl: string, avatarUrl: string, createdAt: any, createdAtEpoch: number, closed: boolean } }> } | null };
export const TeamsDocument = gql`
query Teams($teamId: ID, $partyId: ID) {
teams(teamId: $teamId, partyId: $partyId) {
edges {
node {
teamId
referrer
name
teamUrl
avatarUrl
createdAt
createdAtEpoch
closed
}
}
}
}
`;
/**
* __useTeamsQuery__
*
* To run a query within a React component, call `useTeamsQuery` and pass it any options that fit your needs.
* When your component renders, `useTeamsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useTeamsQuery({
* variables: {
* teamId: // value for 'teamId'
* partyId: // value for 'partyId'
* },
* });
*/
export function useTeamsQuery(baseOptions?: Apollo.QueryHookOptions<TeamsQuery, TeamsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TeamsQuery, TeamsQueryVariables>(TeamsDocument, options);
}
export function useTeamsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamsQuery, TeamsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TeamsQuery, TeamsQueryVariables>(TeamsDocument, options);
}
export type TeamsQueryHookResult = ReturnType<typeof useTeamsQuery>;
export type TeamsLazyQueryHookResult = ReturnType<typeof useTeamsLazyQuery>;
export type TeamsQueryResult = Apollo.QueryResult<TeamsQuery, TeamsQueryVariables>;
@@ -0,0 +1,58 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type TeamsStatisticsQueryVariables = Types.Exact<{
teamId?: Types.InputMaybe<Types.Scalars['ID']>;
aggregationEpochs?: Types.InputMaybe<Types.Scalars['Int']>;
}>;
export type TeamsStatisticsQuery = { __typename?: 'Query', teamsStatistics?: { __typename?: 'TeamsStatisticsConnection', edges: Array<{ __typename?: 'TeamStatisticsEdge', node: { __typename?: 'TeamStatistics', teamId: string, totalQuantumVolume: string, totalQuantumRewards: string, totalGamesPlayed: number, gamesPlayed: Array<string> } }> } | null };
export const TeamsStatisticsDocument = gql`
query TeamsStatistics($teamId: ID, $aggregationEpochs: Int) {
teamsStatistics(teamId: $teamId, aggregationEpochs: $aggregationEpochs) {
edges {
node {
teamId
totalQuantumVolume
totalQuantumRewards
totalGamesPlayed
gamesPlayed
}
}
}
}
`;
/**
* __useTeamsStatisticsQuery__
*
* To run a query within a React component, call `useTeamsStatisticsQuery` and pass it any options that fit your needs.
* When your component renders, `useTeamsStatisticsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useTeamsStatisticsQuery({
* variables: {
* teamId: // value for 'teamId'
* aggregationEpochs: // value for 'aggregationEpochs'
* },
* });
*/
export function useTeamsStatisticsQuery(baseOptions?: Apollo.QueryHookOptions<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>(TeamsStatisticsDocument, options);
}
export function useTeamsStatisticsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>(TeamsStatisticsDocument, options);
}
export type TeamsStatisticsQueryHookResult = ReturnType<typeof useTeamsStatisticsQuery>;
export type TeamsStatisticsLazyQueryHookResult = ReturnType<typeof useTeamsStatisticsLazyQuery>;
export type TeamsStatisticsQueryResult = Apollo.QueryResult<TeamsStatisticsQuery, TeamsStatisticsQueryVariables>;
@@ -0,0 +1,38 @@
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 recurring = node.transfer.kind.__typename !== 'RecurringTransfer';
const active = onlyActive ? isActiveReward(node, currentEpoch) : true;
return active && recurring && isScopedToTeams(node);
});
return {
data: games,
loading,
error,
};
};
@@ -0,0 +1,67 @@
import compact from 'lodash/compact';
import orderBy from 'lodash/orderBy';
import {
useTeamQuery,
type TeamFieldsFragment,
type TeamStatsFieldsFragment,
type TeamRefereeFieldsFragment,
type TeamEntityFragment,
} from './__generated__/Team';
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 },
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,
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,
};
};
@@ -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';
};
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,
};
};
@@ -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 './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');
});
});
@@ -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 './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>
);
};
@@ -5,17 +5,19 @@ import {
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import type { FieldValues } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import classNames from 'classnames';
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { RainbowButton } from './buttons';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { useCallback } from 'react';
import { RainbowButton } from '../../components/rainbow-button';
import {
useSimpleTransaction,
useVegaWallet,
useVegaWalletDialogStore,
} from '@vegaprotocol/wallet';
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { Routes } from '../../lib/links';
import { useTransactionEventSubscription } from '@vegaprotocol/web3';
import { Statistics, useStats } from './referral-statistics';
import { useReferralProgram } from './hooks/use-referral-program';
import { ns, useT } from '../../lib/use-t';
@@ -73,6 +75,10 @@ export const ApplyCodeFormContainer = ({
return <ApplyCodeForm onSuccess={onSuccess} />;
};
type FormFields = {
code: string;
};
export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
const t = useT();
const program = useReferralProgram();
@@ -81,31 +87,47 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
(store) => store.openVegaWalletDialog
);
const [status, setStatus] = useState<
'requested' | 'no-funds' | 'successful' | null
>(null);
const txHash = useRef<string | null>(null);
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
const { isReadOnly, pubKey } = useVegaWallet();
const { isEligible, requiredFunds } = useFundsAvailable();
const currentRouteId = useGetCurrentRouteId();
const setViews = useSidebar((s) => s.setViews);
const [params] = useSearchParams();
const {
register,
handleSubmit,
formState: { errors },
setValue,
setError,
watch,
} = useForm();
const [params] = useSearchParams();
} = useForm<FormFields>({
defaultValues: {
code: params.get('code') || '',
},
});
const codeField = watch('code');
const { data: previewData, loading: previewLoading } = useReferral({
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)
*/
@@ -135,99 +157,55 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
return true;
}, [codeField, previewData, previewLoading, t]);
useEffect(() => {
const code = params.get('code');
if (code) setValue('code', code);
}, [params, setValue]);
const noFunds = validateFundsAvailable() !== true ? true : false;
useEffect(() => {
const err = validateFundsAvailable();
if (err !== true) {
setStatus('no-funds');
} else {
setStatus(null);
}
}, [isEligible, validateFundsAvailable]);
const onSubmit = ({ code }: FieldValues) => {
const onSubmit = ({ code }: FormFields) => {
if (isReadOnly || !pubKey || !code || code.length === 0) {
return;
}
setStatus('requested');
sendTx(pubKey, {
send({
applyReferralCode: {
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({
variables: { partyId: pubKey || '' },
skip: !pubKey,
fetchPolicy: 'no-cache',
onData: ({ data: result }) =>
result.data?.busEvents?.forEach((event) => {
if (event.event.__typename === 'TransactionResult') {
const hash = event.event.hash.toLowerCase();
if (txHash.current && txHash.current === hash) {
const err = event.event.error;
const status = event.event.status;
if (err) {
setStatus(null);
setError('code', {
type: 'required',
message: err,
});
}
if (status && !err) {
setStatus('successful');
}
}
}
}),
});
// sendTx(pubKey, {
// applyReferralCode: {
// 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'),
// });
// }
// });
};
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
if (status === 'successful') {
if (status === 'confirmed') {
return (
<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">
@@ -261,7 +239,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
};
}
if (status === 'no-funds') {
if (noFunds) {
return {
disabled: false,
children: t('Deposit funds'),
@@ -332,7 +310,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
</label>
<RainbowButton variant="border" {...getButtonProps()} />
</form>
{status === 'no-funds' ? (
{noFunds ? (
<InputError intent="warning" className="overflow-auto break-words">
<span>
<SpamProtectionErr requiredFunds={requiredFunds?.toString()} />
@@ -4,41 +4,6 @@ import type { ComponentProps, ButtonHTMLAttributes } from 'react';
import { forwardRef } from 'react';
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(
'inline-block',
'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 =
'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
export const REFERRAL_DOCS_LINK =
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
@@ -1,9 +1,5 @@
import {
useVegaWallet,
useVegaWalletDialogStore,
determineId,
} from '@vegaprotocol/wallet';
import { RainbowButton } from './buttons';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
import { RainbowButton } from '../../components/rainbow-button';
import { useState } from 'react';
import {
CopyWithTooltip,
@@ -18,13 +14,13 @@ import {
} from '@vegaprotocol/ui-toolkit';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
import { useStakeAvailable } from './hooks/use-stake-available';
import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { useT } from '../../lib/use-t';
import { Navigate } from 'react-router-dom';
import { Routes } from '../../lib/links';
import { useReferralProgram } from './hooks/use-referral-program';
import { useCreateReferralSet } from '../../lib/hooks/use-create-referral-set';
export const CreateCodeContainer = () => {
const { pubKey } = useVegaWallet();
@@ -95,67 +91,42 @@ const CreateCodeDialog = ({
}) => {
const t = useT();
const createLink = useLinks(DApp.Governance);
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
const { pubKey } = useVegaWallet();
const { refetch } = useReferral({ pubKey, role: 'referrer' });
const [err, setErr] = useState<string | null>(null);
const [code, setCode] = useState<string | null>(null);
const [status, setStatus] = useState<
'idle' | 'loading' | 'success' | 'error'
>('idle');
const { stakeAvailable: currentStakeAvailable, requiredStake } =
useStakeAvailable();
const {
err,
code,
status,
stakeAvailable: currentStakeAvailable,
requiredStake,
onSubmit,
} = useCreateReferralSet();
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 = () => {
if (status === 'idle' || status === 'error') {
if (status === 'idle') {
return {
children: t('Generate code'),
onClick: () => onSubmit(),
onClick: () => onSubmit({ createReferralSet: { isTeam: false } }),
};
}
if (status === 'loading') {
if (status === 'requested') {
return {
children: t('Confirm in wallet...'),
disabled: true,
};
}
if (status === 'success') {
if (status === 'pending') {
return {
children: t('Waiting for transaction...'),
disabled: true,
};
}
if (status === 'confirmed') {
return {
children: t('Close'),
intent: Intent.Success,
@@ -209,7 +180,10 @@ const CreateCodeDialog = ({
if (!programDetails) {
return (
<div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && (
{(status === 'idle' ||
status === 'requested' ||
status === 'pending' ||
err) && (
<>
{
<p>
@@ -220,7 +194,7 @@ const CreateCodeDialog = ({
}
</>
)}
{status === 'success' && code && (
{status === 'confirmed' && code && (
<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">
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
@@ -240,7 +214,7 @@ const CreateCodeDialog = ({
<TradingButton
fill={true}
intent={Intent.Primary}
onClick={() => onSubmit()}
onClick={() => onSubmit({ createReferralSet: { isTeam: false } })}
{...getButtonProps()}
>
{t('Yes')}
@@ -269,14 +243,17 @@ const CreateCodeDialog = ({
return (
<div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && (
{(status === 'idle' ||
status === 'requested' ||
status === 'pending' ||
err) && (
<p>
{t(
'Generate a referral code to share with your friends and access the commission benefits of the current program.'
)}
</p>
)}
{status === 'success' && code && (
{status === 'confirmed' && code && (
<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">
<p className="overflow-hidden whitespace-nowrap text-ellipsis">
@@ -1,7 +1,7 @@
import { isRouteErrorResponse, useNavigate, useRouteError } 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 { LayoutWithSky } from './layout';
import { Routes } from '../../lib/links';
import { useT } from '../../lib/use-t';
@@ -8,13 +8,13 @@ import type {
ReferralSetsQueryVariables,
} 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 type Role = 'referrer' | 'referee';
type UseReferralArgs = (
| { code: string }
| { code: string | undefined }
| { pubKey: string | null; role: Role }
) & {
aggregationEpochs?: number;
@@ -1,6 +1,9 @@
import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
import { render, waitFor } from '@testing-library/react';
import { type VegaWalletContextShape } from '@vegaprotocol/wallet';
import { render, screen, waitFor } from '@testing-library/react';
import {
VegaWalletContext,
type VegaWalletContextShape,
} from '@vegaprotocol/wallet';
import { ReferralStatistics } from './referral-statistics';
import {
ReferralProgramDocument,
@@ -15,7 +18,7 @@ import {
StakeAvailableDocument,
type StakeAvailableQueryVariables,
type StakeAvailableQuery,
} from './hooks/__generated__/StakeAvailable';
} from '../../lib/hooks/__generated__/StakeAvailable';
import {
RefereesDocument,
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', () => {
it('displays apply code when no data has been found for given pubkey', () => {
const { queryByTestId } = render(
const renderComponent = (mocks: MockedResponse[]) => {
const walletContext = {
pubKey: MOCK_PUBKEY,
isReadOnly: false,
sendTx: jest.fn(),
} as unknown as VegaWalletContextShape;
return render(
<MemoryRouter>
<MockedProvider mocks={[]} showWarnings={false}>
<ReferralStatistics />
</MockedProvider>
<VegaWalletContext.Provider value={walletContext}>
<MockedProvider mocks={mocks} showWarnings={false}>
<ReferralStatistics />
</MockedProvider>
</VegaWalletContext.Provider>
</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 () => {
const { queryByTestId } = render(
<MemoryRouter>
<MockedProvider
mocks={[
programMock,
referralSetAsReferrerMock,
noReferralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
refereesMock30,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
renderComponent([
programMock,
referralSetAsReferrerMock,
noReferralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
refereesMock30,
]);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referrer'
);
// gets commision from 30 epochs query
expect(queryByTestId('total-commission-value')).toHaveTextContent(
expect(screen.queryByTestId('total-commission-value')).toHaveTextContent(
'12,340'
);
});
});
it('displays referee stats when given pubkey is a referee', async () => {
const { queryByTestId } = render(
<MemoryRouter>
<MockedProvider
mocks={[
programMock,
noReferralSetAsReferrerMock,
referralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
renderComponent([
programMock,
noReferralSetAsReferrerMock,
referralSetAsRefereeMock,
stakeAvailableMock,
refereesMock,
]);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
});
});
it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => {
const { queryByTestId } = render(
<MemoryRouter>
<MockedProvider
mocks={[
programMock,
noReferralSetAsReferrerMock,
referralSetAsRefereeMock,
nonEligibleStakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
renderComponent([
programMock,
noReferralSetAsReferrerMock,
referralSetAsRefereeMock,
nonEligibleStakeAvailableMock,
refereesMock,
]);
await waitFor(() => {
expect(
queryByTestId('referral-create-code-form')
screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual(
expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee'
);
expect(queryByTestId('referral-eligibility-warning')).toBeInTheDocument();
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument();
expect(
screen.queryByTestId('referral-eligibility-warning')
).toBeInTheDocument();
expect(
screen.queryByTestId('referral-apply-code-form')
).toBeInTheDocument();
});
});
});
@@ -26,7 +26,7 @@ import {
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
import compact from 'lodash/compact';
import { useReferralProgram } from './hooks/use-referral-program';
import { useStakeAvailable } from './hooks/use-stake-available';
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
import sortBy from 'lodash/sortBy';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
-1
View File
@@ -1 +0,0 @@
export { Teams } from './teams';
@@ -1,7 +0,0 @@
export const Teams = () => {
return (
<div>
<h1>Teams</h1>
</div>
);
};
@@ -11,6 +11,7 @@ import {
} from '@vegaprotocol/candles-chart';
import { useEnvironment } from '@vegaprotocol/environment';
import { useChartSettings, STUDY_SIZE } from './use-chart-settings';
import { SUPPORTED_INTERVALS, type SupportedInterval } from './constants';
/**
* Renders either the pennant chart or the tradingview chart
@@ -36,7 +37,7 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
const pennantChart = (
<CandlesChartContainer
marketId={marketId}
interval={toPennantInterval(interval)}
interval={toPennantInterval(interval as SupportedInterval)}
chartType={chartType}
overlays={overlays}
studies={studies}
@@ -63,7 +64,7 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
libraryPath={CHARTING_LIBRARY_PATH}
libraryHash={CHARTING_LIBRARY_HASH}
marketId={marketId}
interval={toTradingViewResolution(interval)}
interval={toTradingViewResolution(interval as SupportedInterval)}
studies={tradingViewStudies}
onIntervalChange={(newInterval) => {
setInterval(fromTradingViewResolution(newInterval));
@@ -83,7 +84,11 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
}
};
const toTradingViewResolution = (interval: Interval) => {
const toTradingViewResolution = (interval: SupportedInterval) => {
if (!SUPPORTED_INTERVALS.includes(interval)) {
throw new Error(`interval ${interval} is not supported`);
}
const resolution = TRADINGVIEW_INTERVAL_MAP[interval];
if (!resolution) {
@@ -107,7 +112,11 @@ const fromTradingViewResolution = (resolution: string) => {
return interval as Interval;
};
const toPennantInterval = (interval: Interval) => {
const toPennantInterval = (interval: SupportedInterval) => {
if (!SUPPORTED_INTERVALS.includes(interval)) {
throw new Error(`interval ${interval} is not supported`);
}
const pennantInterval = PENNANT_INTERVAL_MAP[interval];
if (!pennantInterval) {
@@ -18,21 +18,13 @@ import {
TradingDropdownTrigger,
Icon,
} from '@vegaprotocol/ui-toolkit';
import { Interval } from '@vegaprotocol/types';
import { type Interval } from '@vegaprotocol/types';
import { useEnvironment } from '@vegaprotocol/environment';
import { ALLOWED_TRADINGVIEW_HOSTNAMES } from '@vegaprotocol/trading-view';
import { IconNames, type IconName } from '@blueprintjs/icons';
import { useChartSettings } from './use-chart-settings';
import { useT } from '../../lib/use-t';
const INTERVALS = [
Interval.INTERVAL_I1M,
Interval.INTERVAL_I5M,
Interval.INTERVAL_I15M,
Interval.INTERVAL_I1H,
Interval.INTERVAL_I6H,
Interval.INTERVAL_I1D,
];
import { SUPPORTED_INTERVALS } from './constants';
const chartTypeIcon = new Map<ChartType, IconName>([
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
@@ -93,7 +85,7 @@ export const ChartMenu = () => {
setInterval(value as Interval);
}}
>
{INTERVALS.map((timeInterval) => (
{SUPPORTED_INTERVALS.map((timeInterval) => (
<TradingDropdownRadioItem
key={timeInterval}
inset
@@ -0,0 +1,12 @@
import { Interval } from '@vegaprotocol/types';
export type SupportedInterval = typeof SUPPORTED_INTERVALS[number];
export const SUPPORTED_INTERVALS = [
Interval.INTERVAL_I1M,
Interval.INTERVAL_I5M,
Interval.INTERVAL_I15M,
Interval.INTERVAL_I1H,
Interval.INTERVAL_I6H,
Interval.INTERVAL_I1D,
] as const;
@@ -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
)}
/>
);
};
@@ -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>
);
};
@@ -0,0 +1,30 @@
import classNames from 'classnames';
import { AnimatedDudeWithWire } from '../../client-pages/referrals/graphics/dude';
import { type ReactNode } from 'react';
export const CompetitionsHeader = ({
title,
children,
}: {
title: string;
children?: ReactNode;
}) => {
return (
<div className={classNames('relative mb-10 lg:mb-20')}>
<div className="">
<div
aria-hidden
className="absolute top-20 right-[220px] md:right-[240px] max-sm:hidden"
>
<AnimatedDudeWithWire />
</div>
<div className="pt-10 lg:pt-20 sm:w-[50%]">
<h1 className="text-3xl _text-[6vw] lg:!text-6xl leading-[1em] font-alpha calt mb-10">
{title}
</h1>
{children}
</div>
</div>
</div>
);
};
@@ -0,0 +1,73 @@
import { Link } from 'react-router-dom';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { getNumberFormat } from '@vegaprotocol/utils';
import { type useTeams } from '../../client-pages/competitions/hooks/use-teams';
import { useT } from '../../lib/use-t';
import { Table } from '../table';
import { Rank } from './graphics/rank';
import { Links } from '../../lib/links';
import { TeamAvatar } from './team-avatar';
export const CompetitionsLeaderboard = ({
data,
}: {
data: ReturnType<typeof useTeams>['data'];
}) => {
const t = useT();
const num = (n?: number | string) =>
!n ? '-' : getNumberFormat(0).format(Number(n));
if (!data || data.length === 0) {
return <Splash>{t('Could not find any teams')}</Splash>;
}
return (
<Table
columns={[
{ name: 'rank', displayName: '#' },
{ name: 'avatar', displayName: '' },
{ name: 'team', displayName: t('Team') },
{ name: 'earned', displayName: t('Rewards earned') },
{ name: 'games', displayName: t('Total games') },
{ name: 'members', displayName: t('No. of members') },
{ name: 'status', displayName: t('Status') },
{ name: 'volume', displayName: t('Volume') },
]}
data={data.map((td, i) => {
// leaderboard place or medal
let rank: number | React.ReactNode = i + 1;
if (rank === 1) rank = <Rank variant="gold" />;
if (rank === 2) rank = <Rank variant="silver" />;
if (rank === 3) rank = <Rank variant="bronze" />;
const avatar = (
<TeamAvatar
teamId={td.teamId}
imgUrl={td.avatarUrl}
alt={td.name}
size="small"
/>
);
return {
rank,
avatar,
team: (
<Link
className="hover:underline"
to={Links.COMPETITIONS_TEAM(td.teamId)}
>
{td.name}
</Link>
),
earned: num(td.totalQuantumRewards),
games: num(td.totalGamesPlayed),
members: 0,
status: td.closed ? t('Closed') : t('Open'),
volume: num(td.totalQuantumVolume),
};
})}
></Table>
);
};
@@ -0,0 +1,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>
);
};
@@ -0,0 +1,40 @@
import classNames from 'classnames';
import { DudeWithFlag } from './dude-with-flag';
/**
* Pre-defined badge gradients
*/
export const BADGE_GRADIENT_VARIANT_A =
'bg-gradient-to-r from-vega-blue-500 via-vega-purple-500 to-vega-pink-500';
export const BADGE_GRADIENT_VARIANT_B =
'bg-gradient-to-r from-vega-purple-500 via-vega-green-500 to-vega-blue-500';
export const BADGE_GRADIENT_VARIANT_C =
'bg-gradient-to-r from-vega-blue-500 via-vega-purple-500 to-vega-green-500';
/** Badge */
export const DudeBadge = ({
variant,
className,
}: {
variant: 'A' | 'B' | 'C' | undefined;
className?: classNames.Argument;
}) => {
return (
<div
className={classNames(
'w-24 h-24 rounded-full bg-black relative',
'rotate-12',
{
[BADGE_GRADIENT_VARIANT_A]: variant === 'A',
[BADGE_GRADIENT_VARIANT_B]: variant === 'B',
[BADGE_GRADIENT_VARIANT_C]: variant === 'C',
},
className
)}
>
<DudeWithFlag className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 -rotate-12" />
</div>
);
};
@@ -0,0 +1,50 @@
import { theme } from '@vegaprotocol/tailwindcss-config';
type DudeWithFlagProps = {
flagColor?: string;
withStar?: boolean;
className?: string;
};
const DEFAULT_FLAG_COLOR = theme.colors.vega.green[500];
export const DudeWithFlag = ({
flagColor = DEFAULT_FLAG_COLOR,
withStar = true,
className,
}: DudeWithFlagProps) => {
return (
<svg
width="49"
height="43"
viewBox="0 0 49 43"
fill="none"
className={className}
>
{withStar && (
<>
<path d="M3.99992 0H2V1.99993H3.99992V0Z" fill="white" />
<path
d="M2 1.99993L0 1.99981V3.99974H1.99992L2 1.99993Z"
fill="white"
/>
<path
d="M3.99995 3.99992L1.99992 3.99974L2 5.99988H3.99995V3.99992Z"
fill="white"
/>
<path
d="M5.99997 1.99981L3.99992 1.99993L3.99995 3.99992L5.99997 3.99974V1.99981Z"
fill="white"
/>
</>
)}
<path
d="M32 4H11V33H15V43H20V33H23V43H28V33H32V4ZM20 17H15V12H20V17ZM28 17H23V12H28V17Z"
fill="white"
/>
<path d="M41 25L32 25L32 20L41 20L41 25Z" fill="white" />
<path d="M36 29V4H35V29" fill="white" />
<path d="M36 13H49L44.55 8.5L49 4H36V13Z" fill={flagColor} />
</svg>
);
};
@@ -0,0 +1,110 @@
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import classNames from 'classnames';
export const Rank = ({
variant,
className,
}: {
variant?: 'gold' | 'silver' | 'bronze';
className?: classNames.Argument;
}) => {
const { theme } = useThemeSwitcher();
return (
<div
title={classNames({
'1': variant === 'gold',
'2': variant === 'silver',
'3': variant === 'bronze',
})}
className={classNames(
{
'text-yellow-300': variant === 'gold',
'text-vega-clight-500': variant === 'silver',
'text-vega-orange-500': variant === 'bronze',
'text-black dark:text-white': variant === undefined,
},
className
)}
>
<svg width="18" height="30" viewBox="0 0 18 30" fill="none">
<defs>
<linearGradient x1="0" y1="0" x2="100%" y2="100%" id="medal">
<stop offset="33%" 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>
);
};
@@ -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"
/>
);
};
@@ -1,21 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { GridSettings } from './grid-settings';
import '@testing-library/jest-dom';
describe('GridSettings', () => {
it('calls updateGridStore with correct arguments on button click', async () => {
const mockUpdateGridStore = jest.fn();
const { getByText } = render(
<GridSettings updateGridStore={mockUpdateGridStore} />
);
await userEvent.click(getByText('Reset Columns'));
expect(mockUpdateGridStore).toHaveBeenCalledWith({
columnState: undefined,
filterModel: undefined,
});
});
});
@@ -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>
);
};

Some files were not shown because too many files have changed in this diff Show More