Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22fc6125b4 | ||
|
|
c496d11331 | ||
|
|
1788753494 | ||
|
|
cf5fa0c454 | ||
|
|
f2a434c1c0 | ||
|
|
94b84ab1dc | ||
|
|
022e2010c4 | ||
|
|
67e598873f | ||
|
|
066f34fd5d | ||
|
|
6b5fa440fb | ||
|
|
0894854d3e | ||
|
|
e59b2e049f | ||
|
|
e0f04ab1b4 | ||
|
|
4484bdf0e0 | ||
|
|
316c31ee5d | ||
|
|
398fda6245 | ||
|
|
47aff2a24d | ||
|
|
cadf9509e7 | ||
|
|
85489a6a6e | ||
|
|
e00d933c84 | ||
|
|
799a95e7a1 | ||
|
|
01c03377e2 | ||
|
|
3ef3ac9cd2 | ||
|
|
18e461ecea | ||
|
|
7fa1f1ec5c | ||
|
|
4e13ad52a9 |
@@ -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">
|
||||||
|
|||||||
@@ -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> {daysClosedAgo}</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!majorityMet) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span>{t('voteFailedReason')}</span>
|
|
||||||
<StatusFail>{t('majorityNotMet')}</StatusFail>
|
|
||||||
<span> {daysClosedAgo}</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span>{t('voteFailedReason')}</span>
|
|
||||||
<StatusFail>
|
|
||||||
{proposal?.errorDetails ||
|
|
||||||
proposal?.rejectionReason ||
|
|
||||||
t('unknownReason')}
|
|
||||||
</StatusFail>
|
|
||||||
<span> {daysClosedAgo}</span>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
proposal?.state === ProposalState.STATE_ENACTED ||
|
|
||||||
proposal?.state === ProposalState.STATE_PASSED
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<span>{t('votePassed')}</span>
|
|
||||||
<StatusPass>
|
|
||||||
|
|
||||||
{proposal?.state === ProposalState.STATE_ENACTED
|
|
||||||
? t('Enacted')
|
|
||||||
: t('Passed')}
|
|
||||||
</StatusPass>
|
|
||||||
<span>
|
|
||||||
|
|
||||||
{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
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 +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>
|
||||||
|
);
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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]);
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
|
After Width: | Height: | Size: 668 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 90 KiB |