Compare commits

...
Author SHA1 Message Date
Matthew Russell 22fc6125b4 test: make sure join button displays correctly 2024-01-17 15:14:19 -05:00
Matthew Russell c496d11331 chore: add i18n 2024-01-17 14:03:59 -05:00
Matthew Russell 1788753494 chore: useSimpleTransaction for create referral code hook 2024-01-17 13:10:50 -05:00
Matthew Russell cf5fa0c454 fix: exports and make sure queries make a request against api 2024-01-17 12:07:06 -05:00
Matthew Russell f2a434c1c0 chore: add i18n for simple tx hook 2024-01-17 10:55:33 -05:00
Matthew Russell 94b84ab1dc feat: separate join tx components 2024-01-17 10:55:33 -05:00
Matthew Russell 022e2010c4 feat: use simple transaction hook in apply code 2024-01-17 10:55:33 -05:00
Matthew Russell 67e598873f feat: handle join button states and join tx 2024-01-17 10:55:33 -05:00
Matthew Russell 066f34fd5d feat: add partyStatus for more clarity on party member state 2024-01-17 10:55:33 -05: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
135 changed files with 4755 additions and 1591 deletions
@@ -1,9 +1,11 @@
query ExplorerProposal($id: ID!) { query ExplorerProposal($id: ID!) {
proposal(id: $id) { proposal(id: $id) {
id ... on Proposal {
rationale { id
title rationale {
description 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` export const ExplorerProposalDocument = gql`
query ExplorerProposal($id: ID!) { query ExplorerProposal($id: ID!) {
proposal(id: $id) { proposal(id: $id) {
id ... on Proposal {
rationale { id
title rationale {
description 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 { ExternalLink } from '@vegaprotocol/ui-toolkit';
import { ENV } from '../../../config/env'; import { ENV } from '../../../config/env';
import Hash from '../hash'; import Hash from '../hash';
export type ProposalLinkProps = { export type ProposalLinkProps = {
id: string; id: string;
text?: string; text?: string;
@@ -16,8 +20,13 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
variables: { id }, variables: { id },
}); });
const proposal = data?.proposal as Extract<
ExplorerProposalQuery['proposal'],
{ __typename?: 'Proposal' }
>;
const base = ENV.dataSources.governanceUrl; const base = ENV.dataSources.governanceUrl;
const label = data?.proposal?.rationale.title || id; const label = proposal?.rationale.title || id;
return ( return (
<ExternalLink href={`${base}/proposals/${id}`}> <ExternalLink href={`${base}/proposals/${id}`}>
@@ -1,7 +1,9 @@
query ExplorerProposalStatus($id: ID!) { query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) { proposal(id: $id) {
id ... on Proposal {
state id
rejectionReason 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` export const ExplorerProposalStatusDocument = gql`
query ExplorerProposalStatus($id: ID!) { query ExplorerProposalStatus($id: ID!) {
proposal(id: $id) { proposal(id: $id) {
id ... on Proposal {
state id
rejectionReason state
rejectionReason
}
} }
} }
`; `;
@@ -14,16 +14,18 @@ export function format(date: string | undefined, def: string) {
return new Date().toLocaleDateString() || def; return new Date().toLocaleDateString() || def;
} }
export function getDate( type Proposal = Extract<
data: ExplorerProposalStatusQuery | undefined, ExplorerProposalStatusQuery['proposal'],
terms: Terms { __typename?: 'Proposal' }
): string { >;
export function getDate(proposal: Proposal | undefined, terms: Terms): string {
const DEFAULT = t('Unknown'); const DEFAULT = t('Unknown');
if (!data?.proposal?.state) { if (!proposal?.state) {
return DEFAULT; return DEFAULT;
} }
switch (data.proposal.state) { switch (proposal.state) {
case 'STATE_DECLINED': case 'STATE_DECLINED':
return `${t('Rejected on')}: ${format(terms.closingTimestamp, DEFAULT)}`; return `${t('Rejected on')}: ${format(terms.closingTimestamp, DEFAULT)}`;
case 'STATE_ENACTED': case 'STATE_ENACTED':
@@ -62,9 +64,11 @@ export const ProposalDate = ({ terms, id }: ProposalDateProps) => {
}, },
}); });
const proposal = data?.proposal as Proposal;
return ( return (
<Lozenge className="font-sans text-xs float-right"> <Lozenge className="font-sans text-xs float-right">
{getDate(data, terms)} {getDate(proposal, terms)}
</Lozenge> </Lozenge>
); );
}; };
@@ -2,17 +2,8 @@ import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import type { IconProps } from '@vegaprotocol/ui-toolkit'; import type { IconProps } from '@vegaprotocol/ui-toolkit';
import { useExplorerProposalStatusQuery } from './__generated__/Proposal'; import { useExplorerProposalStatusQuery } from './__generated__/Proposal';
import type { ExplorerProposalStatusQuery } 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'; import { t } from '@vegaprotocol/i18n';
type ProposalQueryResult = Apollo.QueryResult<
ExplorerProposalStatusQuery,
Types.Exact<{
id: string;
}>
>;
interface ProposalStatusIconProps { interface ProposalStatusIconProps {
id: string; id: string;
} }
@@ -29,29 +20,38 @@ type IconAndLabel = {
* @param data a data result from useExplorerProposalStatusQuery * @param data a data result from useExplorerProposalStatusQuery
* @returns Icon name * @returns Icon name
*/ */
export function getIconAndLabelForStatus( export function useIconAndLabelForStatus(id: string): IconAndLabel {
res: ProposalQueryResult const { data, loading, error } = useExplorerProposalStatusQuery({
): IconAndLabel { variables: {
id,
},
});
const proposal = data?.proposal as Extract<
ExplorerProposalStatusQuery['proposal'],
{ __typename?: 'Proposal' }
>;
const DEFAULT: IconAndLabel = { const DEFAULT: IconAndLabel = {
icon: 'error', icon: 'error',
label: t('Proposal state unknown'), label: t('Proposal state unknown'),
}; };
if (res.loading) { if (loading) {
return { return {
icon: 'more', icon: 'more',
label: t('Loading data'), label: t('Loading data'),
}; };
} }
if (!res?.data?.proposal || res.error) { if (!data?.proposal || error) {
return { return {
icon: 'error', icon: 'error',
label: res.error?.message || DEFAULT.label, label: error?.message || DEFAULT.label,
}; };
} }
switch (res.data.proposal.state) { switch (proposal.state) {
case 'STATE_DECLINED': case 'STATE_DECLINED':
return { return {
icon: 'stop', icon: 'stop',
@@ -99,13 +99,7 @@ export function getIconAndLabelForStatus(
/** /**
*/ */
export const ProposalStatusIcon = ({ id }: ProposalStatusIconProps) => { export const ProposalStatusIcon = ({ id }: ProposalStatusIconProps) => {
const { icon, label } = getIconAndLabelForStatus( const { icon, label } = useIconAndLabelForStatus(id);
useExplorerProposalStatusQuery({
variables: {
id,
},
})
);
return ( return (
<div className="float-left mr-3"> <div className="float-left mr-3">
+2 -2
View File
@@ -31,7 +31,7 @@ import {
orderByUpgradeBlockHeight, orderByUpgradeBlockHeight,
} from '../proposals/components/proposals-list/proposals-list'; } from '../proposals/components/proposals-list/proposals-list';
import { BigNumber } from '../../lib/bignumber'; import { BigNumber } from '../../lib/bignumber';
import type { ProposalQuery } from '../proposals/proposal/__generated__/Proposal'; import { type Proposal } from '../proposals/types';
const nodesToShow = 6; const nodesToShow = 6;
@@ -39,7 +39,7 @@ const HomeProposals = ({
proposals, proposals,
protocolUpgradeProposals, protocolUpgradeProposals,
}: { }: {
proposals: ProposalQuery['proposal'][]; proposals: Proposal[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[]; protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -1,16 +1,11 @@
import { useTranslation } from 'react-i18next'; 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 { ProposalState } from '@vegaprotocol/types';
import { ProposalInfoLabel } from '../proposal-info-label'; import { ProposalInfoLabel } from '../proposal-info-label';
import type { ReactNode } from 'react'; import { type ReactNode } from 'react';
import type { ProposalInfoLabelVariant } from '../proposal-info-label'; import { type ProposalInfoLabelVariant } from '../proposal-info-label';
import { type Proposal } from '../../types';
export const CurrentProposalState = ({ export const CurrentProposalState = ({ proposal }: { proposal: Proposal }) => {
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const { t } = useTranslation(); const { t } = useTranslation();
let proposalStatus: ReactNode; let proposalStatus: ReactNode;
let variant = 'tertiary' as ProposalInfoLabelVariant; 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, KeyValueTableRow,
RoundedWrapper, RoundedWrapper,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import { type Proposal } from '../../types';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
interface ProposalChangeTableProps { interface ProposalChangeTableProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: Proposal;
} }
export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => { export const ProposalChangeTable = ({ proposal }: ProposalChangeTableProps) => {
@@ -23,8 +23,8 @@ import { useFeatureFlags } from '@vegaprotocol/environment';
import { BrowserRouter } from 'react-router-dom'; import { BrowserRouter } from 'react-router-dom';
import { VoteState } from '../vote-details/use-user-vote'; import { VoteState } from '../vote-details/use-user-vote';
import { useNewTransferProposalDetails } from '@vegaprotocol/proposals'; 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.mock('@vegaprotocol/proposals', () => ({
...jest.requireActual('@vegaprotocol/proposals'), ...jest.requireActual('@vegaprotocol/proposals'),
@@ -36,7 +36,7 @@ jest.mock('@vegaprotocol/proposals', () => ({
})); }));
const renderComponent = ( const renderComponent = (
proposal: ProposalQuery['proposal'], proposal: Proposal,
isListItem = true, isListItem = true,
mocks: MockedResponse[] = [], mocks: MockedResponse[] = [],
voteState?: VoteState voteState?: VoteState
@@ -64,6 +64,7 @@ describe('Proposal header', () => {
it('Renders New market proposal', () => { it('Renders New market proposal', () => {
useFeatureFlags.setState({ flags: { SUCCESSOR_MARKETS: true } }); useFeatureFlags.setState({ flags: { SUCCESSOR_MARKETS: true } });
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
rationale: { rationale: {
title: 'New some market', title: 'New some market',
@@ -102,6 +103,7 @@ describe('Proposal header', () => {
it('Renders Update market proposal', () => { it('Renders Update market proposal', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
rationale: { rationale: {
title: 'New market id', title: 'New market id',
@@ -130,6 +132,7 @@ describe('Proposal header', () => {
it('Renders New asset proposal - ERC20', () => { it('Renders New asset proposal - ERC20', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
rationale: { rationale: {
title: 'New asset: Fake currency', title: 'New asset: Fake currency',
@@ -159,6 +162,7 @@ describe('Proposal header', () => {
it('Renders New asset proposal - BuiltInAsset', () => { it('Renders New asset proposal - BuiltInAsset', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
terms: { terms: {
change: { change: {
@@ -184,6 +188,7 @@ describe('Proposal header', () => {
it('Renders Update network', () => { it('Renders Update network', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
rationale: { rationale: {
title: 'Network parameter', title: 'Network parameter',
@@ -213,6 +218,7 @@ describe('Proposal header', () => {
it('Renders Freeform proposal - short rationale', () => { it('Renders Freeform proposal - short rationale', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
id: 'short', id: 'short',
rationale: { rationale: {
@@ -234,6 +240,7 @@ describe('Proposal header', () => {
it('Renders Freeform proposal - long rationale (105 chars) - listing', () => { it('Renders Freeform proposal - long rationale (105 chars) - listing', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
id: 'long', id: 'long',
rationale: { rationale: {
@@ -259,6 +266,7 @@ describe('Proposal header', () => {
// Remove once proposals have rationale and re-enable above tests // Remove once proposals have rationale and re-enable above tests
it('Renders Freeform proposal - id for title', () => { it('Renders Freeform proposal - id for title', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
id: 'freeform id', id: 'freeform id',
rationale: { rationale: {
@@ -280,6 +288,7 @@ describe('Proposal header', () => {
it('Renders asset change proposal header', () => { it('Renders asset change proposal header', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
terms: { terms: {
change: { change: {
@@ -297,6 +306,7 @@ describe('Proposal header', () => {
it("Renders unknown proposal if it's a different proposal type", () => { it("Renders unknown proposal if it's a different proposal type", () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
terms: { terms: {
change: { change: {
@@ -313,6 +323,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Enacted', () => { it('Renders proposal state: Enacted', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
state: ProposalState.STATE_ENACTED, state: ProposalState.STATE_ENACTED,
terms: { terms: {
@@ -325,6 +336,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Passed', () => { it('Renders proposal state: Passed', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
state: ProposalState.STATE_PASSED, state: ProposalState.STATE_PASSED,
terms: { terms: {
@@ -338,6 +350,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Waiting for node vote', () => { it('Renders proposal state: Waiting for node vote', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
state: ProposalState.STATE_WAITING_FOR_NODE_VOTE, state: ProposalState.STATE_WAITING_FOR_NODE_VOTE,
terms: { terms: {
@@ -352,6 +365,7 @@ describe('Proposal header', () => {
it('Renders proposal state: Open', () => { it('Renders proposal state: Open', () => {
renderComponent( renderComponent(
// @ts-ignore we aren't using batch yet
generateProposal({ generateProposal({
state: ProposalState.STATE_OPEN, state: ProposalState.STATE_OPEN,
votes: { votes: {
@@ -2,8 +2,7 @@ import { useTranslation } from 'react-i18next';
import { Lozenge, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit'; import { Lozenge, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { shorten } from '@vegaprotocol/utils'; import { shorten } from '@vegaprotocol/utils';
import { Heading, SubHeading } from '../../../../components/heading'; import { Heading, SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react'; import { type ReactNode } from 'react';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { truncateMiddle } from '../../../../lib/truncate-middle'; import { truncateMiddle } from '../../../../lib/truncate-middle';
import { CurrentProposalState } from '../current-proposal-state'; import { CurrentProposalState } from '../current-proposal-state';
import { ProposalInfoLabel } from '../proposal-info-label'; import { ProposalInfoLabel } from '../proposal-info-label';
@@ -15,16 +14,17 @@ import {
import { useFeatureFlags } from '@vegaprotocol/environment'; import { useFeatureFlags } from '@vegaprotocol/environment';
import Routes from '../../../routes'; import Routes from '../../../routes';
import { Link } from 'react-router-dom'; 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 { VoteBreakdown } from '../vote-breakdown';
import { GovernanceTransferKindMapping } from '@vegaprotocol/types'; import { GovernanceTransferKindMapping } from '@vegaprotocol/types';
import { type Proposal } from '../../types';
export const ProposalHeader = ({ export const ProposalHeader = ({
proposal, proposal,
isListItem = true, isListItem = true,
voteState, voteState,
}: { }: {
proposal: ProposalQuery['proposal']; proposal: Proposal;
isListItem?: boolean; isListItem?: boolean;
voteState?: VoteState | null; voteState?: VoteState | null;
}) => { }) => {
@@ -40,7 +40,7 @@ export const ProposalHeader = ({
const titleContent = shorten(title ?? '', 100); const titleContent = shorten(title ?? '', 100);
const getAsset = (proposal: ProposalQuery['proposal']) => { const getAsset = (proposal: Proposal) => {
const terms = proposal?.terms; const terms = proposal?.terms;
if ( if (
terms?.change.__typename === 'NewMarket' && terms?.change.__typename === 'NewMarket' &&
@@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { import {
KeyValueTable, KeyValueTable,
KeyValueTableRow, KeyValueTableRow,
@@ -14,9 +13,10 @@ import {
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { useAppState } from '../../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../../contexts/app-state/app-state-context';
import { type Proposal } from '../../types';
interface ProposalReferralProgramDetailsProps { interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal']; proposal: Proposal | null;
} }
export const formatEndOfProgramTimestamp = (value: string) => { export const formatEndOfProgramTimestamp = (value: string) => {
@@ -1,6 +1,4 @@
import { useTranslation } from 'react-i18next'; 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 { useCancelTransferProposalDetails } from '@vegaprotocol/proposals';
import { import {
KeyValueTable, KeyValueTable,
@@ -8,11 +6,12 @@ import {
RoundedWrapper, RoundedWrapper,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { type Proposal } from '../../types';
export const ProposalCancelTransferDetails = ({ export const ProposalCancelTransferDetails = ({
proposal, proposal,
}: { }: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: Proposal;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const details = useCancelTransferProposalDetails(proposal?.id); const details = useCancelTransferProposalDetails(proposal?.id);
@@ -1,6 +1,4 @@
import { useState } from 'react'; 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 { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -21,11 +19,12 @@ import {
addDecimalsFormatNumberQuantum, addDecimalsFormatNumberQuantum,
formatDateWithLocalTimezone, formatDateWithLocalTimezone,
} from '@vegaprotocol/utils'; } from '@vegaprotocol/utils';
import { type Proposal } from '../../types';
export const ProposalTransferDetails = ({ export const ProposalTransferDetails = ({
proposal, proposal,
}: { }: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: Proposal;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
@@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { import {
KeyValueTable, KeyValueTable,
KeyValueTableRow, KeyValueTableRow,
@@ -12,6 +11,7 @@ import {
} from '../proposal-referral-program-details'; } from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils'; import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { type Proposal } from '../../types';
// These types are not generated as it's not known how dynamic these are // These types are not generated as it's not known how dynamic these are
type VestingBenefitTier = { type VestingBenefitTier = {
@@ -43,7 +43,7 @@ export const formatVolumeDiscountFactor = (value: string) => {
}; };
interface ProposalReferralProgramDetailsProps { interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal']; proposal: Proposal | null;
} }
/** /**
@@ -5,13 +5,13 @@ import {
RoundedWrapper, RoundedWrapper,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { Row } from '@vegaprotocol/markets'; import { Row } from '@vegaprotocol/markets';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { useState } from 'react'; import { useState } from 'react';
import { CollapsibleToggle } from '../../../../components/collapsible-toggle'; import { CollapsibleToggle } from '../../../../components/collapsible-toggle';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import { type Proposal } from '../../types';
interface ProposalUpdateMarketStateProps { interface ProposalUpdateMarketStateProps {
proposal: ProposalQuery['proposal']; proposal: Proposal | null;
} }
export const ProposalUpdateMarketState = ({ export const ProposalUpdateMarketState = ({
@@ -1,5 +1,4 @@
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { import {
KeyValueTable, KeyValueTable,
KeyValueTableRow, KeyValueTableRow,
@@ -12,9 +11,10 @@ import {
} from '../proposal-referral-program-details'; } from '../proposal-referral-program-details';
import { formatNumberPercentage } from '@vegaprotocol/utils'; import { formatNumberPercentage } from '@vegaprotocol/utils';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { type Proposal } from '../../types';
interface ProposalReferralProgramDetailsProps { interface ProposalReferralProgramDetailsProps {
proposal: ProposalQuery['proposal']; proposal: Proposal | null;
} }
export const formatVolumeDiscountFactor = (value: string) => { export const formatVolumeDiscountFactor = (value: string) => {
@@ -1,13 +1,13 @@
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { MockedProvider } from '@apollo/client/testing'; import { MockedProvider } from '@apollo/client/testing';
import { VegaWalletProvider } from '@vegaprotocol/wallet'; 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 { render, screen } from '@testing-library/react';
import { generateProposal } from '../../test-helpers/generate-proposals'; import { generateProposal } from '../../test-helpers/generate-proposals';
import { Proposal } from './proposal'; import { Proposal } from './proposal';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { mockNetworkParams } from '../../test-helpers/mocks'; import { mockNetworkParams } from '../../test-helpers/mocks';
import { type Proposal as IProposal } from '../../types';
jest.mock('@vegaprotocol/network-parameters', () => ({ jest.mock('@vegaprotocol/network-parameters', () => ({
...jest.requireActual('@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( render(
<MemoryRouter> <MemoryRouter>
<MockedProvider> <MockedProvider>
<VegaWalletProvider config={vegaWalletConfig}> <VegaWalletProvider config={vegaWalletConfig}>
<Proposal <Proposal
restData={{}} restData={{}}
proposal={proposal as ProposalQuery['proposal']} proposal={proposal}
networkParams={mockNetworkParams} networkParams={mockNetworkParams}
/> />
</VegaWalletProvider> </VegaWalletProvider>
@@ -12,14 +12,13 @@ import { UserVote } from '../vote-details';
import { ListAsset } from '../list-asset'; import { ListAsset } from '../list-asset';
import Routes from '../../../routes'; import Routes from '../../../routes';
import { ProposalMarketData } from '../proposal-market-data'; import { ProposalMarketData } from '../proposal-market-data';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import { type MarketInfo } from '@vegaprotocol/markets';
import type { MarketInfo } from '@vegaprotocol/markets'; import { type AssetQuery } from '@vegaprotocol/assets';
import type { AssetQuery } from '@vegaprotocol/assets';
import { removePaginationWrapper } from '@vegaprotocol/utils'; import { removePaginationWrapper } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { ProposalMarketChanges } from '../proposal-market-changes'; import { ProposalMarketChanges } from '../proposal-market-changes';
import { ProposalUpdateMarketState } from '../proposal-update-market-state'; 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 { useVoteSubmit } from '@vegaprotocol/proposals';
import { useUserVote } from '../vote-details/use-user-vote'; import { useUserVote } from '../vote-details/use-user-vote';
import { import {
@@ -28,9 +27,10 @@ import {
} from '../proposal-transfer'; } from '../proposal-transfer';
import { useFeatureFlags } from '@vegaprotocol/environment'; import { useFeatureFlags } from '@vegaprotocol/environment';
import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers'; import { ProposalUpdateBenefitTiers } from '../proposal-update-benefit-tiers';
import { type Proposal as IProposal } from '../../types';
export interface ProposalProps { export interface ProposalProps {
proposal: ProposalQuery['proposal']; proposal: IProposal;
networkParams: Partial<NetworkParamsResult>; networkParams: Partial<NetworkParamsResult>;
marketData?: MarketInfo | null; marketData?: MarketInfo | null;
parentMarketData?: MarketInfo | null; parentMarketData?: MarketInfo | null;
@@ -1,7 +1,7 @@
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider'; import { AppStateProvider } from '../../../../contexts/app-state/app-state-provider';
import { VegaWalletContext } from '@vegaprotocol/wallet'; 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 { MockedProvider } from '@apollo/client/testing';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import { format } from 'date-fns'; import { format } from 'date-fns';
@@ -18,10 +18,10 @@ import {
lastWeek, lastWeek,
nextWeek, nextWeek,
} from '../../test-helpers/mocks'; } from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import { type Proposal } from '../../types';
const renderComponent = ( const renderComponent = (
proposal: ProposalQuery['proposal'], proposal: Proposal,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock] mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) => ) =>
@@ -1,21 +1,20 @@
import { type ReactNode } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Button } from '@vegaprotocol/ui-toolkit'; import { Button } from '@vegaprotocol/ui-toolkit';
import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns'; import { differenceInHours, format, formatDistanceToNowStrict } from 'date-fns';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats'; import { DATE_FORMAT_DETAILED } from '../../../../lib/date-formats';
import type { ReactNode } from 'react';
import { import {
ProposalRejectionReasonMapping, ProposalRejectionReasonMapping,
ProposalState, ProposalState,
} from '@vegaprotocol/types'; } from '@vegaprotocol/types';
import Routes from '../../../routes'; import Routes from '../../../routes';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import { type Proposal } from '../../types';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
export const ProposalsListItemDetails = ({ export const ProposalsListItemDetails = ({
proposal, proposal,
}: { }: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: Proposal;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const state = proposal?.state; const state = proposal?.state;
@@ -2,10 +2,10 @@ import { RoundedWrapper } from '@vegaprotocol/ui-toolkit';
import { ProposalHeader } from '../proposal-detail-header/proposal-header'; import { ProposalHeader } from '../proposal-detail-header/proposal-header';
import { ProposalsListItemDetails } from './proposals-list-item-details'; import { ProposalsListItemDetails } from './proposals-list-item-details';
import { useUserVote } from '../vote-details/use-user-vote'; import { useUserVote } from '../vote-details/use-user-vote';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import { type Proposal } from '../../types';
interface ProposalsListItemProps { interface ProposalsListItemProps {
proposal?: ProposalQuery['proposal'] | null; proposal?: Proposal | null;
} }
export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => { export const ProposalsListItem = ({ proposal }: ProposalsListItemProps) => {
@@ -17,8 +17,8 @@ import {
lastMonth, lastMonth,
nextMonth, nextMonth,
} from '../../test-helpers/mocks'; } 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({ const openProposalClosesNextMonth = generateProposal({
id: 'proposal1', id: 'proposal1',
@@ -63,7 +63,7 @@ const closedProtocolUpgradeProposal = generateProtocolUpgradeProposal({
}); });
const renderComponent = ( const renderComponent = (
proposals: ProposalQuery['proposal'][], proposals: Proposal,
protocolUpgradeProposals?: ProtocolUpgradeProposalFieldsFragment[] protocolUpgradeProposals?: ProtocolUpgradeProposalFieldsFragment[]
) => ( ) => (
<Router> <Router>
@@ -10,20 +10,20 @@ import Routes from '../../../routes';
import { Button, Toggle } from '@vegaprotocol/ui-toolkit'; import { Button, Toggle } from '@vegaprotocol/ui-toolkit';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { ExternalLink } from '@vegaprotocol/ui-toolkit'; 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 { ExternalLinks } from '@vegaprotocol/environment';
import { type ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../../types';
interface ProposalsListProps { interface ProposalsListProps {
proposals: Array<ProposalQuery['proposal']>; proposals: Proposal[];
protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[]; protocolUpgradeProposals: ProtocolUpgradeProposalFieldsFragment[];
lastBlockHeight?: string; lastBlockHeight?: string;
} }
interface SortedProposalsProps { interface SortedProposalsProps {
open: ProposalQuery['proposal'][]; open: Proposal[];
closed: ProposalQuery['proposal'][]; closed: Proposal[];
} }
interface SortedProtocolUpgradeProposalsProps { interface SortedProtocolUpgradeProposalsProps {
@@ -31,7 +31,7 @@ interface SortedProtocolUpgradeProposalsProps {
closed: ProtocolUpgradeProposalFieldsFragment[]; closed: ProtocolUpgradeProposalFieldsFragment[];
} }
export const orderByDate = (arr: ProposalQuery['proposal'][]) => export const orderByDate = (arr: Proposal[]) =>
orderBy( orderBy(
arr, arr,
[ [
@@ -91,14 +91,10 @@ export const ProposalsList = ({
); );
return { return {
open: open:
initialSorting.open.length > 0 initialSorting.open.length > 0 ? orderByDate(initialSorting.open) : [],
? orderByDate(initialSorting.open as ProposalQuery['proposal'][])
: [],
closed: closed:
initialSorting.closed.length > 0 initialSorting.closed.length > 0
? orderByDate( ? orderByDate(initialSorting.closed).reverse()
initialSorting.closed as ProposalQuery['proposal'][]
).reverse()
: [], : [],
}; };
}, [proposals]); }, [proposals]);
@@ -125,9 +121,7 @@ export const ProposalsList = ({
}; };
}, [protocolUpgradeProposals, lastBlockHeight]); }, [protocolUpgradeProposals, lastBlockHeight]);
const filterPredicate = ( const filterPredicate = (p: ProposalFieldsFragment | Proposal) =>
p: ProposalFieldsFragment | ProposalQuery['proposal']
) =>
p?.id?.includes(filterString) || p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString); p?.party?.id?.toString().includes(filterString);
@@ -12,7 +12,7 @@ import {
nextWeek, nextWeek,
lastMonth, lastMonth,
} from '../../test-helpers/mocks'; } from '../../test-helpers/mocks';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import { type Proposal } from '../../types';
const rejectedProposalClosesNextWeek = generateProposal({ const rejectedProposalClosesNextWeek = generateProposal({
id: 'rejected1', id: 'rejected1',
@@ -35,7 +35,7 @@ const rejectedProposalClosedLastMonth = generateProposal({
}, },
}); });
const renderComponent = (proposals: ProposalQuery['proposal'][]) => ( const renderComponent = (proposals: Proposal[]) => (
<Router> <Router>
<MockedProvider mocks={[networkParamsQueryMock]}> <MockedProvider mocks={[networkParamsQueryMock]}>
<AppStateProvider> <AppStateProvider>
@@ -3,17 +3,17 @@ import { useTranslation } from 'react-i18next';
import { Heading } from '../../../../components/heading'; import { Heading } from '../../../../components/heading';
import { ProposalsListItem } from '../proposals-list-item'; import { ProposalsListItem } from '../proposals-list-item';
import { ProposalsListFilter } from '../proposals-list-filter'; import { ProposalsListFilter } from '../proposals-list-filter';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import { type Proposal } from '../../types';
interface ProposalsListProps { interface ProposalsListProps {
proposals: ProposalQuery['proposal'][]; proposals: Proposal[];
} }
export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => { export const RejectedProposalsList = ({ proposals }: ProposalsListProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [filterString, setFilterString] = useState(''); const [filterString, setFilterString] = useState('');
const filterPredicate = (p: ProposalQuery['proposal']) => const filterPredicate = (p: Proposal) =>
p?.id?.includes(filterString) || p?.id?.includes(filterString) ||
p?.party?.id?.toString().includes(filterString); p?.party?.id?.toString().includes(filterString);
@@ -9,8 +9,7 @@ import {
nextWeek, nextWeek,
} from '../../test-helpers/mocks'; } from '../../test-helpers/mocks';
import { CompactVotes, VoteBreakdown } from './vote-breakdown'; 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 { import {
generateNoVotes, generateNoVotes,
generateProposal, generateProposal,
@@ -18,7 +17,8 @@ import {
} from '../../test-helpers/generate-proposals'; } from '../../test-helpers/generate-proposals';
import { ProposalState } from '@vegaprotocol/types'; import { ProposalState } from '@vegaprotocol/types';
import { BigNumber } from '../../../../lib/bignumber'; 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); const mockTotalSupply = new BigNumber(100);
// Note - giving a fixedTokenValue of 1 means a ratio of 1:1 votes to tokens, making sums easier :) // 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 = ( const renderComponent = (
proposal: ProposalQuery['proposal'], proposal: Proposal,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
mocks: MockedResponse<any>[] = [networkParamsQueryMock] mocks: MockedResponse<any>[] = [networkParamsQueryMock]
) => ) =>
@@ -1,3 +1,4 @@
import { type ReactNode } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import BigNumber from 'bignumber.js'; import BigNumber from 'bignumber.js';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -5,10 +6,8 @@ import { useVoteInformation } from '../../hooks';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit'; import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
import { formatNumber } from '@vegaprotocol/utils'; import { formatNumber } from '@vegaprotocol/utils';
import { ProposalState } from '@vegaprotocol/types'; 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 { CompactNumber } from '@vegaprotocol/react-helpers';
import { type Proposal } from '../../types';
export const CompactVotes = ({ number }: { number: BigNumber }) => ( export const CompactVotes = ({ number }: { number: BigNumber }) => (
<CompactNumber <CompactNumber
@@ -20,7 +19,7 @@ export const CompactVotes = ({ number }: { number: BigNumber }) => (
); );
interface VoteBreakdownProps { interface VoteBreakdownProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: Proposal;
} }
interface VoteProgressProps { interface VoteProgressProps {
@@ -5,14 +5,13 @@ import { ProposalState } from '@vegaprotocol/types';
import { ConnectToVega } from '../../../../components/connect-to-vega'; import { ConnectToVega } from '../../../../components/connect-to-vega';
import { VoteButtonsContainer } from './vote-buttons'; import { VoteButtonsContainer } from './vote-buttons';
import { SubHeading } from '../../../../components/heading'; import { SubHeading } from '../../../../components/heading';
import type { VoteValue } from '@vegaprotocol/types'; import { type VoteValue } from '@vegaprotocol/types';
import type { DialogProps, VegaTxState } from '@vegaprotocol/proposals'; import { type DialogProps, type VegaTxState } from '@vegaprotocol/proposals';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals'; import { type VoteState } from './use-user-vote';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal'; import { type Proposal } from '../../types';
import type { VoteState } from './use-user-vote';
interface UserVoteProps { interface UserVoteProps {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: Proposal;
minVoterBalance: string | null | undefined; minVoterBalance: string | null | undefined;
spamProtectionMinTokens: string | null | undefined; spamProtectionMinTokens: string | null | undefined;
transaction: VegaTxState | null; transaction: VegaTxState | null;
@@ -3,13 +3,12 @@ import {
useNetworkParams, useNetworkParams,
} from '@vegaprotocol/network-parameters'; } from '@vegaprotocol/network-parameters';
import { BigNumber } from '../../../lib/bignumber'; import { BigNumber } from '../../../lib/bignumber';
import type { ProposalFieldsFragment } from '../proposals/__generated__/Proposals'; import { type Proposal } from '../types';
import type { ProposalQuery } from '../proposal/__generated__/Proposal';
export const useProposalNetworkParams = ({ export const useProposalNetworkParams = ({
proposal, proposal,
}: { }: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal']; proposal: Proposal;
}) => { }) => {
const { params } = useNetworkParams([ const { params } = useNetworkParams([
NetworkParams.governance_proposal_updateMarket_requiredMajority, NetworkParams.governance_proposal_updateMarket_requiredMajority,
@@ -2,15 +2,10 @@ import { useMemo } from 'react';
import { useAppState } from '../../../contexts/app-state/app-state-context'; import { useAppState } from '../../../contexts/app-state/app-state-context';
import { BigNumber } from '../../../lib/bignumber'; import { BigNumber } from '../../../lib/bignumber';
import { useProposalNetworkParams } from './use-proposal-network-params'; 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 { addDecimal } from '@vegaprotocol/utils';
import { type Proposal } from '../types';
export const useVoteInformation = ({ export const useVoteInformation = ({ proposal }: { proposal: Proposal }) => {
proposal,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
}) => {
const { const {
appState: { totalSupply, decimals }, appState: { totalSupply, decimals },
} = useAppState(); } = useAppState();
@@ -86,229 +86,65 @@ query Proposal(
$includeUpdateReferralProgram: Boolean! $includeUpdateReferralProgram: Boolean!
) { ) {
proposal(id: $proposalId) { proposal(id: $proposalId) {
id ... on Proposal {
rationale {
title
description
}
reference
state
datetime
rejectionReason
party {
id id
} rationale {
errorDetails title
...NewMarketProductField @include(if: $includeNewMarketProductField) description
...UpdateMarketState @include(if: $includeUpdateMarketState) }
...UpdateReferralProgram @include(if: $includeUpdateReferralProgram) reference
...UpdateVolumeDiscountProgram state
terms { datetime
closingDatetime rejectionReason
enactmentDatetime party {
change { id
... on NewMarket { }
decimalPlaces errorDetails
metadata ...NewMarketProductField @include(if: $includeNewMarketProductField)
riskParameters { ...UpdateMarketState @include(if: $includeUpdateMarketState)
... on LogNormalRiskModel { ...UpdateReferralProgram @include(if: $includeUpdateReferralProgram)
riskAversionParameter ...UpdateVolumeDiscountProgram
tau terms {
params { closingDatetime
mu enactmentDatetime
r change {
sigma ... on NewMarket {
} decimalPlaces
} metadata
... on SimpleRiskModel { riskParameters {
params { ... on LogNormalRiskModel {
factorLong riskAversionParameter
factorShort tau
} params {
} mu
} r
instrument { sigma
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
}
}
}
}
}
}
} }
} }
... on PerpetualProduct { ... on SimpleRiskModel {
settlementAsset { params {
id factorLong
name factorShort
symbol
decimals
quantum
} }
quoteName
} }
} }
}
priceMonitoringParameters {
triggers {
horizonSecs
probability
auctionExtensionSecs
}
}
liquidityMonitoringParameters {
targetStakeParameters {
timeWindow
scalingFactor
}
}
positionDecimalPlaces
linearSlippageFactor
quadraticSlippageFactor
}
... on UpdateMarket {
marketId
updateMarketConfiguration {
instrument { instrument {
name
code code
product { product {
... on UpdateFutureProduct { ... on FutureProduct {
quoteName settlementAsset {
dataSourceSpecForSettlementData { id
sourceType { name
... on DataSourceDefinitionInternal { symbol
sourceType { decimals
... on DataSourceSpecConfigurationTime { quantum
conditions {
operator
value
}
}
}
}
... on DataSourceDefinitionExternal {
sourceType {
... on DataSourceSpecConfiguration {
signers {
signer {
... on PubKey {
key
}
... on ETHAddress {
address
}
}
}
filters {
key {
name
type
}
conditions {
operator
value
}
}
}
}
}
}
} }
# dataSourceSpecForTradingTermination { quoteName
# 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 { dataSourceSpecBinding {
settlementDataProperty settlementDataProperty
tradingTerminationProperty tradingTerminationProperty
} }
}
... on UpdatePerpetualProduct {
quoteName
dataSourceSpecForSettlementData { dataSourceSpecForSettlementData {
sourceType { sourceType {
... on DataSourceDefinitionInternal { ... on DataSourceDefinitionInternal {
@@ -349,14 +185,19 @@ query Proposal(
} }
} }
} }
dataSourceSpecBinding { }
settlementDataProperty ... on PerpetualProduct {
settlementScheduleProperty settlementAsset {
id
name
symbol
decimals
quantum
} }
quoteName
} }
} }
} }
metadata
priceMonitoringParameters { priceMonitoringParameters {
triggers { triggers {
horizonSecs horizonSecs
@@ -370,71 +211,232 @@ query Proposal(
scalingFactor scalingFactor
} }
} }
riskParameters { positionDecimalPlaces
... on UpdateMarketSimpleRiskModel { linearSlippageFactor
simple { quadraticSlippageFactor
factorLong }
factorShort ... 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 { metadata
logNormal { priceMonitoringParameters {
riskAversionParameter triggers {
tau horizonSecs
params { probability
r auctionExtensionSecs
sigma }
mu }
liquidityMonitoringParameters {
targetStakeParameters {
timeWindow
scalingFactor
}
}
riskParameters {
... on UpdateMarketSimpleRiskModel {
simple {
factorLong
factorShort
}
}
... on UpdateMarketLogNormalRiskModel {
logNormal {
riskAversionParameter
tau
params {
r
sigma
mu
}
} }
} }
} }
} }
} }
} ... on NewAsset {
... on NewAsset { name
name symbol
symbol decimals
decimals quantum
quantum source {
source { ... on BuiltinAsset {
... on BuiltinAsset { maxFaucetAmountMint
maxFaucetAmountMint }
} ... on ERC20 {
... on ERC20 { contractAddress
contractAddress lifetimeLimit
lifetimeLimit withdrawThreshold
withdrawThreshold }
} }
} }
} ... on UpdateNetworkParameter {
... on UpdateNetworkParameter { networkParameter {
networkParameter { key
key value
value }
} }
} ... on UpdateAsset {
... on UpdateAsset { quantum
quantum assetId
assetId source {
source { ... on UpdateERC20 {
... on UpdateERC20 { lifetimeLimit
lifetimeLimit withdrawThreshold
withdrawThreshold }
} }
} }
} }
} }
} votes {
votes { yes {
yes { totalTokens
totalTokens totalNumber
totalNumber totalEquityLikeShareWeight
totalEquityLikeShareWeight }
} no {
no { totalTokens
totalTokens totalNumber
totalNumber totalEquityLikeShareWeight
totalEquityLikeShareWeight }
} }
} }
} }
File diff suppressed because one or more lines are too long
@@ -17,6 +17,7 @@ import {
import { useParentMarketIdQuery } from '@vegaprotocol/markets'; import { useParentMarketIdQuery } from '@vegaprotocol/markets';
import { useFeatureFlags } from '@vegaprotocol/environment'; import { useFeatureFlags } from '@vegaprotocol/environment';
import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals'; import { useSuccessorMarketProposalDetails } from '@vegaprotocol/proposals';
import { type Proposal as IProposal } from '../types';
export const ProposalContainer = () => { export const ProposalContainer = () => {
const featureFlags = useFeatureFlags((state) => state.flags); const featureFlags = useFeatureFlags((state) => state.flags);
@@ -67,6 +68,8 @@ export const ProposalContainer = () => {
skip: !params.proposalId, skip: !params.proposalId,
}); });
const proposal = data?.proposal as IProposal;
const successor = useSuccessorMarketProposalDetails(params.proposalId); const successor = useSuccessorMarketProposalDetails(params.proposalId);
const isSuccessor = !!successor?.parentMarketId || !!successor.code; const isSuccessor = !!successor?.parentMarketId || !!successor.code;
@@ -79,12 +82,12 @@ export const ProposalContainer = () => {
}, },
} = useFetch( } = useFetch(
`${ENV.rest}governance?proposalId=${ `${ENV.rest}governance?proposalId=${
data?.proposal?.terms.change.__typename === 'UpdateMarket' && proposal?.terms.change.__typename === 'UpdateMarket' &&
data?.proposal.terms.change.marketId proposal.terms.change.marketId
}`, }`,
undefined, undefined,
true, true,
data?.proposal?.terms.change.__typename !== 'UpdateMarket' proposal?.terms.change.__typename !== 'UpdateMarket'
); );
const { const {
@@ -97,7 +100,7 @@ export const ProposalContainer = () => {
`${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`, `${ENV.rest}governances?proposalState=STATE_ENACTED&proposalType=TYPE_UPDATE_MARKET`,
undefined, undefined,
true, true,
data?.proposal?.terms.change.__typename !== 'UpdateMarket' proposal?.terms.change.__typename !== 'UpdateMarket'
); );
const { const {
@@ -108,8 +111,8 @@ export const ProposalContainer = () => {
dataProvider: marketInfoProvider, dataProvider: marketInfoProvider,
skipUpdates: true, skipUpdates: true,
variables: { variables: {
marketId: data?.proposal?.id || '', marketId: proposal?.id || '',
skip: !data?.proposal?.id, skip: !proposal?.id,
}, },
}); });
@@ -148,23 +151,22 @@ export const ProposalContainer = () => {
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
variables: { variables: {
assetId: assetId:
(data?.proposal?.terms.change.__typename === 'NewAsset' && (proposal?.terms.change.__typename === 'NewAsset' && proposal?.id) ||
data?.proposal?.id) || (proposal?.terms.change.__typename === 'UpdateAsset' &&
(data?.proposal?.terms.change.__typename === 'UpdateAsset' && proposal.terms.change.assetId) ||
data.proposal.terms.change.assetId) ||
'', '',
}, },
skip: !['NewAsset', 'UpdateAsset'].includes( skip: !['NewAsset', 'UpdateAsset'].includes(
data?.proposal?.terms?.change?.__typename || '' proposal?.terms?.change?.__typename || ''
), ),
}); });
useEffect(() => { useEffect(() => {
if ( if (
previouslyEnactedMarketProposalsRestData && 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 = const filteredProposals =
// @ts-ignore rest data is not typed // @ts-ignore rest data is not typed
@@ -188,8 +190,8 @@ export const ProposalContainer = () => {
}, [ }, [
previouslyEnactedMarketProposalsRestData, previouslyEnactedMarketProposalsRestData,
params.proposalId, params.proposalId,
data?.proposal?.terms.change.__typename, proposal?.terms.change.__typename,
data?.proposal?.terms.change, proposal?.terms.change,
]); ]);
useEffect(() => { useEffect(() => {
@@ -242,7 +244,7 @@ export const ProposalContainer = () => {
> >
{data?.proposal ? ( {data?.proposal ? (
<Proposal <Proposal
proposal={data.proposal} proposal={proposal}
networkParams={networkParams} networkParams={networkParams}
restData={restData} restData={restData}
marketData={marketData} marketData={marketData}
@@ -8,6 +8,7 @@ import mergeWith from 'lodash/mergeWith';
import { type PartialDeep } from 'type-fest'; import { type PartialDeep } from 'type-fest';
import { type ProposalQuery } from '../proposal/__generated__/Proposal'; import { type ProposalQuery } from '../proposal/__generated__/Proposal';
import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals'; import { type ProtocolUpgradeProposalFieldsFragment } from '@vegaprotocol/proposals';
import { type Proposal } from '../types';
export function generateProtocolUpgradeProposal( export function generateProtocolUpgradeProposal(
override: PartialDeep<ProtocolUpgradeProposalFieldsFragment> = {} override: PartialDeep<ProtocolUpgradeProposalFieldsFragment> = {}
@@ -43,8 +44,8 @@ export function generateProtocolUpgradeProposal(
} }
export function generateProposal( export function generateProposal(
override: PartialDeep<ProposalQuery['proposal']> = {} override: PartialDeep<Proposal> = {}
): ProposalQuery['proposal'] { ): Proposal {
const defaultProposal: ProposalQuery['proposal'] = { const defaultProposal: ProposalQuery['proposal'] = {
__typename: 'Proposal', __typename: 'Proposal',
id: faker.datatype.uuid(), id: faker.datatype.uuid(),
@@ -92,15 +93,16 @@ export function generateProposal(
}, },
}; };
return mergeWith< return mergeWith<Proposal, PartialDeep<Proposal>>(
ProposalQuery['proposal'], defaultProposal,
PartialDeep<ProposalQuery['proposal']> override,
>(defaultProposal, override, (objValue, srcValue) => { (objValue, srcValue) => {
if (!isArray(objValue)) { if (!isArray(objValue)) {
return; return;
}
return srcValue;
} }
return srcValue; );
});
} }
type Vote = Pick<Schema.Vote, '__typename' | 'value' | 'party' | 'datetime'>; 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' }
>;
@@ -0,0 +1,332 @@
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 { 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">
<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="lg:gap-6 container p-4 mx-auto">
<div className="mx-auto md:w-2/3 max-w-xl">
<Box className="flex flex-col gap-4">
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
{t('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>
</div>
</div>
</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,392 @@
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,
} 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 { 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>
<p>Loading...</p>
</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 (
<div className="relative h-full overflow-y-auto pt-5">
<div className="absolute top-0 left-0 w-full h-[40%] -z-10 bg-[40%_0px] bg-cover bg-no-repeat bg-local bg-[url(/cover.png)]">
<div className="absolute top-o left-0 w-full h-full bg-gradient-to-t from-white dark:from-vega-cdark-900 to-transparent from-20% to-60%" />
</div>
<div className="flex flex-col gap-4 lg:gap-6 container p-4 mx-auto">
<header className="flex gap-3 lg:gap-4 pt-5 lg:pt-10">
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
<div className="flex flex-col items-start gap-1 lg:gap-3">
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
{team.name}
</h1>
<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>
</div>
</div>
);
};
const Games = ({ games }: { games?: TeamGame[] }) => {
const t = useT();
if (!games?.length) {
return <p>{t('No games')}</p>;
}
return (
<Table
columns={[
{ name: 'rank', displayName: t('Rank') },
{
name: 'epoch',
displayName: t('Epoch'),
headerClassName: 'hidden md:block',
className: 'hidden md:block',
},
{ name: 'type', displayName: t('Type') },
{ name: 'amount', displayName: t('Amount earned') },
{
name: 'teams',
displayName: t('No. of participating teams'),
headerClassName: 'hidden md:block',
className: 'hidden md:block',
},
{ name: 'status', displayName: t('Status') },
]}
data={games.map((game) => ({
rank: game.team.rank,
epoch: game.epoch,
type: game.team.rewardMetric,
amount: game.team.totalRewardsEarned,
teams: game.numberOfParticipants,
}))}
noCollapse={true}
/>
);
};
const Members = ({ members }: { members?: Member[] }) => {
const t = useT();
if (!members?.length) {
return <p>{t('No members')}</p>;
}
const data = orderBy(
members.map((m) => ({
referee: <RefereeCell pubkey={m.referee} />,
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
joinedAtEpoch: Number(m.joinedAtEpoch),
explorerLink: <RefereeLink pubkey={m.referee} />,
})),
'joinedAtEpoch',
'desc'
);
return (
<Table
columns={[
{ name: 'referee', displayName: t('Referee') },
{
name: 'joinedAt',
displayName: t('Joined at'),
},
{
name: 'joinedAtEpoch',
displayName: t('Joined epoch'),
headerClassName: 'text-right',
className: 'text-right',
},
{
name: 'explorerLink',
displayName: '',
headerClassName: 'hidden md:block',
className: 'hidden md:block text-right',
},
]}
data={data}
noCollapse={true}
/>
);
};
const RefereeCell = ({ pubkey }: { pubkey: string }) => {
return <span title={pubkey}>{truncateMiddle(pubkey)}</span>;
};
const RefereeLink = ({ pubkey }: { pubkey: string }) => {
const t = useT();
const linkCreator = useLinks(DApp.Explorer);
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
return (
<Link to={link} className="underline underline-offset-4">
{t('View on explorer')}
</Link>
);
};
const 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, VegaIcon,
VegaIconNames, VegaIconNames,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import type { FieldValues } from 'react-hook-form';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import classNames from 'classnames'; import classNames from 'classnames';
import { Navigate, useNavigate, useSearchParams } from 'react-router-dom'; import { Navigate, useNavigate, useSearchParams } from 'react-router-dom';
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react'; import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback } from 'react';
import { RainbowButton } from './buttons'; import { RainbowButton } from '../../components/rainbow-button';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet'; import {
useSimpleTransaction,
useVegaWallet,
useVegaWalletDialogStore,
} from '@vegaprotocol/wallet';
import { useIsInReferralSet, useReferral } from './hooks/use-referral'; import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { Routes } from '../../lib/links'; import { Routes } from '../../lib/links';
import { useTransactionEventSubscription } from '@vegaprotocol/web3';
import { Statistics, useStats } from './referral-statistics'; import { Statistics, useStats } from './referral-statistics';
import { useReferralProgram } from './hooks/use-referral-program'; import { useReferralProgram } from './hooks/use-referral-program';
import { ns, useT } from '../../lib/use-t'; import { ns, useT } from '../../lib/use-t';
@@ -73,6 +75,10 @@ export const ApplyCodeFormContainer = ({
return <ApplyCodeForm onSuccess={onSuccess} />; return <ApplyCodeForm onSuccess={onSuccess} />;
}; };
type FormFields = {
code: string;
};
export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => { export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
const t = useT(); const t = useT();
const program = useReferralProgram(); const program = useReferralProgram();
@@ -81,31 +87,47 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
(store) => store.openVegaWalletDialog (store) => store.openVegaWalletDialog
); );
const [status, setStatus] = useState< const { isReadOnly, pubKey } = useVegaWallet();
'requested' | 'no-funds' | 'successful' | null
>(null);
const txHash = useRef<string | null>(null);
const { isReadOnly, pubKey, sendTx } = useVegaWallet();
const { isEligible, requiredFunds } = useFundsAvailable(); const { isEligible, requiredFunds } = useFundsAvailable();
const currentRouteId = useGetCurrentRouteId(); const currentRouteId = useGetCurrentRouteId();
const setViews = useSidebar((s) => s.setViews); const setViews = useSidebar((s) => s.setViews);
const [params] = useSearchParams();
const { const {
register, register,
handleSubmit, handleSubmit,
formState: { errors }, formState: { errors },
setValue,
setError, setError,
watch, watch,
} = useForm(); } = useForm<FormFields>({
const [params] = useSearchParams(); defaultValues: {
code: params.get('code') || '',
},
});
const codeField = watch('code'); const codeField = watch('code');
const { data: previewData, loading: previewLoading } = useReferral({ const { data: previewData, loading: previewLoading } = useReferral({
code: validateCode(codeField, t) ? codeField : undefined, code: validateCode(codeField, t) ? codeField : undefined,
}); });
const { send, status } = useSimpleTransaction({
onSuccess: () => {
// go to main page when successfully applied
setTimeout(() => {
if (onSuccess) onSuccess();
navigate(Routes.REFERRALS);
}, RELOAD_DELAY);
},
onError: (msg) => {
setError('code', {
type: 'required',
message: msg,
});
},
});
/** /**
* Validates if a connected party can apply a code (min funds span protection) * Validates if a connected party can apply a code (min funds span protection)
*/ */
@@ -135,99 +157,55 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
return true; return true;
}, [codeField, previewData, previewLoading, t]); }, [codeField, previewData, previewLoading, t]);
useEffect(() => { const noFunds = validateFundsAvailable() !== true ? true : false;
const code = params.get('code');
if (code) setValue('code', code);
}, [params, setValue]);
useEffect(() => { const onSubmit = ({ code }: FormFields) => {
const err = validateFundsAvailable();
if (err !== true) {
setStatus('no-funds');
} else {
setStatus(null);
}
}, [isEligible, validateFundsAvailable]);
const onSubmit = ({ code }: FieldValues) => {
if (isReadOnly || !pubKey || !code || code.length === 0) { if (isReadOnly || !pubKey || !code || code.length === 0) {
return; return;
} }
setStatus('requested'); send({
sendTx(pubKey, {
applyReferralCode: { applyReferralCode: {
id: code as string, id: code as string,
}, },
}) });
.then((res) => {
if (!res) {
setError('code', {
type: 'required',
message: t('The transaction could not be sent'),
});
}
if (res) {
txHash.current = res.transactionHash.toLowerCase();
}
})
.catch((err) => {
if (err.message.includes('user rejected')) {
setStatus(null);
} else {
setStatus(null);
setError('code', {
type: 'required',
message:
err instanceof Error
? err.message
: t('Your code has been rejected'),
});
}
});
};
useTransactionEventSubscription({ // sendTx(pubKey, {
variables: { partyId: pubKey || '' }, // applyReferralCode: {
skip: !pubKey, // id: code as string,
fetchPolicy: 'no-cache', // },
onData: ({ data: result }) => // })
result.data?.busEvents?.forEach((event) => { // .then((res) => {
if (event.event.__typename === 'TransactionResult') { // if (!res) {
const hash = event.event.hash.toLowerCase(); // setError('code', {
if (txHash.current && txHash.current === hash) { // type: 'required',
const err = event.event.error; // message: t('The transaction could not be sent'),
const status = event.event.status; // });
if (err) { // }
setStatus(null); // if (res) {
setError('code', { // txHash.current = res.transactionHash.toLowerCase();
type: 'required', // }
message: err, // })
}); // .catch((err) => {
} // if (err.message.includes('user rejected')) {
if (status && !err) { // setStatus(null);
setStatus('successful'); // } else {
} // setStatus(null);
} // setError('code', {
} // type: 'required',
}), // message:
}); // err instanceof Error
// ? err.message
// : t('Your code has been rejected'),
// });
// }
// });
};
const { epochsValue, nextBenefitTierValue } = useStats({ program }); const { epochsValue, nextBenefitTierValue } = useStats({ program });
// go to main page when successfully applied
useEffect(() => {
if (status === 'successful') {
setTimeout(() => {
if (onSuccess) onSuccess();
navigate(Routes.REFERRALS);
}, RELOAD_DELAY);
}
}, [navigate, onSuccess, status]);
// show "code applied" message when successfully applied // show "code applied" message when successfully applied
if (status === 'successful') { if (status === 'confirmed') {
return ( return (
<div className="mx-auto w-1/2"> <div className="mx-auto w-1/2">
<h3 className="calt mb-5 flex flex-row items-center justify-center gap-2 text-center text-xl uppercase"> <h3 className="calt mb-5 flex flex-row items-center justify-center gap-2 text-center text-xl uppercase">
@@ -261,7 +239,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
}; };
} }
if (status === 'no-funds') { if (noFunds) {
return { return {
disabled: false, disabled: false,
children: t('Deposit funds'), children: t('Deposit funds'),
@@ -332,7 +310,7 @@ export const ApplyCodeForm = ({ onSuccess }: { onSuccess?: () => void }) => {
</label> </label>
<RainbowButton variant="border" {...getButtonProps()} /> <RainbowButton variant="border" {...getButtonProps()} />
</form> </form>
{status === 'no-funds' ? ( {noFunds ? (
<InputError intent="warning" className="overflow-auto break-words"> <InputError intent="warning" className="overflow-auto break-words">
<span> <span>
<SpamProtectionErr requiredFunds={requiredFunds?.toString()} /> <SpamProtectionErr requiredFunds={requiredFunds?.toString()} />
@@ -4,41 +4,6 @@ import type { ComponentProps, ButtonHTMLAttributes } from 'react';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { NavLink } from 'react-router-dom'; import { NavLink } from 'react-router-dom';
type RainbowButtonProps = {
variant?: 'full' | 'border';
};
export const RainbowButton = ({
variant = 'full',
children,
className,
...props
}: RainbowButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={classNames(
'bg-rainbow rounded-lg overflow-hidden disabled:opacity-40',
'hover:bg-rainbow-180 hover:animate-spin-rainbow',
{
'px-5 py-3 text-white': variant === 'full',
'p-[0.125rem]': variant === 'border',
}
)}
{...props}
>
<div
className={classNames(
{
'bg-vega-clight-800 dark:bg-vega-cdark-800 text-black dark:text-white px-5 py-3 rounded-[0.35rem] overflow-hidden':
variant === 'border',
},
className
)}
>
{children}
</div>
</button>
);
const RAINBOW_TAB_STYLE = classNames( const RAINBOW_TAB_STYLE = classNames(
'inline-block', 'inline-block',
'bg-vega-clight-500 dark:bg-vega-cdark-500', 'bg-vega-clight-500 dark:bg-vega-cdark-500',
@@ -1,9 +1,5 @@
import { import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
useVegaWallet, import { RainbowButton } from '../../components/rainbow-button';
useVegaWalletDialogStore,
determineId,
} from '@vegaprotocol/wallet';
import { RainbowButton } from './buttons';
import { useState } from 'react'; import { useState } from 'react';
import { import {
CopyWithTooltip, CopyWithTooltip,
@@ -18,13 +14,13 @@ import {
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { addDecimalsFormatNumber } from '@vegaprotocol/utils'; import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment'; import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
import { useStakeAvailable } from './hooks/use-stake-available';
import { ABOUT_REFERRAL_DOCS_LINK } from './constants'; import { ABOUT_REFERRAL_DOCS_LINK } from './constants';
import { useIsInReferralSet, useReferral } from './hooks/use-referral'; import { useIsInReferralSet, useReferral } from './hooks/use-referral';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { Navigate } from 'react-router-dom'; import { Navigate } from 'react-router-dom';
import { Routes } from '../../lib/links'; import { Routes } from '../../lib/links';
import { useReferralProgram } from './hooks/use-referral-program'; import { useReferralProgram } from './hooks/use-referral-program';
import { useCreateReferralSet } from '../../lib/hooks/use-create-referral-set';
export const CreateCodeContainer = () => { export const CreateCodeContainer = () => {
const { pubKey } = useVegaWallet(); const { pubKey } = useVegaWallet();
@@ -95,67 +91,42 @@ const CreateCodeDialog = ({
}) => { }) => {
const t = useT(); const t = useT();
const createLink = useLinks(DApp.Governance); const createLink = useLinks(DApp.Governance);
const { isReadOnly, pubKey, sendTx } = useVegaWallet(); const { pubKey } = useVegaWallet();
const { refetch } = useReferral({ pubKey, role: 'referrer' }); const { refetch } = useReferral({ pubKey, role: 'referrer' });
const [err, setErr] = useState<string | null>(null); const {
const [code, setCode] = useState<string | null>(null); err,
const [status, setStatus] = useState< code,
'idle' | 'loading' | 'success' | 'error' status,
>('idle'); stakeAvailable: currentStakeAvailable,
requiredStake,
const { stakeAvailable: currentStakeAvailable, requiredStake } = onSubmit,
useStakeAvailable(); } = useCreateReferralSet();
const { details: programDetails } = useReferralProgram(); const { details: programDetails } = useReferralProgram();
const onSubmit = () => {
if (isReadOnly || !pubKey) {
setErr('Not connected');
} else {
setErr(null);
setStatus('loading');
setCode(null);
sendTx(pubKey, {
createReferralSet: {
isTeam: false,
},
})
.then((res) => {
if (!res) {
setErr(`Invalid response: ${JSON.stringify(res)}`);
return;
}
const code = determineId(res.signature);
setCode(code);
setStatus('success');
})
.catch((err) => {
if (err.message.includes('user rejected')) {
setStatus('idle');
return;
}
setStatus('error');
setErr(err.message);
});
}
};
const getButtonProps = () => { const getButtonProps = () => {
if (status === 'idle' || status === 'error') { if (status === 'idle') {
return { return {
children: t('Generate code'), children: t('Generate code'),
onClick: () => onSubmit(), onClick: () => onSubmit({ createReferralSet: { isTeam: false } }),
}; };
} }
if (status === 'loading') { if (status === 'requested') {
return { return {
children: t('Confirm in wallet...'), children: t('Confirm in wallet...'),
disabled: true, disabled: true,
}; };
} }
if (status === 'success') { if (status === 'pending') {
return {
children: t('Waiting for transaction...'),
disabled: true,
};
}
if (status === 'confirmed') {
return { return {
children: t('Close'), children: t('Close'),
intent: Intent.Success, intent: Intent.Success,
@@ -209,7 +180,10 @@ const CreateCodeDialog = ({
if (!programDetails) { if (!programDetails) {
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && ( {(status === 'idle' ||
status === 'requested' ||
status === 'pending' ||
err) && (
<> <>
{ {
<p> <p>
@@ -220,7 +194,7 @@ const CreateCodeDialog = ({
} }
</> </>
)} )}
{status === 'success' && code && ( {status === 'confirmed' && code && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700"> <div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
<p className="overflow-hidden whitespace-nowrap text-ellipsis"> <p className="overflow-hidden whitespace-nowrap text-ellipsis">
@@ -240,7 +214,7 @@ const CreateCodeDialog = ({
<TradingButton <TradingButton
fill={true} fill={true}
intent={Intent.Primary} intent={Intent.Primary}
onClick={() => onSubmit()} onClick={() => onSubmit({ createReferralSet: { isTeam: false } })}
{...getButtonProps()} {...getButtonProps()}
> >
{t('Yes')} {t('Yes')}
@@ -269,14 +243,17 @@ const CreateCodeDialog = ({
return ( return (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{(status === 'idle' || status === 'loading' || status === 'error') && ( {(status === 'idle' ||
status === 'requested' ||
status === 'pending' ||
err) && (
<p> <p>
{t( {t(
'Generate a referral code to share with your friends and access the commission benefits of the current program.' 'Generate a referral code to share with your friends and access the commission benefits of the current program.'
)} )}
</p> </p>
)} )}
{status === 'success' && code && ( {status === 'confirmed' && code && (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700"> <div className="flex-1 min-w-0 p-2 text-sm rounded bg-vega-clight-700 dark:bg-vega-cdark-700">
<p className="overflow-hidden whitespace-nowrap text-ellipsis"> <p className="overflow-hidden whitespace-nowrap text-ellipsis">
@@ -1,5 +1,5 @@
import { isRouteErrorResponse, useNavigate, useRouteError } from 'react-router'; import { isRouteErrorResponse, useNavigate, useRouteError } from 'react-router';
import { RainbowButton } from './buttons'; import { RainbowButton } from '../../components/rainbow-button';
import { AnimatedDudeWithWire } from './graphics/dude'; import { AnimatedDudeWithWire } from './graphics/dude';
import { LayoutWithSky } from './layout'; import { LayoutWithSky } from './layout';
import { Routes } from '../../lib/links'; import { Routes } from '../../lib/links';
@@ -8,13 +8,13 @@ import type {
ReferralSetsQueryVariables, ReferralSetsQueryVariables,
} from './__generated__/ReferralSets'; } from './__generated__/ReferralSets';
import { useReferralSetsQuery } from './__generated__/ReferralSets'; import { useReferralSetsQuery } from './__generated__/ReferralSets';
import { useStakeAvailable } from './use-stake-available'; import { useStakeAvailable } from '../../../lib/hooks/use-stake-available';
export const DEFAULT_AGGREGATION_DAYS = 30; export const DEFAULT_AGGREGATION_DAYS = 30;
export type Role = 'referrer' | 'referee'; export type Role = 'referrer' | 'referee';
type UseReferralArgs = ( type UseReferralArgs = (
| { code: string } | { code: string | undefined }
| { pubKey: string | null; role: Role } | { pubKey: string | null; role: Role }
) & { ) & {
aggregationEpochs?: number; aggregationEpochs?: number;
@@ -1,6 +1,9 @@
import { MockedProvider, type MockedResponse } from '@apollo/react-testing'; import { MockedProvider, type MockedResponse } from '@apollo/react-testing';
import { render, waitFor } from '@testing-library/react'; import { render, screen, waitFor } from '@testing-library/react';
import { type VegaWalletContextShape } from '@vegaprotocol/wallet'; import {
VegaWalletContext,
type VegaWalletContextShape,
} from '@vegaprotocol/wallet';
import { ReferralStatistics } from './referral-statistics'; import { ReferralStatistics } from './referral-statistics';
import { import {
ReferralProgramDocument, ReferralProgramDocument,
@@ -15,7 +18,7 @@ import {
StakeAvailableDocument, StakeAvailableDocument,
type StakeAvailableQueryVariables, type StakeAvailableQueryVariables,
type StakeAvailableQuery, type StakeAvailableQuery,
} from './hooks/__generated__/StakeAvailable'; } from '../../lib/hooks/__generated__/StakeAvailable';
import { import {
RefereesDocument, RefereesDocument,
type RefereesQueryVariables, type RefereesQueryVariables,
@@ -296,122 +299,99 @@ const refereesMock30: MockedResponse<RefereesQuery, RefereesQueryVariables> = {
}, },
}; };
jest.mock('@vegaprotocol/wallet', () => {
return {
...jest.requireActual('@vegaprotocol/wallet'),
useVegaWallet: () => {
const ctx: Partial<VegaWalletContextShape> = {
pubKey: MOCK_PUBKEY,
};
return ctx;
},
};
});
describe('ReferralStatistics', () => { describe('ReferralStatistics', () => {
it('displays apply code when no data has been found for given pubkey', () => { const renderComponent = (mocks: MockedResponse[]) => {
const { queryByTestId } = render( const walletContext = {
pubKey: MOCK_PUBKEY,
isReadOnly: false,
sendTx: jest.fn(),
} as unknown as VegaWalletContextShape;
return render(
<MemoryRouter> <MemoryRouter>
<MockedProvider mocks={[]} showWarnings={false}> <VegaWalletContext.Provider value={walletContext}>
<ReferralStatistics /> <MockedProvider mocks={mocks} showWarnings={false}>
</MockedProvider> <ReferralStatistics />
</MockedProvider>
</VegaWalletContext.Provider>
</MemoryRouter> </MemoryRouter>
); );
};
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument(); it('displays apply code when no data has been found for given pubkey', () => {
renderComponent([]);
expect(
screen.queryByTestId('referral-apply-code-form')
).toBeInTheDocument();
}); });
it('displays referrer stats when given pubkey is a referrer', async () => { it('displays referrer stats when given pubkey is a referrer', async () => {
const { queryByTestId } = render( renderComponent([
<MemoryRouter> programMock,
<MockedProvider referralSetAsReferrerMock,
mocks={[ noReferralSetAsRefereeMock,
programMock, stakeAvailableMock,
referralSetAsReferrerMock, refereesMock,
noReferralSetAsRefereeMock, refereesMock30,
stakeAvailableMock, ]);
refereesMock,
refereesMock30,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => { await waitFor(() => {
expect( expect(
queryByTestId('referral-create-code-form') screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument(); expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual( expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referrer' 'referrer'
); );
// gets commision from 30 epochs query // gets commision from 30 epochs query
expect(queryByTestId('total-commission-value')).toHaveTextContent( expect(screen.queryByTestId('total-commission-value')).toHaveTextContent(
'12,340' '12,340'
); );
}); });
}); });
it('displays referee stats when given pubkey is a referee', async () => { it('displays referee stats when given pubkey is a referee', async () => {
const { queryByTestId } = render( renderComponent([
<MemoryRouter> programMock,
<MockedProvider noReferralSetAsReferrerMock,
mocks={[ referralSetAsRefereeMock,
programMock, stakeAvailableMock,
noReferralSetAsReferrerMock, refereesMock,
referralSetAsRefereeMock, ]);
stakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => { await waitFor(() => {
expect( expect(
queryByTestId('referral-create-code-form') screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument(); expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual( expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee' 'referee'
); );
}); });
}); });
it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => { it('displays eligibility warning when the set is no longer valid due to the referrers stake', async () => {
const { queryByTestId } = render( renderComponent([
<MemoryRouter> programMock,
<MockedProvider noReferralSetAsReferrerMock,
mocks={[ referralSetAsRefereeMock,
programMock, nonEligibleStakeAvailableMock,
noReferralSetAsReferrerMock, refereesMock,
referralSetAsRefereeMock, ]);
nonEligibleStakeAvailableMock,
refereesMock,
]}
showWarnings={false}
>
<ReferralStatistics />
</MockedProvider>
</MemoryRouter>
);
await waitFor(() => { await waitFor(() => {
expect( expect(
queryByTestId('referral-create-code-form') screen.queryByTestId('referral-create-code-form')
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
expect(queryByTestId('referral-statistics')).toBeInTheDocument(); expect(screen.queryByTestId('referral-statistics')).toBeInTheDocument();
expect(queryByTestId('referral-statistics')?.dataset.as).toEqual( expect(screen.queryByTestId('referral-statistics')?.dataset.as).toEqual(
'referee' 'referee'
); );
expect(queryByTestId('referral-eligibility-warning')).toBeInTheDocument(); expect(
expect(queryByTestId('referral-apply-code-form')).toBeInTheDocument(); screen.queryByTestId('referral-eligibility-warning')
).toBeInTheDocument();
expect(
screen.queryByTestId('referral-apply-code-form')
).toBeInTheDocument();
}); });
}); });
}); });
@@ -26,7 +26,7 @@ import {
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats'; import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
import compact from 'lodash/compact'; import compact from 'lodash/compact';
import { useReferralProgram } from './hooks/use-referral-program'; 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 sortBy from 'lodash/sortBy';
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch'; 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'; } from '@vegaprotocol/candles-chart';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { useChartSettings, STUDY_SIZE } from './use-chart-settings'; import { useChartSettings, STUDY_SIZE } from './use-chart-settings';
import { SUPPORTED_INTERVALS, type SupportedInterval } from './constants';
/** /**
* Renders either the pennant chart or the tradingview chart * Renders either the pennant chart or the tradingview chart
@@ -36,7 +37,7 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
const pennantChart = ( const pennantChart = (
<CandlesChartContainer <CandlesChartContainer
marketId={marketId} marketId={marketId}
interval={toPennantInterval(interval)} interval={toPennantInterval(interval as SupportedInterval)}
chartType={chartType} chartType={chartType}
overlays={overlays} overlays={overlays}
studies={studies} studies={studies}
@@ -63,7 +64,7 @@ export const ChartContainer = ({ marketId }: { marketId: string }) => {
libraryPath={CHARTING_LIBRARY_PATH} libraryPath={CHARTING_LIBRARY_PATH}
libraryHash={CHARTING_LIBRARY_HASH} libraryHash={CHARTING_LIBRARY_HASH}
marketId={marketId} marketId={marketId}
interval={toTradingViewResolution(interval)} interval={toTradingViewResolution(interval as SupportedInterval)}
studies={tradingViewStudies} studies={tradingViewStudies}
onIntervalChange={(newInterval) => { onIntervalChange={(newInterval) => {
setInterval(fromTradingViewResolution(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]; const resolution = TRADINGVIEW_INTERVAL_MAP[interval];
if (!resolution) { if (!resolution) {
@@ -107,7 +112,11 @@ const fromTradingViewResolution = (resolution: string) => {
return interval as Interval; 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]; const pennantInterval = PENNANT_INTERVAL_MAP[interval];
if (!pennantInterval) { if (!pennantInterval) {
@@ -18,21 +18,13 @@ import {
TradingDropdownTrigger, TradingDropdownTrigger,
Icon, Icon,
} from '@vegaprotocol/ui-toolkit'; } from '@vegaprotocol/ui-toolkit';
import { Interval } from '@vegaprotocol/types'; import { type Interval } from '@vegaprotocol/types';
import { useEnvironment } from '@vegaprotocol/environment'; import { useEnvironment } from '@vegaprotocol/environment';
import { ALLOWED_TRADINGVIEW_HOSTNAMES } from '@vegaprotocol/trading-view'; import { ALLOWED_TRADINGVIEW_HOSTNAMES } from '@vegaprotocol/trading-view';
import { IconNames, type IconName } from '@blueprintjs/icons'; import { IconNames, type IconName } from '@blueprintjs/icons';
import { useChartSettings } from './use-chart-settings'; import { useChartSettings } from './use-chart-settings';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { SUPPORTED_INTERVALS } from './constants';
const INTERVALS = [
Interval.INTERVAL_I1M,
Interval.INTERVAL_I5M,
Interval.INTERVAL_I15M,
Interval.INTERVAL_I1H,
Interval.INTERVAL_I6H,
Interval.INTERVAL_I1D,
];
const chartTypeIcon = new Map<ChartType, IconName>([ const chartTypeIcon = new Map<ChartType, IconName>([
[ChartType.AREA, IconNames.TIMELINE_AREA_CHART], [ChartType.AREA, IconNames.TIMELINE_AREA_CHART],
@@ -93,7 +85,7 @@ export const ChartMenu = () => {
setInterval(value as Interval); setInterval(value as Interval);
}} }}
> >
{INTERVALS.map((timeInterval) => ( {SUPPORTED_INTERVALS.map((timeInterval) => (
<TradingDropdownRadioItem <TradingDropdownRadioItem
key={timeInterval} key={timeInterval}
inset 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,38 @@
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-rows-3 grid-cols-1 md:grid-rows-1 md:grid-cols-3 gap-6 mb-12">
{children}
</div>
);
export const CompetitionsAction = ({
variant,
title,
description,
actionElement,
children,
}: {
variant: ComponentProps<typeof DudeBadge>['variant'];
title: string;
description?: string;
actionElement: ReactNode;
children?: ReactNode;
}) => {
return (
<Box className="flex flex-col items-center gap-6 text-center">
<DudeBadge variant={variant} />
<h2 className="text-2xl">{title}</h2>
{description && <p className="text-muted">{description}</p>}
{actionElement}
</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,39 @@
import { type TransferNode } from '@vegaprotocol/types';
import { ActiveRewardCard } from '../rewards-container/active-rewards';
import { useT } from '../../lib/use-t';
import { Splash } from '@vegaprotocol/ui-toolkit';
export const GamesContainer = ({
data,
currentEpoch,
}: {
data: TransferNode[];
currentEpoch: number;
}) => {
const t = useT();
if (!data || data.length === 0) {
return <Splash>{t('There are currently no games available.')}</Splash>;
}
return (
<div className="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{data.map((game, i) => {
// TODO: Remove `kind` prop from ActiveRewardCard
const { transfer } = game;
if (
transfer.kind.__typename !== 'RecurringTransfer' ||
!transfer.kind.dispatchStrategy?.dispatchMetric
) {
return null;
}
return (
<ActiveRewardCard
key={i}
transferNode={game}
currentEpoch={currentEpoch}
kind={transfer.kind}
/>
);
})}
</div>
);
};
@@ -0,0 +1,40 @@
import classNames from 'classnames';
import { DudeWithFlag } from './dude-with-flag';
/**
* Pre-defined badge gradients
*/
export const BADGE_GRADIENT_VARIANT_A =
'bg-gradient-to-r from-vega-blue-500 via-vega-purple-500 to-vega-pink-500';
export const BADGE_GRADIENT_VARIANT_B =
'bg-gradient-to-r from-vega-purple-500 via-vega-green-500 to-vega-blue-500';
export const BADGE_GRADIENT_VARIANT_C =
'bg-gradient-to-r from-vega-blue-500 via-vega-purple-500 to-vega-green-500';
/** Badge */
export const DudeBadge = ({
variant,
className,
}: {
variant: 'A' | 'B' | 'C' | undefined;
className?: classNames.Argument;
}) => {
return (
<div
className={classNames(
'w-24 h-24 rounded-full bg-black relative',
'rotate-12',
{
[BADGE_GRADIENT_VARIANT_A]: variant === 'A',
[BADGE_GRADIENT_VARIANT_B]: variant === 'B',
[BADGE_GRADIENT_VARIANT_C]: variant === 'C',
},
className
)}
>
<DudeWithFlag className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 -rotate-12" />
</div>
);
};
@@ -0,0 +1,50 @@
import { theme } from '@vegaprotocol/tailwindcss-config';
type DudeWithFlagProps = {
flagColor?: string;
withStar?: boolean;
className?: string;
};
const DEFAULT_FLAG_COLOR = theme.colors.vega.green[500];
export const DudeWithFlag = ({
flagColor = DEFAULT_FLAG_COLOR,
withStar = true,
className,
}: DudeWithFlagProps) => {
return (
<svg
width="49"
height="43"
viewBox="0 0 49 43"
fill="none"
className={className}
>
{withStar && (
<>
<path d="M3.99992 0H2V1.99993H3.99992V0Z" fill="white" />
<path
d="M2 1.99993L0 1.99981V3.99974H1.99992L2 1.99993Z"
fill="white"
/>
<path
d="M3.99995 3.99992L1.99992 3.99974L2 5.99988H3.99995V3.99992Z"
fill="white"
/>
<path
d="M5.99997 1.99981L3.99992 1.99993L3.99995 3.99992L5.99997 3.99974V1.99981Z"
fill="white"
/>
</>
)}
<path
d="M32 4H11V33H15V43H20V33H23V43H28V33H32V4ZM20 17H15V12H20V17ZM28 17H23V12H28V17Z"
fill="white"
/>
<path d="M41 25L32 25L32 20L41 20L41 25Z" fill="white" />
<path d="M36 29V4H35V29" fill="white" />
<path d="M36 13H49L44.55 8.5L49 4H36V13Z" fill={flagColor} />
</svg>
);
};
@@ -0,0 +1,110 @@
import { useThemeSwitcher } from '@vegaprotocol/react-helpers';
import classNames from 'classnames';
export const Rank = ({
variant,
className,
}: {
variant?: 'gold' | 'silver' | 'bronze';
className?: classNames.Argument;
}) => {
const { theme } = useThemeSwitcher();
return (
<div
title={classNames({
'1': variant === 'gold',
'2': variant === 'silver',
'3': variant === 'bronze',
})}
className={classNames(
{
'text-yellow-300': variant === 'gold',
'text-vega-clight-500': variant === 'silver',
'text-vega-orange-500': variant === 'bronze',
'text-black dark:text-white': variant === undefined,
},
className
)}
>
<svg width="18" height="30" viewBox="0 0 18 30" fill="none">
<defs>
<linearGradient x1="0" y1="0" x2="100%" y2="100%" id="medal">
<stop offset="33%" 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"
/>
);
};
@@ -202,6 +202,13 @@ const NavbarMenu = ({ onClick }: { onClick: () => void }) => {
{t('Portfolio')} {t('Portfolio')}
</NavbarLink> </NavbarLink>
</NavbarItem> </NavbarItem>
{featureFlags.TEAM_COMPETITION && (
<NavbarItem>
<NavbarLink to={Links.COMPETITIONS()} onClick={onClick}>
{t('Competitions')}
</NavbarLink>
</NavbarItem>
)}
{featureFlags.REFERRALS && ( {featureFlags.REFERRALS && (
<NavbarItem> <NavbarItem>
<NavbarLink end={false} to={Links.REFERRALS()} onClick={onClick}> <NavbarLink end={false} to={Links.REFERRALS()} onClick={onClick}>
@@ -0,0 +1 @@
export { RainbowButton } from './rainbow-button';
@@ -0,0 +1,37 @@
import classNames from 'classnames';
import { type ButtonHTMLAttributes } from 'react';
type RainbowButtonProps = {
variant?: 'full' | 'border';
};
export const RainbowButton = ({
variant = 'full',
children,
className,
...props
}: RainbowButtonProps & ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={classNames(
'bg-rainbow rounded-lg overflow-hidden disabled:opacity-40',
'hover:bg-rainbow-180 hover:animate-spin-rainbow',
{
'px-5 py-3 text-white': variant === 'full',
'p-[0.125rem]': variant === 'border',
}
)}
{...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>
);
+22 -6
View File
@@ -11,14 +11,21 @@ type TableColumnDefinition = {
name: string; name: string;
tooltip?: string; tooltip?: string;
className?: string; className?: string;
headerClassName?: string;
testId?: string; testId?: string;
}; };
type DataEntry = {
[key: TableColumnDefinition['name']]: ReactNode;
className?: string;
};
type TableProps = { type TableProps = {
columns: TableColumnDefinition[]; columns: TableColumnDefinition[];
data: Record<TableColumnDefinition['name'] | 'className', React.ReactNode>[]; data: DataEntry[];
noHeader?: boolean; noHeader?: boolean;
noCollapse?: boolean; noCollapse?: boolean;
onRowClick?: (index: number) => void;
}; };
const INNER_BORDER_STYLE = `border-b ${BORDER_COLOR}`; const INNER_BORDER_STYLE = `border-b ${BORDER_COLOR}`;
@@ -34,6 +41,7 @@ export const Table = forwardRef<
noHeader = false, noHeader = false,
noCollapse = false, noCollapse = false,
className, className,
onRowClick,
...props ...props
}, },
ref ref
@@ -41,13 +49,14 @@ export const Table = forwardRef<
const header = ( const header = (
<thead className={classNames({ 'max-md:hidden': !noCollapse })}> <thead className={classNames({ 'max-md:hidden': !noCollapse })}>
<tr> <tr>
{columns.map(({ displayName, name, tooltip }) => ( {columns.map(({ displayName, name, tooltip, headerClassName }) => (
<th <th
key={name} key={name}
col-id={name} col-id={name}
className={classNames( className={classNames(
'px-5 py-3 text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-normal', 'px-5 py-3 text-xs text-vega-clight-100 dark:text-vega-cdark-100 font-normal',
INNER_BORDER_STYLE INNER_BORDER_STYLE,
headerClassName
)} )}
> >
<span className="flex flex-row items-center gap-2"> <span className="flex flex-row items-center gap-2">
@@ -79,12 +88,17 @@ export const Table = forwardRef<
> >
{!noHeader && header} {!noHeader && header}
<tbody> <tbody>
{data.map((d, i) => ( {data.map((dataEntry, i) => (
<tr <tr
key={i} key={i}
className={classNames(d['className'] as string, { className={classNames(dataEntry['className'] as string, {
'max-md:flex flex-col w-full': !noCollapse, 'max-md:flex flex-col w-full': !noCollapse,
})} })}
onClick={() => {
if (onRowClick) {
onRowClick(i);
}
}}
> >
{columns.map(({ name, displayName, className, testId }, j) => ( {columns.map(({ name, displayName, className, testId }, j) => (
<td <td
@@ -114,7 +128,9 @@ export const Table = forwardRef<
{displayName} {displayName}
</span> </span>
)} )}
<span data-testid={`${testId || name}-${i}`}>{d[name]}</span> <span data-testid={`${testId || name}-${i}`}>
{dataEntry[name]}
</span>
</td> </td>
))} ))}
</tr> </tr>
@@ -0,0 +1,58 @@
import pytest
from playwright.sync_api import expect, Page
from vega_sim.null_service import VegaServiceNull
from actions.utils import next_epoch
from wallet_config import PARTY_A, PARTY_B, PARTY_C, PARTY_D
@pytest.mark.usefixtures("auth", "risk_accepted")
def test_teams(vega: VegaServiceNull, page: Page):
# submit_transaction will fail with spam statistics error if block height is 0
# so go forward at least one block
vega.wait_fn(1)
for key in [PARTY_A, PARTY_B, PARTY_C, PARTY_D]:
vega.wallet.create_key(key.name)
team_name = create_team(vega)
next_epoch(vega)
# would be better to derive the team id from the signature if possible but
# vega.submit_transaction does not return anything
teams = vega.list_teams()
# list_teams actually returns a dictionary {"team_id": Team}
team_id = list(teams.keys())[0]
vega.apply_referral_code(PARTY_B.name, team_id)
# go to next epoch so we can check joinedAt and joinedAtEpoch appropriately
next_epoch(vega)
vega.apply_referral_code(PARTY_C.name, team_id)
next_epoch(vega)
vega.apply_referral_code(PARTY_D.name, team_id)
vega.wait_fn(1)
vega.forward("10s")
vega.wait_for_total_catchup()
page.goto(f"/#/competitions/team/{team_id}")
expect(page.get_by_role('heading', level=1)).to_have_text(team_name)
expect(page.get_by_text('Members (3)')).to_be_visible()
def create_team(vega: VegaServiceNull):
team_name = "Foobar"
vega.create_referral_set(
key_name=PARTY_A.name,
name=team_name,
team_url="https://vega.xyz",
avatar_url="http://placekitten.com/200/200",
closed=False
)
return team_name
@@ -0,0 +1,32 @@
import {
useSimpleTransaction,
type CreateReferralSet,
type Options,
} from '@vegaprotocol/wallet';
import { useStakeAvailable } from './use-stake-available';
/**
* Manages state for creating a referral set or team
*/
export const useCreateReferralSet = (opts?: Options) => {
const { stakeAvailable, requiredStake, isEligible } = useStakeAvailable();
const { status, result, error, send } = useSimpleTransaction({
onSuccess: opts?.onSuccess,
onError: opts?.onError,
});
const onSubmit = (tx: CreateReferralSet) => {
send(tx);
};
return {
err: error ? error : null,
code: result ? result.id : null,
status,
stakeAvailable,
requiredStake,
onSubmit,
isEligible,
};
};
+18
View File
@@ -0,0 +1,18 @@
import { useEffect, useMemo } from 'react';
import { titlefy } from '@vegaprotocol/utils';
import { usePageTitleStore } from '../../stores';
export const usePageTitle = (title: string | string[]) => {
const { updateTitle } = usePageTitleStore((store) => ({
updateTitle: store.updateTitle,
}));
const memotitle = useMemo(
() => titlefy(Array.isArray(title) ? title : [title]),
[title]
);
useEffect(() => {
updateTitle(memotitle);
}, [updateTitle, memotitle]);
};
+11 -2
View File
@@ -16,7 +16,11 @@ export const Routes = {
REFERRALS: '/referrals', REFERRALS: '/referrals',
REFERRALS_APPLY_CODE: '/referrals/apply-code', REFERRALS_APPLY_CODE: '/referrals/apply-code',
REFERRALS_CREATE_CODE: '/referrals/create-code', REFERRALS_CREATE_CODE: '/referrals/create-code',
TEAMS: '/teams', COMPETITIONS: '/competitions',
COMPETITIONS_TEAMS: '/competitions/teams',
COMPETITIONS_CREATE_TEAM: '/competitions/teams/create',
COMPETITIONS_CREATE_TEAM_SOLO: '/competitions/teams/create?solo=true',
COMPETITIONS_TEAM: '/competitions/teams/:teamId',
FEES: '/fees', FEES: '/fees',
REWARDS: '/rewards', REWARDS: '/rewards',
} as const; } as const;
@@ -41,7 +45,12 @@ export const Links: ConsoleLinks = {
REFERRALS: () => Routes.REFERRALS, REFERRALS: () => Routes.REFERRALS,
REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE, REFERRALS_APPLY_CODE: () => Routes.REFERRALS_APPLY_CODE,
REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE, REFERRALS_CREATE_CODE: () => Routes.REFERRALS_CREATE_CODE,
TEAMS: () => Routes.TEAMS, COMPETITIONS: () => Routes.COMPETITIONS,
COMPETITIONS_TEAMS: () => Routes.COMPETITIONS_TEAMS,
COMPETITIONS_CREATE_TEAM: () => Routes.COMPETITIONS_CREATE_TEAM,
COMPETITIONS_CREATE_TEAM_SOLO: () => Routes.COMPETITIONS_CREATE_TEAM_SOLO,
COMPETITIONS_TEAM: (teamId: string) =>
Routes.COMPETITIONS_TEAM.replace(':teamId', teamId),
FEES: () => Routes.FEES, FEES: () => Routes.FEES,
REWARDS: () => Routes.REWARDS, REWARDS: () => Routes.REWARDS,
}; };
+31 -4
View File
@@ -14,7 +14,6 @@ import { Withdraw } from '../client-pages/withdraw';
import { Transfer } from '../client-pages/transfer'; import { Transfer } from '../client-pages/transfer';
import { Fees } from '../client-pages/fees'; import { Fees } from '../client-pages/fees';
import { Rewards } from '../client-pages/rewards'; import { Rewards } from '../client-pages/rewards';
import { Teams } from '../client-pages/teams';
import { Routes as AppRoutes } from '../lib/links'; import { Routes as AppRoutes } from '../lib/links';
import { LayoutWithSky } from '../client-pages/referrals/layout'; import { LayoutWithSky } from '../client-pages/referrals/layout';
import { Referrals } from '../client-pages/referrals/referrals'; import { Referrals } from '../client-pages/referrals/referrals';
@@ -30,6 +29,10 @@ import { PortfolioSidebar } from '../client-pages/portfolio/portfolio-sidebar';
import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar'; import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar';
import { MarketsSidebar } from '../client-pages/markets/markets-sidebar'; import { MarketsSidebar } from '../client-pages/markets/markets-sidebar';
import { useT } from '../lib/use-t'; import { useT } from '../lib/use-t';
import { CompetitionsHome } from '../client-pages/competitions/competitions-home';
import { CompetitionsTeams } from '../client-pages/competitions/competitions-teams';
import { CompetitionsTeam } from '../client-pages/competitions/competitions-team';
import { CompetitionsCreateTeam } from '../client-pages/competitions/competitions-create-team';
// These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM // These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM
// Using dynamic imports is a workaround for this until pennant is published as ESM // Using dynamic imports is a workaround for this until pennant is published as ESM
@@ -47,7 +50,7 @@ const NotFound = () => {
export const useRouterConfig = (): RouteObject[] => { export const useRouterConfig = (): RouteObject[] => {
const featureFlags = useFeatureFlags((state) => state.flags); const featureFlags = useFeatureFlags((state) => state.flags);
return compact([ const routeConfig = compact([
{ {
index: true, index: true,
element: <Home />, element: <Home />,
@@ -95,8 +98,30 @@ export const useRouterConfig = (): RouteObject[] => {
: undefined, : undefined,
featureFlags.TEAM_COMPETITION featureFlags.TEAM_COMPETITION
? { ? {
path: AppRoutes.TEAMS, path: AppRoutes.COMPETITIONS,
element: <Teams />, element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />,
children: [
// pages with planets and stars
{
element: <LayoutWithSky />,
children: [
{ index: true, element: <CompetitionsHome /> },
{
path: AppRoutes.COMPETITIONS_TEAMS,
element: <CompetitionsTeams />,
},
],
},
// pages with blurred background
{
path: AppRoutes.COMPETITIONS_CREATE_TEAM,
element: <CompetitionsCreateTeam />,
},
{
path: AppRoutes.COMPETITIONS_TEAM,
element: <CompetitionsTeam />,
},
],
} }
: undefined, : undefined,
{ {
@@ -189,6 +214,8 @@ export const useRouterConfig = (): RouteObject[] => {
element: <NotFound />, element: <NotFound />,
}, },
]); ]);
return routeConfig;
}; };
export const ClientRouter = () => { export const ClientRouter = () => {
Binary file not shown.

After

Width:  |  Height:  |  Size: 668 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

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