Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0aa0065b6 | ||
|
|
bb6ecd2c72 | ||
|
|
de62f09fda | ||
|
|
6d70b28f30 | ||
|
|
0d850bd8b9 | ||
|
|
44189591fc | ||
|
|
3ed2ec88d7 | ||
|
|
a21feea699 | ||
|
|
41fd14dd00 | ||
|
|
c5a27dc6a2 | ||
|
|
0a3b1cadba | ||
|
|
b953de953a | ||
|
|
5ddcb613e2 | ||
|
|
76c07992d3 | ||
|
|
46e2965fa2 | ||
|
|
532ad3a4b9 | ||
|
|
3a8b40d7a5 | ||
|
|
be38813a33 | ||
|
|
bf70dc33ec | ||
|
|
d6084e75a0 | ||
|
|
75e7cea32a | ||
|
|
b4e98e285e | ||
|
|
f62e29c67f | ||
|
|
94e7ad489f | ||
|
|
657942d995 | ||
|
|
a529b9b031 | ||
|
|
0f311611ba | ||
|
|
02bd031bee | ||
|
|
bd97651dd3 | ||
|
|
516b3e5b93 | ||
|
|
42a98b6a35 | ||
|
|
2002731c52 | ||
|
|
a49139f127 | ||
|
|
e216b23472 | ||
|
|
e52ae97233 | ||
|
|
1780f6fa7f |
@@ -205,7 +205,7 @@ jobs:
|
|||||||
# run tests
|
# run tests
|
||||||
#----------------------------------------------
|
#----------------------------------------------
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 1 --dist loadfile --durations=45
|
run: CONSOLE_IMAGE_NAME=ci/trading:local poetry run pytest -v --numprocesses 4 --dist loadfile --durations=45
|
||||||
working-directory: apps/trading/e2e
|
working-directory: apps/trading/e2e
|
||||||
#----------------------------------------------
|
#----------------------------------------------
|
||||||
# upload traces
|
# upload traces
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ The front-end monorepo provides a toolkit for building apps that interact with V
|
|||||||
|
|
||||||
This repository is managed using [Nx](https://nx.dev).
|
This repository is managed using [Nx](https://nx.dev).
|
||||||
|
|
||||||
# 🔎 Applications in this repo
|
## 🔎 Applications in this repo
|
||||||
|
|
||||||
### [Block explorer](./apps/explorer)
|
### [Block explorer](./apps/explorer)
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ Hosting for static content being shared across apps, for example fonts.
|
|||||||
|
|
||||||
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
|
The utility dApp for validators wishing to add or remove themselves as a signer of the multisig contract.
|
||||||
|
|
||||||
# 🧱 Libraries in this repo
|
## 🧱 Libraries in this repo
|
||||||
|
|
||||||
### [UI toolkit](./libs/ui-toolkit)
|
### [UI toolkit](./libs/ui-toolkit)
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ A utility library for connecting to the Ethereum network and interacting with Ve
|
|||||||
|
|
||||||
Generic react helpers that can be used across multiple applications, along with other utilities.
|
Generic react helpers that can be used across multiple applications, along with other utilities.
|
||||||
|
|
||||||
# 💻 Develop
|
## 💻 Develop
|
||||||
|
|
||||||
### Set up
|
### Set up
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ In CI linting, formatting and also run. These checks can be seen in the [CI work
|
|||||||
|
|
||||||
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
|
Visit the [Nx Documentation](https://nx.dev/getting-started/intro) to learn more.
|
||||||
|
|
||||||
# 🐋 Hosting a console
|
## 🐋 Hosting a console
|
||||||
|
|
||||||
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
|
To host a console there are two possible build scenarios for running the frontends: nx performed **outside** or **inside** docker build. For specific build instructions follow [build instructions](#build-instructions).
|
||||||
|
|
||||||
@@ -226,6 +226,6 @@ Note: The script is only needed if capsule was built for first time or fresh. To
|
|||||||
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
|
vega wallet service run -n DV --load-tokens --tokens-passphrase-file passphrase --no-version-check --automatic-consent --home ~/.vegacapsule/testnet/wallet
|
||||||
```
|
```
|
||||||
|
|
||||||
# 📑 License
|
## 📑 License
|
||||||
|
|
||||||
[MIT](./LICENSE)
|
[MIT](./LICENSE)
|
||||||
|
|||||||
@@ -24,10 +24,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
|
|||||||
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
|
cy.get_element_by_col_id('title').should('have.text', proposalTitle);
|
||||||
cy.get_element_by_col_id('type').should('have.text', 'NewMarket');
|
cy.get_element_by_col_id('type').should('have.text', 'NewMarket');
|
||||||
cy.get_element_by_col_id('state').should('have.text', 'Enacted');
|
cy.get_element_by_col_id('state').should('have.text', 'Enacted');
|
||||||
cy.getByTestId('vote-progress').should('be.visible');
|
|
||||||
cy.getByTestId('vote-progress-bar-for')
|
|
||||||
.invoke('attr', 'style')
|
|
||||||
.should('eq', 'width: 100%;');
|
|
||||||
cy.get('[col-id="cDate"]')
|
cy.get('[col-id="cDate"]')
|
||||||
.invoke('text')
|
.invoke('text')
|
||||||
.should('match', dateTimeRegex);
|
.should('match', dateTimeRegex);
|
||||||
@@ -73,10 +69,6 @@ context('Proposal page', { tags: '@smoke' }, function () {
|
|||||||
'have.text',
|
'have.text',
|
||||||
'Waiting for Node Vote'
|
'Waiting for Node Vote'
|
||||||
);
|
);
|
||||||
cy.getByTestId('vote-progress').should('be.visible');
|
|
||||||
cy.getByTestId('vote-progress-bar-against')
|
|
||||||
.invoke('attr', 'style')
|
|
||||||
.should('eq', 'width: 100%;');
|
|
||||||
cy.get('[col-id="cDate"]')
|
cy.get('[col-id="cDate"]')
|
||||||
.invoke('text')
|
.invoke('text')
|
||||||
.should('match', dateTimeRegex);
|
.should('match', dateTimeRegex);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export type HashProps = {
|
export type HashProps = React.HTMLProps<HTMLSpanElement> & {
|
||||||
text: string;
|
text: string;
|
||||||
truncate?: boolean;
|
truncate?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,4 +2,5 @@ export { default as BlockLink } from './block-link/block-link';
|
|||||||
export { default as PartyLink } from './party-link/party-link';
|
export { default as PartyLink } from './party-link/party-link';
|
||||||
export { default as NodeLink } from './node-link/node-link';
|
export { default as NodeLink } from './node-link/node-link';
|
||||||
export { default as MarketLink } from './market-link/market-link';
|
export { default as MarketLink } from './market-link/market-link';
|
||||||
|
export { default as NetworkParameterLink } from './network-parameter-link/network-parameter-link';
|
||||||
export * from './asset-link/asset-link';
|
export * from './asset-link/asset-link';
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Routes } from '../../../routes/route-names';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import type { ComponentProps } from 'react';
|
||||||
|
import Hash from '../hash';
|
||||||
|
|
||||||
|
export type NetworkParameterLinkProps = Partial<ComponentProps<typeof Link>> & {
|
||||||
|
parameter: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Links a given network parameter to the relevant page and anchor on the page
|
||||||
|
*/
|
||||||
|
const NetworkParameterLink = ({
|
||||||
|
parameter,
|
||||||
|
...props
|
||||||
|
}: NetworkParameterLinkProps) => {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
className="underline"
|
||||||
|
{...props}
|
||||||
|
to={`/${Routes.NETWORK_PARAMETERS}#${parameter}`}
|
||||||
|
>
|
||||||
|
<Hash text={parameter} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NetworkParameterLink;
|
||||||
@@ -26,7 +26,7 @@ const ProposalLink = ({ id, text }: ProposalLinkProps) => {
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
const base = ENV.dataSources.governanceUrl;
|
const base = ENV.dataSources.governanceUrl;
|
||||||
const label = proposal?.rationale.title || id;
|
const label = proposal?.rationale?.title || id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExternalLink href={`${base}/proposals/${id}`}>
|
<ExternalLink href={`${base}/proposals/${id}`}>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals';
|
import { type ProposalListFieldsFragment } from '@vegaprotocol/proposals';
|
||||||
import { VoteProgress } from '@vegaprotocol/proposals';
|
|
||||||
import { type AgGridReact } from 'ag-grid-react';
|
import { type AgGridReact } from 'ag-grid-react';
|
||||||
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
import { ExternalLink } from '@vegaprotocol/ui-toolkit';
|
||||||
import { AgGrid } from '@vegaprotocol/datagrid';
|
import { AgGrid } from '@vegaprotocol/datagrid';
|
||||||
@@ -12,12 +11,7 @@ import { type ColDef } from 'ag-grid-community';
|
|||||||
import type { RowClickedEvent } from 'ag-grid-community';
|
import type { RowClickedEvent } from 'ag-grid-community';
|
||||||
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
import { getDateTimeFormat } from '@vegaprotocol/utils';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
import {
|
|
||||||
NetworkParams,
|
|
||||||
useNetworkParams,
|
|
||||||
} from '@vegaprotocol/network-parameters';
|
|
||||||
import { ProposalStateMapping } from '@vegaprotocol/types';
|
import { ProposalStateMapping } from '@vegaprotocol/types';
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
import { DApp, TOKEN_PROPOSAL, useLinks } from '@vegaprotocol/environment';
|
||||||
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
import { BREAKPOINT_MD } from '../../config/breakpoints';
|
||||||
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
|
import { JsonViewerDialog } from '../dialogs/json-viewer-dialog';
|
||||||
@@ -31,15 +25,7 @@ type ProposalsTableProps = {
|
|||||||
data: ProposalListFieldsFragment[] | null;
|
data: ProposalListFieldsFragment[] | null;
|
||||||
};
|
};
|
||||||
export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
||||||
const { params } = useNetworkParams([
|
|
||||||
NetworkParams.governance_proposal_market_requiredMajority,
|
|
||||||
]);
|
|
||||||
const tokenLink = useLinks(DApp.Governance);
|
const tokenLink = useLinks(DApp.Governance);
|
||||||
const requiredMajorityPercentage = useMemo(() => {
|
|
||||||
const requiredMajority =
|
|
||||||
params?.governance_proposal_market_requiredMajority ?? 1;
|
|
||||||
return new BigNumber(requiredMajority).times(100);
|
|
||||||
}, [params?.governance_proposal_market_requiredMajority]);
|
|
||||||
|
|
||||||
const gridRef = useRef<AgGridReact>(null);
|
const gridRef = useRef<AgGridReact>(null);
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
@@ -90,33 +76,6 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
|||||||
return value ? ProposalStateMapping[value] : '-';
|
return value ? ProposalStateMapping[value] : '-';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
colId: 'voting',
|
|
||||||
maxWidth: 100,
|
|
||||||
hide: window.innerWidth <= BREAKPOINT_MD,
|
|
||||||
headerName: t('Voting'),
|
|
||||||
cellRenderer: ({
|
|
||||||
data,
|
|
||||||
}: VegaICellRendererParams<ProposalListFieldsFragment>) => {
|
|
||||||
if (data) {
|
|
||||||
const yesTokens = new BigNumber(data.votes.yes.totalTokens);
|
|
||||||
const noTokens = new BigNumber(data.votes.no.totalTokens);
|
|
||||||
const totalTokensVoted = yesTokens.plus(noTokens);
|
|
||||||
const yesPercentage = totalTokensVoted.isZero()
|
|
||||||
? new BigNumber(0)
|
|
||||||
: yesTokens.multipliedBy(100).dividedBy(totalTokensVoted);
|
|
||||||
return (
|
|
||||||
<div className="flex h-full items-center justify-center pt-2 uppercase">
|
|
||||||
<VoteProgress
|
|
||||||
threshold={requiredMajorityPercentage}
|
|
||||||
progress={yesPercentage}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return '-';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
colId: 'cDate',
|
colId: 'cDate',
|
||||||
maxWidth: 150,
|
maxWidth: 150,
|
||||||
@@ -184,7 +143,7 @@ export const ProposalsTable = ({ data }: ProposalsTableProps) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[requiredMajorityPercentage, tokenLink]
|
[tokenLink]
|
||||||
);
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -5,5 +5,10 @@ query ExplorerProposalStatus($id: ID!) {
|
|||||||
state
|
state
|
||||||
rejectionReason
|
rejectionReason
|
||||||
}
|
}
|
||||||
|
... on BatchProposal {
|
||||||
|
id
|
||||||
|
state
|
||||||
|
rejectionReason
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -8,7 +8,7 @@ export type ExplorerProposalStatusQueryVariables = Types.Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal' } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
|
export type ExplorerProposalStatusQuery = { __typename?: 'Query', proposal?: { __typename?: 'BatchProposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | { __typename?: 'Proposal', id?: string | null, state: Types.ProposalState, rejectionReason?: Types.ProposalRejectionReason | null } | null };
|
||||||
|
|
||||||
|
|
||||||
export const ExplorerProposalStatusDocument = gql`
|
export const ExplorerProposalStatusDocument = gql`
|
||||||
@@ -19,6 +19,11 @@ export const ExplorerProposalStatusDocument = gql`
|
|||||||
state
|
state
|
||||||
rejectionReason
|
rejectionReason
|
||||||
}
|
}
|
||||||
|
... on BatchProposal {
|
||||||
|
id
|
||||||
|
state
|
||||||
|
rejectionReason
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { BatchItem } from './batch-item';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
|
import type { components } from '../../../../../types/explorer';
|
||||||
|
type Item = components['schemas']['vegaBatchProposalTermsChange'];
|
||||||
|
|
||||||
|
describe('BatchItem', () => {
|
||||||
|
it('Renders "Unknown proposal type" by default', () => {
|
||||||
|
const item = {};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Unknown proposal type')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Unknown proposal type" for unknown items', () => {
|
||||||
|
const item = {
|
||||||
|
newLochNessMonster: {
|
||||||
|
location: 'Loch Ness',
|
||||||
|
},
|
||||||
|
} as unknown as Item;
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Unknown proposal type')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "New spot market"', () => {
|
||||||
|
const item = {
|
||||||
|
newSpotMarket: {},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('New spot market')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Cancel transfer"', () => {
|
||||||
|
const item = {
|
||||||
|
cancelTransfer: {
|
||||||
|
changes: {
|
||||||
|
transferId: 'transfer123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Cancel transfer')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('transf')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Cancel transfer" without an id', () => {
|
||||||
|
const item = {
|
||||||
|
cancelTransfer: {
|
||||||
|
changes: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Cancel transfer')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "New freeform"', () => {
|
||||||
|
const item = {
|
||||||
|
newFreeform: {},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('New freeform proposal')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "New market"', () => {
|
||||||
|
const item = {
|
||||||
|
newMarket: {},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('New market')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "New transfer"', () => {
|
||||||
|
const item = {
|
||||||
|
newTransfer: {},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('New transfer')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update asset" with assetId', () => {
|
||||||
|
const item = {
|
||||||
|
updateAsset: {
|
||||||
|
assetId: 'asset123',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update asset')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('asset123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update asset" even if assetId is not set', () => {
|
||||||
|
const item = {
|
||||||
|
updateAsset: {
|
||||||
|
assetId: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Update asset')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update market state" with marketId', () => {
|
||||||
|
const item = {
|
||||||
|
updateMarketState: {
|
||||||
|
changes: {
|
||||||
|
marketId: 'market123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update market state')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('market123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update market state" even if marketId is not set', () => {
|
||||||
|
const item = {
|
||||||
|
updateMarketState: {
|
||||||
|
changes: {
|
||||||
|
marketId: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update market state')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update network parameter" with parameter', () => {
|
||||||
|
const item = {
|
||||||
|
updateNetworkParameter: {
|
||||||
|
changes: {
|
||||||
|
key: 'parameter123',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MockedProvider>
|
||||||
|
<MemoryRouter>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MemoryRouter>
|
||||||
|
</MockedProvider>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update network parameter')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('parameter123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update network parameter" even if parameter is not set', () => {
|
||||||
|
const item = {
|
||||||
|
updateNetworkParameter: {
|
||||||
|
changes: {
|
||||||
|
key: undefined,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Update network parameter')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update referral program"', () => {
|
||||||
|
const item = {
|
||||||
|
updateReferralProgram: {},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(screen.getByText('Update referral program')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update spot market" with marketId', () => {
|
||||||
|
const item = {
|
||||||
|
updateSpotMarket: {
|
||||||
|
marketId: 'market123',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update spot market')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('market123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update spot market" even if marketId is not set', () => {
|
||||||
|
const item = {
|
||||||
|
updateSpotMarket: {
|
||||||
|
marketId: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update spot market')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Renders "Update market" with marketId', () => {
|
||||||
|
const item = {
|
||||||
|
updateMarket: {
|
||||||
|
marketId: 'market123',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update market')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('market123')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update market" even if marketId is not set', () => {
|
||||||
|
const item = {
|
||||||
|
updateMarket: {
|
||||||
|
marketId: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<MockedProvider>
|
||||||
|
<BatchItem item={item} />
|
||||||
|
</MockedProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Update market')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Renders "Update volume discount program"', () => {
|
||||||
|
const item = {
|
||||||
|
updateVolumeDiscountProgram: {},
|
||||||
|
};
|
||||||
|
render(<BatchItem item={item} />);
|
||||||
|
expect(
|
||||||
|
screen.getByText('Update volume discount program')
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { AssetLink, MarketLink, NetworkParameterLink } from '../../../links';
|
||||||
|
import type { components } from '../../../../../types/explorer';
|
||||||
|
import Hash from '../../../links/hash';
|
||||||
|
|
||||||
|
type Item = components['schemas']['vegaBatchProposalTermsChange'];
|
||||||
|
|
||||||
|
export interface BatchItemProps {
|
||||||
|
item: Item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produces a one line summary for an item in a batch proposal. Could
|
||||||
|
* easily be adapted to summarise individual proposals, but there is no
|
||||||
|
* place for that yet.
|
||||||
|
*
|
||||||
|
* Details (like IDs) should be shown and linked if available, but handled
|
||||||
|
* if not available. This is adequate as the ProposalSummary component contains
|
||||||
|
* a JSON viewer for the full proposal.
|
||||||
|
*/
|
||||||
|
export const BatchItem = ({ item }: BatchItemProps) => {
|
||||||
|
if (item.cancelTransfer) {
|
||||||
|
const transferId = item?.cancelTransfer?.changes?.transferId || false;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Cancel transfer')}
|
||||||
|
{transferId && (
|
||||||
|
<Hash className="ml-1" truncate={true} text={transferId} />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.newFreeform) {
|
||||||
|
return <span>{t('New freeform proposal')}</span>;
|
||||||
|
} else if (item.newMarket) {
|
||||||
|
return <span>{t('New market')}</span>;
|
||||||
|
} else if (item.newSpotMarket) {
|
||||||
|
return <span>{t('New spot market')}</span>;
|
||||||
|
} else if (item.newTransfer) {
|
||||||
|
return <span>{t('New transfer')}</span>;
|
||||||
|
} else if (item.updateAsset) {
|
||||||
|
const assetId = item?.updateAsset?.assetId || false;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Update asset')}
|
||||||
|
{assetId && <AssetLink className="ml-1" assetId={assetId} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.updateMarket) {
|
||||||
|
const marketId = item?.updateMarket?.marketId || false;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Update market')}{' '}
|
||||||
|
{marketId && <MarketLink className="ml-1" id={marketId} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.updateMarketState) {
|
||||||
|
const marketId = item?.updateMarketState?.changes?.marketId || false;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Update market state')}
|
||||||
|
{marketId && <MarketLink className="ml-1" id={marketId} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.updateNetworkParameter) {
|
||||||
|
const param = item?.updateNetworkParameter?.changes?.key || false;
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Update network parameter')}
|
||||||
|
{param && <NetworkParameterLink className="ml-1" parameter={param} />}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.updateReferralProgram) {
|
||||||
|
return <span>{t('Update referral program')}</span>;
|
||||||
|
} else if (item.updateSpotMarket) {
|
||||||
|
const marketId = item?.updateSpotMarket?.marketId || '';
|
||||||
|
return (
|
||||||
|
<span>
|
||||||
|
{t('Update spot market')}
|
||||||
|
<MarketLink className="ml-1" id={marketId} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
} else if (item.updateVolumeDiscountProgram) {
|
||||||
|
return <span>{t('Update volume discount program')}</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span>{t('Unknown proposal type')}</span>;
|
||||||
|
};
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import type { ProposalTerms } from '../tx-proposal';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { components } from '../../../../../types/explorer';
|
|
||||||
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
|
import { JsonViewerDialog } from '../../../dialogs/json-viewer-dialog';
|
||||||
import ProposalLink from '../../../links/proposal-link/proposal-link';
|
import ProposalLink from '../../../links/proposal-link/proposal-link';
|
||||||
import truncate from 'lodash/truncate';
|
import truncate from 'lodash/truncate';
|
||||||
@@ -9,7 +7,12 @@ import ReactMarkdown from 'react-markdown';
|
|||||||
import { ProposalDate } from './proposal-date';
|
import { ProposalDate } from './proposal-date';
|
||||||
import { t } from '@vegaprotocol/i18n';
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
|
||||||
|
import type { ProposalTerms } from '../tx-proposal';
|
||||||
|
import type { components } from '../../../../../types/explorer';
|
||||||
|
import { BatchItem } from './batch-item';
|
||||||
|
|
||||||
type Rationale = components['schemas']['vegaProposalRationale'];
|
type Rationale = components['schemas']['vegaProposalRationale'];
|
||||||
|
type Batch = components['schemas']['v1BatchProposalSubmissionTerms']['changes'];
|
||||||
|
|
||||||
type ProposalTermsDialog = {
|
type ProposalTermsDialog = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -21,6 +24,7 @@ interface ProposalSummaryProps {
|
|||||||
id: string;
|
id: string;
|
||||||
rationale?: Rationale;
|
rationale?: Rationale;
|
||||||
terms?: ProposalTerms;
|
terms?: ProposalTerms;
|
||||||
|
batch?: Batch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +35,7 @@ export const ProposalSummary = ({
|
|||||||
id,
|
id,
|
||||||
rationale,
|
rationale,
|
||||||
terms,
|
terms,
|
||||||
|
batch,
|
||||||
}: ProposalSummaryProps) => {
|
}: ProposalSummaryProps) => {
|
||||||
const [dialog, setDialog] = useState<ProposalTermsDialog>({
|
const [dialog, setDialog] = useState<ProposalTermsDialog>({
|
||||||
open: false,
|
open: false,
|
||||||
@@ -72,6 +77,18 @@ export const ProposalSummary = ({
|
|||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{batch && (
|
||||||
|
<section className="pt-2 text-sm leading-tight my-3">
|
||||||
|
<h2 className="text-lg pb-1">{t('Changes')}</h2>
|
||||||
|
<ol>
|
||||||
|
{batch.map((change, index) => (
|
||||||
|
<li className="ml-4 list-decimal" key={`batch-${index}`}>
|
||||||
|
<BatchItem item={change} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
<div className="pt-5">
|
<div className="pt-5">
|
||||||
<button className="underline max-md:hidden mr-5" onClick={openDialog}>
|
<button className="underline max-md:hidden mr-5" onClick={openDialog}>
|
||||||
{t('View terms')}
|
{t('View terms')}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import { TxDetailsShared } from '../shared/tx-details-shared';
|
||||||
|
import { TableWithTbody } from '../../../table';
|
||||||
|
import type { components } from '../../../../../types/explorer';
|
||||||
|
|
||||||
|
import type { BlockExplorerTransactionResult } from '../../../../routes/types/block-explorer-response';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
import { TableCell, TableRow } from '../../../table';
|
||||||
|
|
||||||
|
type Update = components['schemas']['v1UpdatePartyProfile'];
|
||||||
|
|
||||||
|
interface TxDetailsUpdatePartyProfileProps {
|
||||||
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
|
pubKey: string | undefined;
|
||||||
|
blockData: TendermintBlocksResponse | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Party profiles can be an alias and arbitrary key/values pairs.
|
||||||
|
* This component displays the alias, if any, but not the metadata. When there is
|
||||||
|
* some wider usage, we can decide how to render it. For now, it's available in the
|
||||||
|
* full TX details.
|
||||||
|
*/
|
||||||
|
export const TxDetailsUpdatePartyProfile = ({
|
||||||
|
txData,
|
||||||
|
pubKey,
|
||||||
|
blockData,
|
||||||
|
}: TxDetailsUpdatePartyProfileProps) => {
|
||||||
|
if (!txData?.command.updatePartyProfile) {
|
||||||
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const update: Update = txData.command.updatePartyProfile;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||||
|
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||||
|
{update.alias && (
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('New alias')}</TableCell>
|
||||||
|
<TableCell>{update.alias}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableWithTbody>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
import { sharedHeaderProps, TxDetailsShared } from './shared/tx-details-shared';
|
||||||
|
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||||
|
import type { components } from '../../../../types/explorer';
|
||||||
|
import { txSignatureToDeterministicId } from '../lib/deterministic-ids';
|
||||||
|
import { ProposalSummary } from './proposal/summary';
|
||||||
|
import Hash from '../../links/hash';
|
||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
|
||||||
|
export type Proposal = components['schemas']['v1BatchProposalSubmission'];
|
||||||
|
export type ProposalTerms = components['schemas']['vegaProposalTerms'];
|
||||||
|
|
||||||
|
interface TxBatchProposalProps {
|
||||||
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
|
pubKey: string | undefined;
|
||||||
|
blockData: TendermintBlocksResponse | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export const TxBatchProposal = ({
|
||||||
|
txData,
|
||||||
|
pubKey,
|
||||||
|
blockData,
|
||||||
|
}: TxBatchProposalProps) => {
|
||||||
|
if (!txData || !txData.command.batchProposalSubmission) {
|
||||||
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
|
}
|
||||||
|
let deterministicId = '';
|
||||||
|
|
||||||
|
const proposal: Proposal = txData.command.batchProposalSubmission;
|
||||||
|
const sig = txData?.signature?.value;
|
||||||
|
if (sig) {
|
||||||
|
deterministicId = txSignatureToDeterministicId(sig);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell {...sharedHeaderProps}>{t('Type')}</TableCell>
|
||||||
|
<TableCell>{t('Batch proposal')}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TxDetailsShared
|
||||||
|
txData={txData}
|
||||||
|
pubKey={pubKey}
|
||||||
|
blockData={blockData}
|
||||||
|
hideTypeRow={true}
|
||||||
|
/>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Batch size')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{proposal.terms?.changes?.length || t('No changes')}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Proposal ID')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Hash text={deterministicId} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableWithTbody>
|
||||||
|
{proposal && (
|
||||||
|
<ProposalSummary
|
||||||
|
id={deterministicId}
|
||||||
|
rationale={proposal?.rationale}
|
||||||
|
terms={proposal.terms}
|
||||||
|
batch={proposal.terms?.changes}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -32,6 +32,9 @@ import { TxDetailsCreateReferralSet } from './tx-create-referral-set';
|
|||||||
import { TxDetailsApplyReferralCode } from './tx-apply-referral-code';
|
import { TxDetailsApplyReferralCode } from './tx-apply-referral-code';
|
||||||
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
|
import { TxDetailsUpdateReferralSet } from './tx-update-referral-set';
|
||||||
import { TxDetailsJoinTeam } from './tx-join-team';
|
import { TxDetailsJoinTeam } from './tx-join-team';
|
||||||
|
import { TxDetailsUpdateMarginMode } from './tx-update-margin-mode';
|
||||||
|
import { TxBatchProposal } from './tx-batch-proposal';
|
||||||
|
import { TxDetailsUpdatePartyProfile } from './proposal/tx-update-party-profile';
|
||||||
|
|
||||||
interface TxDetailsWrapperProps {
|
interface TxDetailsWrapperProps {
|
||||||
txData: BlockExplorerTransactionResult | undefined;
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
@@ -133,6 +136,12 @@ function getTransactionComponent(txData?: BlockExplorerTransactionResult) {
|
|||||||
return TxDetailsApplyReferralCode;
|
return TxDetailsApplyReferralCode;
|
||||||
case 'Join Team':
|
case 'Join Team':
|
||||||
return TxDetailsJoinTeam;
|
return TxDetailsJoinTeam;
|
||||||
|
case 'Update Margin Mode':
|
||||||
|
return TxDetailsUpdateMarginMode;
|
||||||
|
case 'Batch Proposal':
|
||||||
|
return TxBatchProposal;
|
||||||
|
case 'Update Party Profile':
|
||||||
|
return TxDetailsUpdatePartyProfile;
|
||||||
default:
|
default:
|
||||||
return TxDetailsGeneric;
|
return TxDetailsGeneric;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { t } from '@vegaprotocol/i18n';
|
||||||
|
import type { BlockExplorerTransactionResult } from '../../../routes/types/block-explorer-response';
|
||||||
|
import type { TendermintBlocksResponse } from '../../../routes/blocks/tendermint-blocks-response';
|
||||||
|
import { TxDetailsShared } from './shared/tx-details-shared';
|
||||||
|
import { TableCell, TableRow, TableWithTbody } from '../../table';
|
||||||
|
import type { components } from '../../../../types/explorer';
|
||||||
|
import { MarketLink } from '../../links';
|
||||||
|
|
||||||
|
interface TxDetailsUpdateMarginModeProps {
|
||||||
|
txData: BlockExplorerTransactionResult | undefined;
|
||||||
|
pubKey: string | undefined;
|
||||||
|
blockData: TendermintBlocksResponse | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Mode = components['schemas']['UpdateMarginModeMode'];
|
||||||
|
|
||||||
|
const MarginModeLabels: Record<Mode, string> = {
|
||||||
|
MODE_CROSS_MARGIN: t('Cross margin'),
|
||||||
|
MODE_ISOLATED_MARGIN: t('Isolated margin'),
|
||||||
|
MODE_UNSPECIFIED: t('Unspecified'),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TxDetailsUpdateMarginMode = ({
|
||||||
|
txData,
|
||||||
|
pubKey,
|
||||||
|
blockData,
|
||||||
|
}: TxDetailsUpdateMarginModeProps) => {
|
||||||
|
if (!txData || !txData.command.updateMarginMode) {
|
||||||
|
return <>{t('Awaiting Block Explorer transaction details')}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const u: components['schemas']['v1UpdateMarginMode'] =
|
||||||
|
txData.command.updateMarginMode;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableWithTbody className="mb-8" allowWrap={true}>
|
||||||
|
<TxDetailsShared txData={txData} pubKey={pubKey} blockData={blockData} />
|
||||||
|
{u.marketId && (
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Market ID')}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<MarketLink id={u.marketId} />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{u.mode && (
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('New margin mode')}</TableCell>
|
||||||
|
<TableCell>{MarginModeLabels[u.mode]}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
{u.marginFactor && (
|
||||||
|
<TableRow modifier="bordered">
|
||||||
|
<TableCell>{t('Margin factor')}</TableCell>
|
||||||
|
<TableCell>{u.marginFactor}</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableWithTbody>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -20,6 +20,7 @@ export type FilterOption =
|
|||||||
| 'Amend Order'
|
| 'Amend Order'
|
||||||
| 'Apply Referral Code'
|
| 'Apply Referral Code'
|
||||||
| 'Batch Market Instructions'
|
| 'Batch Market Instructions'
|
||||||
|
| 'Batch Proposal'
|
||||||
| 'Cancel LiquidityProvision Order'
|
| 'Cancel LiquidityProvision Order'
|
||||||
| 'Cancel Order'
|
| 'Cancel Order'
|
||||||
| 'Cancel Transfer Funds'
|
| 'Cancel Transfer Funds'
|
||||||
@@ -43,7 +44,9 @@ export type FilterOption =
|
|||||||
| 'Submit Order'
|
| 'Submit Order'
|
||||||
| 'Transfer Funds'
|
| 'Transfer Funds'
|
||||||
| 'Undelegate'
|
| 'Undelegate'
|
||||||
|
| 'Update Party Profile'
|
||||||
| 'Update Referral Set'
|
| 'Update Referral Set'
|
||||||
|
| 'Update Margin Mode'
|
||||||
| 'Validator Heartbeat'
|
| 'Validator Heartbeat'
|
||||||
| 'Vote on Proposal'
|
| 'Vote on Proposal'
|
||||||
| 'Withdraw';
|
| 'Withdraw';
|
||||||
@@ -59,17 +62,25 @@ export const filterOptions: Record<string, FilterOption[]> = {
|
|||||||
'Stop Orders Submission',
|
'Stop Orders Submission',
|
||||||
'Stop Orders Cancellation',
|
'Stop Orders Cancellation',
|
||||||
'Submit Order',
|
'Submit Order',
|
||||||
|
'Update Margin Mode',
|
||||||
],
|
],
|
||||||
'Transfers and Withdrawals': [
|
'Transfers and Withdrawals': [
|
||||||
'Transfer Funds',
|
'Transfer Funds',
|
||||||
'Cancel Transfer Funds',
|
'Cancel Transfer Funds',
|
||||||
'Withdraw',
|
'Withdraw',
|
||||||
],
|
],
|
||||||
Governance: ['Delegate', 'Undelegate', 'Vote on Proposal', 'Proposal'],
|
Governance: [
|
||||||
|
'Batch Proposal',
|
||||||
|
'Delegate',
|
||||||
|
'Undelegate',
|
||||||
|
'Vote on Proposal',
|
||||||
|
'Proposal',
|
||||||
|
],
|
||||||
Referrals: [
|
Referrals: [
|
||||||
'Apply Referral Code',
|
'Apply Referral Code',
|
||||||
'Create Referral Set',
|
'Create Referral Set',
|
||||||
'Join Team',
|
'Join Team',
|
||||||
|
'Update Party Profile',
|
||||||
'Update Referral Set',
|
'Update Referral Set',
|
||||||
],
|
],
|
||||||
'External Data': ['Chain Event', 'Submit Oracle Data'],
|
'External Data': ['Chain Event', 'Submit Oracle Data'],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "Mainnet Stats",
|
"short_name": "Explorer VEGA",
|
||||||
"name": "Vega Mainnet statistics",
|
"name": "Vega Protocol - Explorer",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"short_name": "Mainnet Stats",
|
"short_name": "Governance VEGA",
|
||||||
"name": "Vega Mainnet statistics",
|
"name": "Vega Protocol - Governance",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
<title>Vega Protocol static asseets</title>
|
<title>Vega Protocol static assets</title>
|
||||||
<link rel="stylesheet" href="fonts.css" />
|
<link rel="stylesheet" href="fonts.css" />
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ NX_ICEBERG_ORDERS=true
|
|||||||
NX_METAMASK_SNAPS=true
|
NX_METAMASK_SNAPS=true
|
||||||
NX_REFERRALS=true
|
NX_REFERRALS=true
|
||||||
# NX_DISABLE_CLOSE_POSITION=false
|
# NX_DISABLE_CLOSE_POSITION=false
|
||||||
|
NX_TEAM_COMPETITION=true
|
||||||
|
|
||||||
NX_TENDERMINT_URL=https://be.vega.community
|
NX_TENDERMINT_URL=https://be.vega.community
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.vega.community/websocket
|
||||||
|
|||||||
@@ -28,3 +28,8 @@ NX_REFERRALS=true
|
|||||||
|
|
||||||
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
|
NX_TENDERMINT_URL=https://tm.be.devnet1.vega.xyz/
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.devnet1.vega.xyz/websocket
|
||||||
|
|
||||||
|
NX_TEAM_COMPETITION=true
|
||||||
|
|
||||||
|
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
||||||
|
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
NX_ETHEREUM_PROVIDER_URL=https://eth-mainnet.gateway.pokt.network/v1/lb/af6a2d529a11f8158bc8ca2a
|
NX_ETHEREUM_PROVIDER_URL=https://eth-mainnet.rpc.grove.city/v1/af6a2d529a11f8158bc8ca2a
|
||||||
NX_ETHERSCAN_URL=https://etherscan.io
|
NX_ETHERSCAN_URL=https://etherscan.io
|
||||||
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
NX_GITHUB_FEEDBACK_URL=https://github.com/vegaprotocol/feedback/discussions
|
||||||
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
|
NX_SENTRY_DSN=https://2ffce43721964aafa78277c50654ece4@o286262.ingest.sentry.io/6300613
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ NX_ICEBERG_ORDERS=true
|
|||||||
# NX_PRODUCT_PERPETUALS
|
# NX_PRODUCT_PERPETUALS
|
||||||
NX_METAMASK_SNAPS=true
|
NX_METAMASK_SNAPS=true
|
||||||
NX_REFERRALS=true
|
NX_REFERRALS=true
|
||||||
|
NX_TEAM_COMPETITION=true
|
||||||
|
|
||||||
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
NX_CHARTING_LIBRARY_PATH=https://assets.vega.community/trading-view-bundle/v0.0.1/
|
||||||
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
NX_CHARTING_LIBRARY_HASH=PDjWaqPFndDp+LCvqbKvntWriaqNzNpZ5i9R/BULzCg=
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ NX_ISOLATED_MARGIN=true
|
|||||||
NX_ICEBERG_ORDERS=true
|
NX_ICEBERG_ORDERS=true
|
||||||
NX_METAMASK_SNAPS=true
|
NX_METAMASK_SNAPS=true
|
||||||
NX_REFERRALS=true
|
NX_REFERRALS=true
|
||||||
|
NX_TEAM_COMPETITION=true
|
||||||
|
|
||||||
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
NX_TENDERMINT_URL=https://tm.be.testnet.vega.xyz
|
||||||
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
NX_TENDERMINT_WEBSOCKET_URL=wss://be.testnet.vega.xyz/websocket
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
{
|
{
|
||||||
"short_name": "Mainnet Stats",
|
"name": "Vega Protocol - Trading",
|
||||||
"name": "Vega Mainnet statistics",
|
"short_name": "Console",
|
||||||
|
"description": "Vega Protocol - Trading dApp",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"orientation": "portrait",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"background_color": "#ffffff",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
@@ -12,9 +18,5 @@
|
|||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Intent,
|
||||||
|
TradingAnchorButton,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||||
|
import { addDecimalsFormatNumber } from '@vegaprotocol/utils';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
||||||
|
import { DApp, TokenStaticLinks, useLinks } from '@vegaprotocol/environment';
|
||||||
|
import { RainbowButton } from '../../components/rainbow-button';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
|
import { Box } from '../../components/competitions/box';
|
||||||
|
import { LayoutWithGradient } from '../../components/layouts-inner';
|
||||||
|
import { Links } from '../../lib/links';
|
||||||
|
import { TeamForm, TransactionType } from './team-form';
|
||||||
|
|
||||||
|
export const CompetitionsCreateTeam = () => {
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const isSolo = Boolean(searchParams.get('solo'));
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
usePageTitle(t('Create a team'));
|
||||||
|
|
||||||
|
const { isReadOnly, pubKey } = useVegaWallet();
|
||||||
|
const openWalletDialog = useVegaWalletDialogStore(
|
||||||
|
(store) => store.openVegaWalletDialog
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary feature="create-team">
|
||||||
|
<LayoutWithGradient>
|
||||||
|
<div className="mx-auto md:w-2/3 max-w-xl">
|
||||||
|
<Box className="flex flex-col gap-4">
|
||||||
|
<Link
|
||||||
|
to={Links.COMPETITIONS()}
|
||||||
|
className="text-xs inline-flex items-center gap-1 group"
|
||||||
|
>
|
||||||
|
<VegaIcon
|
||||||
|
name={VegaIconNames.CHEVRON_LEFT}
|
||||||
|
size={12}
|
||||||
|
className="text-vega-clight-100 dark:text-vega-cdark-100"
|
||||||
|
/>{' '}
|
||||||
|
<span className="group-hover:underline">
|
||||||
|
{t('Go back to the competitions')}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<h1 className="calt text-2xl lg:text-3xl xl:text-4xl">
|
||||||
|
{isSolo ? t('Create solo team') : t('Create a team')}
|
||||||
|
</h1>
|
||||||
|
{pubKey && !isReadOnly ? (
|
||||||
|
<CreateTeamFormContainer isSolo={isSolo} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
'Create a team to participate in team based rewards as well as access the discount benefits of the current referral program.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<RainbowButton variant="border" onClick={openWalletDialog}>
|
||||||
|
{t('Connect wallet')}
|
||||||
|
</RainbowButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</LayoutWithGradient>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateTeamFormContainer = ({ isSolo }: { isSolo: boolean }) => {
|
||||||
|
const t = useT();
|
||||||
|
const createLink = useLinks(DApp.Governance);
|
||||||
|
|
||||||
|
const { err, status, code, isEligible, requiredStake, onSubmit } =
|
||||||
|
useReferralSetTransaction({
|
||||||
|
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"
|
||||||
|
data-testid="team-creation-success-message"
|
||||||
|
>
|
||||||
|
<p className="text-sm">{t('Team creation transaction successful')}</p>
|
||||||
|
{code && (
|
||||||
|
<>
|
||||||
|
<dl>
|
||||||
|
<dt className="text-sm">{t('Your team ID:')}</dt>
|
||||||
|
<dl>
|
||||||
|
<span
|
||||||
|
className="font-mono break-all bg-rainbow bg-clip-text text-transparent text-2xl"
|
||||||
|
data-testid="team-id-display"
|
||||||
|
>
|
||||||
|
{code}
|
||||||
|
</span>
|
||||||
|
</dl>
|
||||||
|
</dl>
|
||||||
|
<TradingAnchorButton
|
||||||
|
href={Links.COMPETITIONS_TEAM(code)}
|
||||||
|
intent={Intent.Info}
|
||||||
|
size="small"
|
||||||
|
data-testid="view-team-button"
|
||||||
|
>
|
||||||
|
{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 (
|
||||||
|
<TeamForm
|
||||||
|
type={TransactionType.CreateReferralSet}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
status={status}
|
||||||
|
err={err}
|
||||||
|
isCreatingSoloTeam={isSolo}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
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 '../../lib/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 '../../lib/hooks/use-teams';
|
||||||
|
import take from 'lodash/take';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
import { TeamCard } from '../../components/competitions/team-card';
|
||||||
|
import { useMyTeam } from '../../lib/hooks/use-my-team';
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
const {
|
||||||
|
team: myTeam,
|
||||||
|
stats: myTeamStats,
|
||||||
|
games: myTeamGames,
|
||||||
|
rank: myTeamRank,
|
||||||
|
} = useMyTeam();
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
{/** Team card */}
|
||||||
|
{myTeam ? (
|
||||||
|
<>
|
||||||
|
<h2 className="text-2xl mb-6">{t('My team')}</h2>
|
||||||
|
<div className="mb-12">
|
||||||
|
<TeamCard
|
||||||
|
team={myTeam}
|
||||||
|
rank={myTeamRank}
|
||||||
|
stats={myTeamStats}
|
||||||
|
games={myTeamGames}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/** 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());
|
||||||
|
}}
|
||||||
|
data-testId="create-public-team-button"
|
||||||
|
>
|
||||||
|
{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,255 @@
|
|||||||
|
import { useState, type ButtonHTMLAttributes } from 'react';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import { Splash, truncateMiddle, Loader } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { Table } from '../../components/table';
|
||||||
|
import { formatNumber, getDateTimeFormat } from '@vegaprotocol/utils';
|
||||||
|
import {
|
||||||
|
useTeam,
|
||||||
|
type TeamStats as ITeamStats,
|
||||||
|
type Team as TeamType,
|
||||||
|
type Member,
|
||||||
|
type TeamGame,
|
||||||
|
} from '../../lib/hooks/use-team';
|
||||||
|
import { DApp, EXPLORER_PARTIES, useLinks } from '@vegaprotocol/environment';
|
||||||
|
import { TeamAvatar } from '../../components/competitions/team-avatar';
|
||||||
|
import { TeamStats } from '../../components/competitions/team-stats';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
|
import { LayoutWithGradient } from '../../components/layouts-inner';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import { JoinTeam } from './join-team';
|
||||||
|
import { UpdateTeamButton } from './update-team-button';
|
||||||
|
|
||||||
|
export const CompetitionsTeam = () => {
|
||||||
|
const t = useT();
|
||||||
|
const { teamId } = useParams<{ teamId: string }>();
|
||||||
|
usePageTitle([t('Competitions'), t('Team')]);
|
||||||
|
return (
|
||||||
|
<ErrorBoundary feature="team">
|
||||||
|
<TeamPageContainer teamId={teamId} />
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TeamPageContainer = ({ teamId }: { teamId: string | undefined }) => {
|
||||||
|
const t = useT();
|
||||||
|
const { pubKey } = useVegaWallet();
|
||||||
|
const { data, team, partyTeam, stats, members, games, loading, refetch } =
|
||||||
|
useTeam(teamId, pubKey || undefined);
|
||||||
|
|
||||||
|
// only show spinner on first load so when users join teams its smoother
|
||||||
|
if (!data && loading) {
|
||||||
|
return (
|
||||||
|
<Splash>
|
||||||
|
<Loader />
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!team) {
|
||||||
|
return (
|
||||||
|
<Splash>
|
||||||
|
<p>{t('Page not found')}</p>
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TeamPage
|
||||||
|
team={team}
|
||||||
|
partyTeam={partyTeam}
|
||||||
|
stats={stats}
|
||||||
|
members={members}
|
||||||
|
games={games}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TeamPage = ({
|
||||||
|
team,
|
||||||
|
partyTeam,
|
||||||
|
stats,
|
||||||
|
members,
|
||||||
|
games,
|
||||||
|
refetch,
|
||||||
|
}: {
|
||||||
|
team: TeamType;
|
||||||
|
partyTeam?: TeamType;
|
||||||
|
stats?: ITeamStats;
|
||||||
|
members?: Member[];
|
||||||
|
games?: TeamGame[];
|
||||||
|
refetch: () => void;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const [showGames, setShowGames] = useState(true);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LayoutWithGradient>
|
||||||
|
<header className="flex gap-3 lg:gap-4 pt-5 lg:pt-10">
|
||||||
|
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
|
||||||
|
<div className="flex flex-col items-start gap-1 lg:gap-3">
|
||||||
|
<h1
|
||||||
|
className="calt text-2xl lg:text-3xl xl:text-5xl"
|
||||||
|
data-testid="team-name"
|
||||||
|
>
|
||||||
|
{team.name}
|
||||||
|
</h1>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<JoinTeam team={team} partyTeam={partyTeam} refetch={refetch} />
|
||||||
|
<UpdateTeamButton team={team} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<TeamStats stats={stats} members={members} games={games} />
|
||||||
|
<section>
|
||||||
|
<div className="flex gap-4 lg:gap-8 mb-4 border-b border-default">
|
||||||
|
<ToggleButton
|
||||||
|
active={showGames}
|
||||||
|
onClick={() => setShowGames(true)}
|
||||||
|
data-testid="games-toggle"
|
||||||
|
>
|
||||||
|
{t('Games ({{count}})', { count: games ? games.length : 0 })}
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton
|
||||||
|
active={!showGames}
|
||||||
|
onClick={() => setShowGames(false)}
|
||||||
|
data-testid="members-toggle"
|
||||||
|
>
|
||||||
|
{t('Members ({{count}})', {
|
||||||
|
count: members ? members.length : 0,
|
||||||
|
})}
|
||||||
|
</ToggleButton>
|
||||||
|
</div>
|
||||||
|
{showGames ? <Games games={games} /> : <Members members={members} />}
|
||||||
|
</section>
|
||||||
|
</LayoutWithGradient>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Games = ({ games }: { games?: TeamGame[] }) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
if (!games?.length) {
|
||||||
|
return <p>{t('No games')}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
columns={[
|
||||||
|
{ name: 'rank', displayName: t('Rank') },
|
||||||
|
{
|
||||||
|
name: 'epoch',
|
||||||
|
displayName: t('Epoch'),
|
||||||
|
headerClassName: 'hidden md:table-cell',
|
||||||
|
className: 'hidden md:table-cell',
|
||||||
|
},
|
||||||
|
{ name: 'type', displayName: t('Type') },
|
||||||
|
{ name: 'amount', displayName: t('Amount earned') },
|
||||||
|
{
|
||||||
|
name: 'participatingTeams',
|
||||||
|
displayName: t('No. of participating teams'),
|
||||||
|
headerClassName: 'hidden md:table-cell',
|
||||||
|
className: 'hidden md:table-cell',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'participatingMembers',
|
||||||
|
displayName: t('No. of participating members'),
|
||||||
|
headerClassName: 'hidden md:table-cell',
|
||||||
|
className: 'hidden md:table-cell',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
data={games.map((game) => ({
|
||||||
|
rank: game.team.rank,
|
||||||
|
epoch: game.epoch,
|
||||||
|
type: DispatchMetricLabels[game.team.rewardMetric as DispatchMetric],
|
||||||
|
amount: formatNumber(game.team.totalRewardsEarned),
|
||||||
|
participatingTeams: game.entities.length,
|
||||||
|
participatingMembers: game.numberOfParticipants,
|
||||||
|
}))}
|
||||||
|
noCollapse={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Members = ({ members }: { members?: Member[] }) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
if (!members?.length) {
|
||||||
|
return <p>{t('No members')}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = orderBy(
|
||||||
|
members.map((m) => ({
|
||||||
|
referee: <RefereeLink pubkey={m.referee} isCreator={m.isCreator} />,
|
||||||
|
rewards: formatNumber(m.totalQuantumRewards),
|
||||||
|
volume: formatNumber(m.totalQuantumVolume),
|
||||||
|
gamesPlayed: formatNumber(m.totalGamesPlayed),
|
||||||
|
joinedAt: getDateTimeFormat().format(new Date(m.joinedAt)),
|
||||||
|
joinedAtEpoch: Number(m.joinedAtEpoch),
|
||||||
|
})),
|
||||||
|
'joinedAtEpoch',
|
||||||
|
'desc'
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Table
|
||||||
|
columns={[
|
||||||
|
{ name: 'referee', displayName: t('Member ID') },
|
||||||
|
{ name: 'rewards', displayName: t('Rewards earned') },
|
||||||
|
{ name: 'volume', displayName: t('Total volume') },
|
||||||
|
{ name: 'gamesPlayed', displayName: t('Games played') },
|
||||||
|
{
|
||||||
|
name: 'joinedAt',
|
||||||
|
displayName: t('Joined at'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'joinedAtEpoch',
|
||||||
|
displayName: t('Joined epoch'),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
data={data}
|
||||||
|
noCollapse={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RefereeLink = ({
|
||||||
|
pubkey,
|
||||||
|
isCreator,
|
||||||
|
}: {
|
||||||
|
pubkey: string;
|
||||||
|
isCreator: boolean;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const linkCreator = useLinks(DApp.Explorer);
|
||||||
|
const link = linkCreator(EXPLORER_PARTIES.replace(':id', pubkey));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link to={link} target="_blank" className="underline underline-offset-4">
|
||||||
|
{truncateMiddle(pubkey)}
|
||||||
|
</Link>{' '}
|
||||||
|
<span className="text-muted text-xs">{isCreator ? t('Owner') : ''}</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ToggleButton = ({
|
||||||
|
active,
|
||||||
|
...props
|
||||||
|
}: ButtonHTMLAttributes<HTMLButtonElement> & { active: boolean }) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
{...props}
|
||||||
|
className={classNames('relative top-px uppercase border-b-2 py-4', {
|
||||||
|
'text-muted border-transparent': !active,
|
||||||
|
'border-vega-yellow': active,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
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 '../../lib/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();
|
||||||
|
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
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-full md:w-60 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,163 @@
|
|||||||
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
import { Box } from '../../components/competitions/box';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||||
|
import {
|
||||||
|
Intent,
|
||||||
|
Loader,
|
||||||
|
Splash,
|
||||||
|
TradingAnchorButton,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { RainbowButton } from '../../components/rainbow-button';
|
||||||
|
import { Link, Navigate, useParams } from 'react-router-dom';
|
||||||
|
import { Links } from '../../lib/links';
|
||||||
|
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
||||||
|
import { type FormFields, TeamForm, TransactionType } from './team-form';
|
||||||
|
import { useTeam } from '../../lib/hooks/use-team';
|
||||||
|
import { LayoutWithGradient } from '../../components/layouts-inner';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const CompetitionsUpdateTeam = () => {
|
||||||
|
const t = useT();
|
||||||
|
usePageTitle([t('Competitions'), t('Update a team')]);
|
||||||
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
const openWalletDialog = useVegaWalletDialogStore(
|
||||||
|
(store) => store.openVegaWalletDialog
|
||||||
|
);
|
||||||
|
const { teamId } = useParams<{ teamId: string }>();
|
||||||
|
if (!teamId) {
|
||||||
|
return <Navigate to={Links.COMPETITIONS()} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary feature="update-team">
|
||||||
|
<LayoutWithGradient>
|
||||||
|
<div className="mx-auto md:w-2/3 max-w-xl">
|
||||||
|
<Box className="flex flex-col gap-4">
|
||||||
|
<Link
|
||||||
|
to={Links.COMPETITIONS_TEAM(teamId)}
|
||||||
|
className="text-xs inline-flex items-center gap-1 group"
|
||||||
|
>
|
||||||
|
<VegaIcon
|
||||||
|
name={VegaIconNames.CHEVRON_LEFT}
|
||||||
|
size={12}
|
||||||
|
className="text-vega-clight-100 dark:text-vega-cdark-100"
|
||||||
|
/>{' '}
|
||||||
|
<span className="group-hover:underline">
|
||||||
|
{t('Go back to the team profile')}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
<h1 className="calt text-2xl lg:text-3xl xl:text-5xl">
|
||||||
|
{t('Update a team')}
|
||||||
|
</h1>
|
||||||
|
{pubKey && !isReadOnly ? (
|
||||||
|
<UpdateTeamFormContainer teamId={teamId} pubKey={pubKey} />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p>{t('Connect to update the details of your team.')}</p>
|
||||||
|
<RainbowButton variant="border" onClick={openWalletDialog}>
|
||||||
|
{t('Connect wallet')}
|
||||||
|
</RainbowButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
</LayoutWithGradient>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const UpdateTeamFormContainer = ({
|
||||||
|
teamId,
|
||||||
|
pubKey,
|
||||||
|
}: {
|
||||||
|
teamId: string;
|
||||||
|
pubKey: string;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const [refetching, setRefetching] = useState<boolean>(false);
|
||||||
|
const { team, loading, error, refetch } = useTeam(teamId, pubKey);
|
||||||
|
|
||||||
|
const { err, status, onSubmit } = useReferralSetTransaction({
|
||||||
|
onSuccess: () => {
|
||||||
|
// NOOP
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// refetch when saved
|
||||||
|
useEffect(() => {
|
||||||
|
if (refetch && status === 'confirmed') {
|
||||||
|
refetch();
|
||||||
|
setRefetching(true);
|
||||||
|
}
|
||||||
|
}, [refetch, status]);
|
||||||
|
|
||||||
|
if (loading && !refetching) {
|
||||||
|
return <Loader size="small" />;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<Splash className="gap-1">
|
||||||
|
<span>{t('Something went wrong.')}</span>
|
||||||
|
<Link to={Links.COMPETITIONS_TEAM(teamId)} className="underline">
|
||||||
|
{t("Go back to the team's profile")}
|
||||||
|
</Link>
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMyTeam = team?.referrer === pubKey;
|
||||||
|
if (!isMyTeam) {
|
||||||
|
return <Navigate to={Links.COMPETITIONS_TEAM(teamId)} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'confirmed') {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex flex-col items-start gap-2"
|
||||||
|
data-testid="team-creation-success-message"
|
||||||
|
>
|
||||||
|
<p className="text-sm">
|
||||||
|
<VegaIcon
|
||||||
|
name={VegaIconNames.TICK}
|
||||||
|
size={18}
|
||||||
|
className="text-vega-green-500"
|
||||||
|
/>{' '}
|
||||||
|
{t('Changes successfully saved to your team.')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<TradingAnchorButton
|
||||||
|
href={Links.COMPETITIONS_TEAM(teamId)}
|
||||||
|
intent={Intent.Info}
|
||||||
|
size="small"
|
||||||
|
data-testid="view-team-button"
|
||||||
|
>
|
||||||
|
{t('View team')}
|
||||||
|
</TradingAnchorButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultValues: FormFields = {
|
||||||
|
id: team.teamId,
|
||||||
|
name: team.name,
|
||||||
|
url: team.teamUrl,
|
||||||
|
avatarUrl: team.avatarUrl,
|
||||||
|
private: team.closed,
|
||||||
|
allowList: team.allowList.join(','),
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TeamForm
|
||||||
|
type={TransactionType.UpdateReferralSet}
|
||||||
|
status={status}
|
||||||
|
err={err}
|
||||||
|
isCreatingSoloTeam={team.closed}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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 '../../lib/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,235 @@
|
|||||||
|
import {
|
||||||
|
TradingButton as Button,
|
||||||
|
Dialog,
|
||||||
|
Intent,
|
||||||
|
Tooltip,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { useSimpleTransaction, useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { type Team } from '../../lib/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 [confirmDialog, setConfirmDialog] = useState<JoinType>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<JoinButton
|
||||||
|
team={team}
|
||||||
|
partyTeam={partyTeam}
|
||||||
|
pubKey={pubKey}
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
onJoin={setConfirmDialog}
|
||||||
|
/>
|
||||||
|
<Dialog
|
||||||
|
open={confirmDialog !== undefined}
|
||||||
|
onChange={() => setConfirmDialog(undefined)}
|
||||||
|
>
|
||||||
|
{confirmDialog !== undefined && (
|
||||||
|
<DialogContent
|
||||||
|
type={confirmDialog}
|
||||||
|
team={team}
|
||||||
|
partyTeam={partyTeam}
|
||||||
|
onCancel={() => setConfirmDialog(undefined)}
|
||||||
|
refetch={refetch}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</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={t('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}
|
||||||
|
data-testid="switch-team-button"
|
||||||
|
>
|
||||||
|
{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,
|
||||||
|
team,
|
||||||
|
partyTeam,
|
||||||
|
onCancel,
|
||||||
|
refetch,
|
||||||
|
}: {
|
||||||
|
type: JoinType;
|
||||||
|
team: Team;
|
||||||
|
partyTeam?: Team;
|
||||||
|
onCancel: () => void;
|
||||||
|
refetch: () => void;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
const { send, status, error } = useSimpleTransaction({
|
||||||
|
onSuccess: refetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
const joinTeam = () => {
|
||||||
|
send({
|
||||||
|
joinTeam: {
|
||||||
|
id: team.teamId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<p className="text-vega-red break-words first-letter:capitalize">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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={joinTeam}
|
||||||
|
intent={Intent.Success}
|
||||||
|
data-testid="confirm-switch-button"
|
||||||
|
>
|
||||||
|
{t('Confirm')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onCancel} intent={Intent.Danger}>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,318 @@
|
|||||||
|
import {
|
||||||
|
TradingFormGroup,
|
||||||
|
TradingInput,
|
||||||
|
TradingInputError,
|
||||||
|
TradingCheckbox,
|
||||||
|
TextArea,
|
||||||
|
TradingButton,
|
||||||
|
Intent,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { URL_REGEX, isValidVegaPublicKey } from '@vegaprotocol/utils';
|
||||||
|
|
||||||
|
import { type useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
|
import type {
|
||||||
|
CreateReferralSet,
|
||||||
|
UpdateReferralSet,
|
||||||
|
Status,
|
||||||
|
} from '@vegaprotocol/wallet';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useLayoutEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export type FormFields = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
avatarUrl: string;
|
||||||
|
private: boolean;
|
||||||
|
allowList: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum TransactionType {
|
||||||
|
CreateReferralSet = 'CreateReferralSet',
|
||||||
|
UpdateReferralSet = 'UpdateReferralSet',
|
||||||
|
}
|
||||||
|
|
||||||
|
const prepareTransaction = (
|
||||||
|
type: TransactionType,
|
||||||
|
fields: FormFields
|
||||||
|
): CreateReferralSet | UpdateReferralSet => {
|
||||||
|
switch (type) {
|
||||||
|
case TransactionType.CreateReferralSet:
|
||||||
|
return {
|
||||||
|
createReferralSet: {
|
||||||
|
isTeam: true,
|
||||||
|
team: {
|
||||||
|
name: fields.name,
|
||||||
|
teamUrl: fields.url,
|
||||||
|
avatarUrl: fields.avatarUrl,
|
||||||
|
closed: fields.private,
|
||||||
|
allowList: fields.private
|
||||||
|
? parseAllowListText(fields.allowList)
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case TransactionType.UpdateReferralSet:
|
||||||
|
return {
|
||||||
|
updateReferralSet: {
|
||||||
|
id: fields.id,
|
||||||
|
isTeam: true,
|
||||||
|
team: {
|
||||||
|
name: fields.name,
|
||||||
|
teamUrl: fields.url,
|
||||||
|
avatarUrl: fields.avatarUrl,
|
||||||
|
closed: fields.private,
|
||||||
|
allowList: fields.private
|
||||||
|
? parseAllowListText(fields.allowList)
|
||||||
|
: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TeamForm = ({
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
err,
|
||||||
|
isCreatingSoloTeam,
|
||||||
|
onSubmit,
|
||||||
|
defaultValues,
|
||||||
|
}: {
|
||||||
|
type: TransactionType;
|
||||||
|
status: ReturnType<typeof useReferralSetTransaction>['status'];
|
||||||
|
err: ReturnType<typeof useReferralSetTransaction>['err'];
|
||||||
|
isCreatingSoloTeam: boolean;
|
||||||
|
onSubmit: ReturnType<typeof useReferralSetTransaction>['onSubmit'];
|
||||||
|
defaultValues?: FormFields;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
control,
|
||||||
|
watch,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<FormFields>({
|
||||||
|
defaultValues: {
|
||||||
|
private: isCreatingSoloTeam,
|
||||||
|
...defaultValues,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPrivate = watch('private');
|
||||||
|
|
||||||
|
const sendTransaction = (fields: FormFields) => {
|
||||||
|
onSubmit(prepareTransaction(type, fields));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(sendTransaction)}>
|
||||||
|
<input type="hidden" {...register('id')} />
|
||||||
|
<TradingFormGroup label={t('Team name')} labelFor="name">
|
||||||
|
<TradingInput
|
||||||
|
{...register('name', { required: t('Required') })}
|
||||||
|
data-testid="team-name-input"
|
||||||
|
/>
|
||||||
|
{errors.name?.message && (
|
||||||
|
<TradingInputError forInput="name" data-testid="team-name-error">
|
||||||
|
{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') },
|
||||||
|
})}
|
||||||
|
data-testid="team-url-input"
|
||||||
|
/>
|
||||||
|
{errors.url?.message && (
|
||||||
|
<TradingInputError forInput="url" data-testid="team-url-error">
|
||||||
|
{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'),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
data-testid="avatar-url-input"
|
||||||
|
/>
|
||||||
|
{errors.avatarUrl?.message && (
|
||||||
|
<TradingInputError
|
||||||
|
forInput="avatarUrl"
|
||||||
|
data-testid="avatar-url-error"
|
||||||
|
>
|
||||||
|
{errors.avatarUrl.message}
|
||||||
|
</TradingInputError>
|
||||||
|
)}
|
||||||
|
</TradingFormGroup>
|
||||||
|
{
|
||||||
|
// allow changing to private/public if editing, but don't show these options if making a solo team
|
||||||
|
(type === TransactionType.UpdateReferralSet || !isCreatingSoloTeam) && (
|
||||||
|
<>
|
||||||
|
<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);
|
||||||
|
}}
|
||||||
|
data-testid="team-private-checkbox"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</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'),
|
||||||
|
validate: {
|
||||||
|
allowList: (value) => {
|
||||||
|
const publicKeys = parseAllowListText(value);
|
||||||
|
if (
|
||||||
|
publicKeys.every((pk) => isValidVegaPublicKey(pk))
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return t('Invalid public key found in allow list');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
data-testid="team-allow-list-textarea"
|
||||||
|
/>
|
||||||
|
{errors.allowList?.message && (
|
||||||
|
<TradingInputError
|
||||||
|
forInput="avatarUrl"
|
||||||
|
data-testid="team-allow-list-error"
|
||||||
|
>
|
||||||
|
{errors.allowList.message}
|
||||||
|
</TradingInputError>
|
||||||
|
)}
|
||||||
|
</TradingFormGroup>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{err && (
|
||||||
|
<p className="text-danger text-xs mb-4 first-letter:capitalize">
|
||||||
|
{err}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<SubmitButton type={type} status={status} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SubmitButton = ({
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
}: {
|
||||||
|
type?: TransactionType;
|
||||||
|
status: Status;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const disabled = status === 'pending' || status === 'requested';
|
||||||
|
|
||||||
|
let text = t('Create');
|
||||||
|
if (type === TransactionType.UpdateReferralSet) {
|
||||||
|
text = t('Update');
|
||||||
|
}
|
||||||
|
|
||||||
|
let confirmedText = t('Created');
|
||||||
|
if (type === TransactionType.UpdateReferralSet) {
|
||||||
|
confirmedText = t('Updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === 'requested') {
|
||||||
|
text = t('Confirm in wallet...');
|
||||||
|
} else if (status === 'pending') {
|
||||||
|
text = t('Confirming transaction...');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [showConfirmed, setShowConfirmed] = useState<boolean>(false);
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
let to: ReturnType<typeof setTimeout>;
|
||||||
|
if (status === 'confirmed' && !showConfirmed) {
|
||||||
|
to = setTimeout(() => {
|
||||||
|
setShowConfirmed(true);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
clearTimeout(to);
|
||||||
|
};
|
||||||
|
}, [showConfirmed, status]);
|
||||||
|
|
||||||
|
const confirmed = (
|
||||||
|
<span
|
||||||
|
className={classNames('text-sm transition-opacity opacity-0', {
|
||||||
|
'opacity-100': showConfirmed,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<VegaIcon
|
||||||
|
name={VegaIconNames.TICK}
|
||||||
|
size={18}
|
||||||
|
className="text-vega-green-500"
|
||||||
|
/>{' '}
|
||||||
|
{confirmedText}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 items-baseline">
|
||||||
|
<TradingButton
|
||||||
|
type="submit"
|
||||||
|
intent={Intent.Info}
|
||||||
|
disabled={disabled}
|
||||||
|
data-testid="team-form-submit-button"
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</TradingButton>
|
||||||
|
{status === 'confirmed' && confirmed}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const parseAllowListText = (str: string = '') => {
|
||||||
|
return str
|
||||||
|
.split(',')
|
||||||
|
.map((v) => v.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { type Team } from '../../lib/hooks/use-team';
|
||||||
|
import { type ComponentProps } from 'react';
|
||||||
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import { Intent, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { Links } from '../../lib/links';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
|
||||||
|
export const UpdateTeamButton = ({
|
||||||
|
team,
|
||||||
|
size = 'medium',
|
||||||
|
}: {
|
||||||
|
team: Pick<Team, 'teamId' | 'referrer'>;
|
||||||
|
size?: ComponentProps<typeof TradingAnchorButton>['size'];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
|
|
||||||
|
if (pubKey && !isReadOnly && pubKey === team.referrer) {
|
||||||
|
return (
|
||||||
|
<TradingAnchorButton
|
||||||
|
size={size}
|
||||||
|
data-testid="update-team-button"
|
||||||
|
href={Links.COMPETITIONS_UPDATE_TEAM(team.teamId)}
|
||||||
|
intent={Intent.Info}
|
||||||
|
>
|
||||||
|
{t('Update team')}
|
||||||
|
</TradingAnchorButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
@@ -1,27 +1,20 @@
|
|||||||
import { useEffect } from 'react';
|
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
|
||||||
import { titlefy } from '@vegaprotocol/utils';
|
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
import { FeesContainer } from '../../components/fees-container';
|
import { FeesContainer } from '../../components/fees-container';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { usePageTitleStore } from '../../stores';
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
|
||||||
export const Fees = () => {
|
export const Fees = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const title = t('Fees');
|
const title = t('Fees');
|
||||||
const { updateTitle } = usePageTitleStore((store) => ({
|
usePageTitle(title);
|
||||||
updateTitle: store.updateTitle,
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateTitle(titlefy([title]));
|
|
||||||
}, [updateTitle, title]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary feature="fees">
|
<ErrorBoundary feature="fees">
|
||||||
<div className="container p-4 mx-auto">
|
<TinyScroll className="p-4 max-h-full overflow-auto">
|
||||||
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
|
<h1 className="md:px-4 pb-4 text-2xl">{title}</h1>
|
||||||
<FeesContainer />
|
<FeesContainer />
|
||||||
</div>
|
</TinyScroll>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
useFundingRate,
|
useFundingRate,
|
||||||
useMarketTradingMode,
|
useMarketTradingMode,
|
||||||
useExternalTwap,
|
useExternalTwap,
|
||||||
|
getQuoteName,
|
||||||
} from '@vegaprotocol/markets';
|
} from '@vegaprotocol/markets';
|
||||||
import { MarketState as State } from '@vegaprotocol/types';
|
import { MarketState as State } from '@vegaprotocol/types';
|
||||||
import { HeaderStat } from '../../components/header';
|
import { HeaderStat } from '../../components/header';
|
||||||
@@ -41,6 +42,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
|||||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||||
|
|
||||||
const asset = getAsset(market);
|
const asset = getAsset(market);
|
||||||
|
const quoteUnit = getQuoteName(market);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -54,12 +56,15 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
|||||||
<Last24hPriceChange
|
<Last24hPriceChange
|
||||||
marketId={market.id}
|
marketId={market.id}
|
||||||
decimalPlaces={market.decimalPlaces}
|
decimalPlaces={market.decimalPlaces}
|
||||||
|
fallback={<span>-</span>}
|
||||||
/>
|
/>
|
||||||
</HeaderStat>
|
</HeaderStat>
|
||||||
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
|
<HeaderStat heading={t('Volume (24h)')} testId="market-volume">
|
||||||
<Last24hVolume
|
<Last24hVolume
|
||||||
marketId={market.id}
|
marketId={market.id}
|
||||||
positionDecimalPlaces={market.positionDecimalPlaces}
|
positionDecimalPlaces={market.positionDecimalPlaces}
|
||||||
|
marketDecimals={market.decimalPlaces}
|
||||||
|
quoteUnit={quoteUnit}
|
||||||
/>
|
/>
|
||||||
</HeaderStat>
|
</HeaderStat>
|
||||||
<HeaderStatMarketTradingMode
|
<HeaderStatMarketTradingMode
|
||||||
@@ -108,7 +113,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
|||||||
heading={`${t('Funding Rate')} / ${t('Countdown')}`}
|
heading={`${t('Funding Rate')} / ${t('Countdown')}`}
|
||||||
testId="market-funding"
|
testId="market-funding"
|
||||||
>
|
>
|
||||||
<div className="flex justify-between gap-2">
|
<div className="flex gap-2">
|
||||||
<FundingRate marketId={market.id} />
|
<FundingRate marketId={market.id} />
|
||||||
<FundingCountdown marketId={market.id} />
|
<FundingCountdown marketId={market.id} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { type Market } from '@vegaprotocol/markets';
|
|||||||
// TODO: handle oracle banner
|
// TODO: handle oracle banner
|
||||||
// import { OracleBanner } from '@vegaprotocol/markets';
|
// import { OracleBanner } from '@vegaprotocol/markets';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
@@ -12,21 +11,21 @@ import {
|
|||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { MarketBanner } from '../../components/market-banner';
|
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
import { type TradingView } from './trade-views';
|
import { type TradingView } from './trade-views';
|
||||||
import { TradingViews } from './trade-views';
|
import { TradingViews } from './trade-views';
|
||||||
|
|
||||||
interface TradePanelsProps {
|
interface TradePanelsProps {
|
||||||
market: Market;
|
market: Market;
|
||||||
pinnedAsset?: PinnedAsset;
|
pinnedAsset?: PinnedAsset;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
||||||
const [view, setView] = useState<TradingView>('chart');
|
const [topView, setTopView] = useState<TradingView>('chart');
|
||||||
const viewCfg = TradingViews[view];
|
const topViewCfg = TradingViews[topView];
|
||||||
|
const [bottomView, setBottomView] = useState<TradingView>('positions');
|
||||||
|
const bottomViewCfg = TradingViews[bottomView];
|
||||||
|
|
||||||
const renderView = () => {
|
const renderView = (view: TradingView) => {
|
||||||
const Component = TradingViews[view].component;
|
const Component = TradingViews[view].component;
|
||||||
|
|
||||||
if (!Component) {
|
if (!Component) {
|
||||||
@@ -39,12 +38,13 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
|||||||
// so watch out for clashes in props
|
// so watch out for clashes in props
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary feature={view}>
|
<ErrorBoundary feature={view}>
|
||||||
<Component marketId={market?.id} pinnedAsset={pinnedAsset} />;
|
<Component marketId={market?.id} pinnedAsset={pinnedAsset} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderMenu = () => {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const renderMenu = (viewCfg: any) => {
|
||||||
if ('menu' in viewCfg || 'settings' in viewCfg) {
|
if ('menu' in viewCfg || 'settings' in viewCfg) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
|
<div className="flex items-center justify-end gap-1 p-1 bg-vega-clight-800 dark:bg-vega-cdark-800 border-b border-default">
|
||||||
@@ -69,55 +69,80 @@ export const TradePanels = ({ market, pinnedAsset }: TradePanelsProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full grid grid-rows-[min-content_min-content_1fr_min-content]">
|
<div className="h-full flex flex-col lg:grid grid-rows-[min-content_min-content_1fr_min-content]">
|
||||||
<div>
|
<div className="flex flex-col w-full overflow-hidden">
|
||||||
<MarketBanner market={market} />
|
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||||
</div>
|
{['chart', 'orderbook', 'trades', 'liquidity', 'fundingPayments']
|
||||||
<div>{renderMenu()}</div>
|
// filter to control available views for the current market
|
||||||
<div className="h-full relative">
|
// e.g. only perpetuals should get the funding views
|
||||||
<AutoSizer>
|
.filter((_key) => {
|
||||||
{({ width, height }) => (
|
const key = _key as TradingView;
|
||||||
<div style={{ width, height }} className="overflow-auto">
|
const perpOnlyViews = ['funding', 'fundingPayments'];
|
||||||
{renderView()}
|
|
||||||
</div>
|
if (
|
||||||
)}
|
market?.tradableInstrument.instrument.product.__typename ===
|
||||||
</AutoSizer>
|
'Perpetual'
|
||||||
</div>
|
) {
|
||||||
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
return true;
|
||||||
{Object.keys(TradingViews)
|
}
|
||||||
// filter to control available views for the current market
|
|
||||||
// eg only perps should get the funding views
|
if (perpOnlyViews.includes(key)) {
|
||||||
.filter((_key) => {
|
return false;
|
||||||
const key = _key as TradingView;
|
}
|
||||||
const perpOnlyViews = ['funding', 'fundingPayments'];
|
|
||||||
|
|
||||||
if (
|
|
||||||
market?.tradableInstrument.instrument.product.__typename ===
|
|
||||||
'Perpetual'
|
|
||||||
) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
})
|
||||||
|
.map((_key) => {
|
||||||
|
const key = _key as TradingView;
|
||||||
|
const isActive = topView === key;
|
||||||
|
return (
|
||||||
|
<ViewButton
|
||||||
|
key={key}
|
||||||
|
view={key}
|
||||||
|
isActive={isActive}
|
||||||
|
onClick={() => {
|
||||||
|
setTopView(key);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="h-[50vh] lg:h-full relative">
|
||||||
|
<div>{renderMenu(topViewCfg)}</div>
|
||||||
|
<div className="overflow-auto h-full">{renderView(topView)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
if (perpOnlyViews.includes(key)) {
|
<div className="flex flex-col w-full grow">
|
||||||
return false;
|
<div className="flex flex-nowrap overflow-x-auto max-w-full border-t border-default">
|
||||||
}
|
{[
|
||||||
|
'positions',
|
||||||
return true;
|
'activeOrders',
|
||||||
})
|
'closedOrders',
|
||||||
.map((_key) => {
|
'rejectedOrders',
|
||||||
|
'orders',
|
||||||
|
'stopOrders',
|
||||||
|
'collateral',
|
||||||
|
'fills',
|
||||||
|
].map((_key) => {
|
||||||
const key = _key as TradingView;
|
const key = _key as TradingView;
|
||||||
const isActive = view === key;
|
const isActive = bottomView === key;
|
||||||
return (
|
return (
|
||||||
<ViewButton
|
<ViewButton
|
||||||
key={key}
|
key={key}
|
||||||
view={key}
|
view={key}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setView(key);
|
setBottomView(key);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="relative grow">
|
||||||
|
<div className="flex flex-col">{renderMenu(bottomViewCfg)}</div>
|
||||||
|
<div className="overflow-auto h-full">{renderView(bottomView)}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -157,7 +182,7 @@ const useViewLabel = (view: TradingView) => {
|
|||||||
depth: t('Depth'),
|
depth: t('Depth'),
|
||||||
liquidity: t('Liquidity'),
|
liquidity: t('Liquidity'),
|
||||||
funding: t('Funding'),
|
funding: t('Funding'),
|
||||||
fundingPayments: t('Funding Payments'),
|
fundingPayments: t('Funding'),
|
||||||
orderbook: t('Orderbook'),
|
orderbook: t('Orderbook'),
|
||||||
trades: t('Trades'),
|
trades: t('Trades'),
|
||||||
positions: t('Positions'),
|
positions: t('Positions'),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
useDataGridEvents,
|
useDataGridEvents,
|
||||||
} from '@vegaprotocol/datagrid';
|
} from '@vegaprotocol/datagrid';
|
||||||
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
|
import type { MarketMaybeWithData } from '@vegaprotocol/markets';
|
||||||
import { useColumnDefs } from './use-column-defs';
|
import { useMarketsColumnDefs } from './use-column-defs';
|
||||||
import type { DataGridStore } from '../../stores/datagrid-store-slice';
|
import type { DataGridStore } from '../../stores/datagrid-store-slice';
|
||||||
import { type StateCreator, create } from 'zustand';
|
import { type StateCreator, create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
@@ -50,7 +50,7 @@ export const useMarketsStore = create<DataGridSlice>()(
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const MarketListTable = (props: Props) => {
|
export const MarketListTable = (props: Props) => {
|
||||||
const columnDefs = useColumnDefs();
|
const columnDefs = useMarketsColumnDefs();
|
||||||
const gridStore = useMarketsStore((store) => store.gridStore);
|
const gridStore = useMarketsStore((store) => store.gridStore);
|
||||||
const updateGridStore = useMarketsStore((store) => store.updateGridStore);
|
const updateGridStore = useMarketsStore((store) => store.updateGridStore);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import React, { useEffect } from 'react';
|
|
||||||
import { titlefy } from '@vegaprotocol/utils';
|
|
||||||
import {
|
import {
|
||||||
LocalStoragePersistTabs as Tabs,
|
LocalStoragePersistTabs as Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
@@ -7,7 +5,6 @@ import {
|
|||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { OpenMarkets } from './open-markets';
|
import { OpenMarkets } from './open-markets';
|
||||||
import { Proposed } from './proposed';
|
import { Proposed } from './proposed';
|
||||||
import { usePageTitleStore } from '../../stores';
|
|
||||||
import { Closed } from './closed';
|
import { Closed } from './closed';
|
||||||
import {
|
import {
|
||||||
DApp,
|
DApp,
|
||||||
@@ -17,19 +14,14 @@ import {
|
|||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
import { MarketsSettings } from './markets-settings';
|
import { MarketsSettings } from './markets-settings';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
|
||||||
export const MarketsPage = () => {
|
export const MarketsPage = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { updateTitle } = usePageTitleStore((store) => ({
|
|
||||||
updateTitle: store.updateTitle,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const governanceLink = useLinks(DApp.Governance);
|
const governanceLink = useLinks(DApp.Governance);
|
||||||
const externalLink = governanceLink(TOKEN_NEW_MARKET_PROPOSAL);
|
const externalLink = governanceLink(TOKEN_NEW_MARKET_PROPOSAL);
|
||||||
|
|
||||||
useEffect(() => {
|
usePageTitle(t('Markets'));
|
||||||
updateTitle(titlefy([t('Markets')]));
|
|
||||||
}, [updateTitle, t]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full pt-0.5 pb-3 px-1.5">
|
<div className="h-full pt-0.5 pb-3 px-1.5">
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import { Route, Routes } from 'react-router-dom';
|
||||||
|
import {
|
||||||
|
Intent,
|
||||||
|
MobileActionsDropdown,
|
||||||
|
Tooltip,
|
||||||
|
TradingButton,
|
||||||
|
TradingDropdownItem,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { type BarView, ViewType, useSidebar } from '../../components/sidebar';
|
||||||
|
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
|
||||||
|
|
||||||
|
const ViewInitializer = () => {
|
||||||
|
const currentRouteId = useGetCurrentRouteId();
|
||||||
|
const { setViews, getView } = useSidebar();
|
||||||
|
const view = getView(currentRouteId);
|
||||||
|
const { screenSize } = useScreenDimensions();
|
||||||
|
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
|
||||||
|
useEffect(() => {
|
||||||
|
if (largeScreen && view === undefined) {
|
||||||
|
setViews({ type: ViewType.Order }, currentRouteId);
|
||||||
|
}
|
||||||
|
}, [setViews, view, currentRouteId, largeScreen]);
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MarketsMobileSidebar = () => {
|
||||||
|
const t = useT();
|
||||||
|
const currentRouteId = useGetCurrentRouteId();
|
||||||
|
const { pubKeys, isReadOnly } = useVegaWallet();
|
||||||
|
const openVegaWalletDialog = useVegaWalletDialogStore(
|
||||||
|
(store) => store.openVegaWalletDialog
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Routes>
|
||||||
|
<Route
|
||||||
|
path=":marketId"
|
||||||
|
element={
|
||||||
|
<>
|
||||||
|
<ViewInitializer />
|
||||||
|
<div className="grid grid-cols-3 grow md:grow-0 md:flex lg:flex-col items-center gap-2 lg:gap-4 p-1">
|
||||||
|
{!pubKeys || isReadOnly ? (
|
||||||
|
<>
|
||||||
|
<TradingButton
|
||||||
|
intent={Intent.Primary}
|
||||||
|
size="medium"
|
||||||
|
onClick={() => {
|
||||||
|
openVegaWalletDialog();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('Connect')}
|
||||||
|
</TradingButton>
|
||||||
|
<MobileButton
|
||||||
|
view={ViewType.Order}
|
||||||
|
tooltip={t('Trade')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
<MobileBarActionsDropdown currentRouteId={currentRouteId} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<MobileButton
|
||||||
|
view={ViewType.Order}
|
||||||
|
tooltip={t('Trade')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
<MobileButton
|
||||||
|
view={ViewType.Deposit}
|
||||||
|
tooltip={t('Deposit')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
<MobileBarActionsDropdown currentRouteId={currentRouteId} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MobileButton = ({
|
||||||
|
view,
|
||||||
|
tooltip: label,
|
||||||
|
disabled = false,
|
||||||
|
onClick,
|
||||||
|
routeId,
|
||||||
|
}: {
|
||||||
|
view?: ViewType;
|
||||||
|
tooltip: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
routeId: string;
|
||||||
|
}) => {
|
||||||
|
const { setViews, getView } = useSidebar((store) => ({
|
||||||
|
setViews: store.setViews,
|
||||||
|
getView: store.getView,
|
||||||
|
}));
|
||||||
|
const currView = getView(routeId);
|
||||||
|
const onSelect = (view: BarView['type']) => {
|
||||||
|
if (view === currView?.type) {
|
||||||
|
setViews(null, routeId);
|
||||||
|
} else {
|
||||||
|
setViews({ type: view }, routeId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonClasses = classNames(
|
||||||
|
'flex items-center p-1 rounded',
|
||||||
|
'disabled:cursor-not-allowed disabled:text-vega-clight-500 dark:disabled:text-vega-cdark-500',
|
||||||
|
{
|
||||||
|
'text-vega-clight-200 dark:text-vega-cdark-200 enabled:hover:bg-vega-clight-500 dark:enabled:hover:bg-vega-cdark-500':
|
||||||
|
!view || view !== currView?.type,
|
||||||
|
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 text-black':
|
||||||
|
view && view === currView?.type,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip description={label} align="center" side="right" sideOffset={10}>
|
||||||
|
<TradingButton
|
||||||
|
className={buttonClasses}
|
||||||
|
data-testid={view}
|
||||||
|
onClick={onClick || (() => onSelect(view as BarView['type']))}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</TradingButton>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MobileDropdownItem = ({
|
||||||
|
view,
|
||||||
|
icon,
|
||||||
|
tooltip,
|
||||||
|
disabled = false,
|
||||||
|
onClick,
|
||||||
|
routeId,
|
||||||
|
}: {
|
||||||
|
view?: ViewType;
|
||||||
|
icon: VegaIconNames;
|
||||||
|
tooltip: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
routeId: string;
|
||||||
|
}) => {
|
||||||
|
const { setViews, getView } = useSidebar((store) => ({
|
||||||
|
setViews: store.setViews,
|
||||||
|
getView: store.getView,
|
||||||
|
}));
|
||||||
|
const currView = getView(routeId);
|
||||||
|
const onSelect = (view: BarView['type']) => {
|
||||||
|
if (view === currView?.type) {
|
||||||
|
setViews(null, routeId);
|
||||||
|
} else {
|
||||||
|
setViews({ type: view }, routeId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonClasses = classNames(
|
||||||
|
'flex items-center p-1 rounded',
|
||||||
|
'disabled:cursor-not-allowed disabled:text-vega-clight-500 dark:disabled:text-vega-cdark-500',
|
||||||
|
{
|
||||||
|
'text-vega-clight-200 dark:text-vega-cdark-200 enabled:hover:bg-vega-clight-500 dark:enabled:hover:bg-vega-cdark-500':
|
||||||
|
!view || view !== currView?.type,
|
||||||
|
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 text-black':
|
||||||
|
view && view === currView?.type,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip description={tooltip} align="center" side="right" sideOffset={10}>
|
||||||
|
<TradingDropdownItem
|
||||||
|
className={buttonClasses}
|
||||||
|
data-testid={view}
|
||||||
|
onClick={onClick || (() => onSelect(view as BarView['type']))}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<VegaIcon name={icon} size={20} />
|
||||||
|
{tooltip}
|
||||||
|
</TradingDropdownItem>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MobileBarActionsDropdown = ({
|
||||||
|
currentRouteId,
|
||||||
|
}: {
|
||||||
|
currentRouteId: string;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
return (
|
||||||
|
<MobileActionsDropdown>
|
||||||
|
<MobileDropdownItem
|
||||||
|
view={ViewType.Deposit}
|
||||||
|
icon={VegaIconNames.DEPOSIT}
|
||||||
|
tooltip={t('Deposit')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
<MobileDropdownItem
|
||||||
|
view={ViewType.Withdraw}
|
||||||
|
icon={VegaIconNames.WITHDRAW}
|
||||||
|
tooltip={t('Withdraw')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
<MobileDropdownItem
|
||||||
|
view={ViewType.Transfer}
|
||||||
|
icon={VegaIconNames.TRANSFER}
|
||||||
|
tooltip={t('Transfer')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
<MobileDropdownItem
|
||||||
|
view={ViewType.Info}
|
||||||
|
icon={VegaIconNames.BREAKDOWN}
|
||||||
|
tooltip={t('Market specification')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
<MobileDropdownItem
|
||||||
|
view={ViewType.Settings}
|
||||||
|
icon={VegaIconNames.COG}
|
||||||
|
tooltip={t('Settings')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
</MobileActionsDropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -7,21 +7,31 @@ import type {
|
|||||||
} from '@vegaprotocol/datagrid';
|
} from '@vegaprotocol/datagrid';
|
||||||
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
|
import { COL_DEFS, SetFilter } from '@vegaprotocol/datagrid';
|
||||||
import * as Schema from '@vegaprotocol/types';
|
import * as Schema from '@vegaprotocol/types';
|
||||||
import { addDecimalsFormatNumber, toBigNum } from '@vegaprotocol/utils';
|
import {
|
||||||
|
addDecimalsFormatNumber,
|
||||||
|
formatNumber,
|
||||||
|
toBigNum,
|
||||||
|
} from '@vegaprotocol/utils';
|
||||||
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
|
import { ButtonLink, Tooltip } from '@vegaprotocol/ui-toolkit';
|
||||||
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
|
||||||
import type {
|
import type {
|
||||||
|
MarketFieldsFragment,
|
||||||
MarketMaybeWithData,
|
MarketMaybeWithData,
|
||||||
MarketMaybeWithDataAndCandles,
|
MarketMaybeWithDataAndCandles,
|
||||||
} from '@vegaprotocol/markets';
|
} from '@vegaprotocol/markets';
|
||||||
import { MarketActionsDropdown } from './market-table-actions';
|
import { MarketActionsDropdown } from './market-table-actions';
|
||||||
import { calcCandleVolume, getAsset } from '@vegaprotocol/markets';
|
import {
|
||||||
|
calcCandleVolume,
|
||||||
|
calcCandleVolumePrice,
|
||||||
|
getAsset,
|
||||||
|
getQuoteName,
|
||||||
|
} from '@vegaprotocol/markets';
|
||||||
import { MarketCodeCell } from './market-code-cell';
|
import { MarketCodeCell } from './market-code-cell';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
|
|
||||||
const { MarketTradingMode, AuctionTrigger } = Schema;
|
const { MarketTradingMode, AuctionTrigger } = Schema;
|
||||||
|
|
||||||
export const useColumnDefs = () => {
|
export const useMarketsColumnDefs = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
|
||||||
return useMemo<ColDef[]>(
|
return useMemo<ColDef[]>(
|
||||||
@@ -158,11 +168,25 @@ export const useColumnDefs = () => {
|
|||||||
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
|
}: ValueFormatterParams<MarketMaybeWithDataAndCandles, 'candles'>) => {
|
||||||
const candles = data?.candles;
|
const candles = data?.candles;
|
||||||
const vol = candles ? calcCandleVolume(candles) : '0';
|
const vol = candles ? calcCandleVolume(candles) : '0';
|
||||||
|
const quoteName = getQuoteName(data as MarketFieldsFragment);
|
||||||
|
const volPrice =
|
||||||
|
candles &&
|
||||||
|
calcCandleVolumePrice(
|
||||||
|
candles,
|
||||||
|
data.decimalPlaces,
|
||||||
|
data.positionDecimalPlaces
|
||||||
|
);
|
||||||
|
|
||||||
const volume =
|
const volume =
|
||||||
data && vol && vol !== '0'
|
data && vol && vol !== '0'
|
||||||
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
|
? addDecimalsFormatNumber(vol, data.positionDecimalPlaces)
|
||||||
: '0.00';
|
: '0.00';
|
||||||
return volume;
|
const volumePrice =
|
||||||
|
volPrice && formatNumber(volPrice, data?.decimalPlaces);
|
||||||
|
|
||||||
|
return volumePrice
|
||||||
|
? `${volume} (${volumePrice} ${quoteName})`
|
||||||
|
: volume;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
|||||||
import { SidebarButton, ViewType } from '../../components/sidebar';
|
import { SidebarButton, ViewType } from '../../components/sidebar';
|
||||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { MobileButton } from '../markets/mobile-buttons';
|
||||||
|
|
||||||
export const PortfolioSidebar = () => {
|
export const PortfolioSidebar = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@@ -30,3 +31,28 @@ export const PortfolioSidebar = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const PortfolioMobileSidebar = () => {
|
||||||
|
const t = useT();
|
||||||
|
const currentRouteId = useGetCurrentRouteId();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-3 grow md:grow-0 md:flex lg:flex-col items-center gap-2 lg:gap-4 p-1">
|
||||||
|
<MobileButton
|
||||||
|
view={ViewType.Deposit}
|
||||||
|
tooltip={t('Deposit')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
<MobileButton
|
||||||
|
view={ViewType.Withdraw}
|
||||||
|
tooltip={t('Withdraw')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
<MobileButton
|
||||||
|
view={ViewType.Transfer}
|
||||||
|
tooltip={t('Transfer')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { LayoutPriority } from 'allotment';
|
import { LayoutPriority } from 'allotment';
|
||||||
import { titlefy } from '@vegaprotocol/utils';
|
|
||||||
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
|
import { useIncompleteWithdrawals } from '@vegaprotocol/withdraws';
|
||||||
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
|
import { Tab, LocalStoragePersistTabs as Tabs } from '@vegaprotocol/ui-toolkit';
|
||||||
import { usePageTitleStore } from '../../stores';
|
|
||||||
import {
|
import {
|
||||||
AccountsContainer,
|
AccountsContainer,
|
||||||
AccountsSettings,
|
AccountsSettings,
|
||||||
@@ -41,6 +39,7 @@ import { WithdrawalsMenu } from '../../components/withdrawals-menu';
|
|||||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
|
||||||
const WithdrawalsIndicator = () => {
|
const WithdrawalsIndicator = () => {
|
||||||
const { ready } = useIncompleteWithdrawals();
|
const { ready } = useIncompleteWithdrawals();
|
||||||
@@ -69,14 +68,7 @@ const SidebarViewInitializer = () => {
|
|||||||
|
|
||||||
export const Portfolio = () => {
|
export const Portfolio = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
|
usePageTitle(t('Portfolio'));
|
||||||
const { updateTitle } = usePageTitleStore((store) => ({
|
|
||||||
updateTitle: store.updateTitle,
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateTitle(titlefy([t('Portfolio')]));
|
|
||||||
}, [updateTitle, t]);
|
|
||||||
|
|
||||||
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
|
const [sizes, handleOnLayoutChange] = usePaneLayout({ id: 'portfolio' });
|
||||||
const wrapperClasses = 'p-0.5 h-full max-h-full flex flex-col';
|
const wrapperClasses = 'p-0.5 h-full max-h-full flex flex-col';
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ export const BORDER_COLOR = 'border-vega-clight-500 dark:border-vega-cdark-500';
|
|||||||
export const GRADIENT =
|
export const GRADIENT =
|
||||||
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
'bg-gradient-to-b from-vega-clight-800 dark:from-vega-cdark-800 to-transparent';
|
||||||
|
|
||||||
export const SKY_BACKGROUND =
|
|
||||||
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[37%_0px] bg-[length:1440px] bg-no-repeat bg-local';
|
|
||||||
|
|
||||||
// TODO: Update the links to use the correct referral related pages
|
// TODO: Update the links to use the correct referral related pages
|
||||||
export const REFERRAL_DOCS_LINK =
|
export const REFERRAL_DOCS_LINK =
|
||||||
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
|
'https://docs.vega.xyz/mainnet/concepts/trading-on-vega/discounts-rewards#referral-program';
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -11,6 +7,7 @@ import {
|
|||||||
ExternalLink,
|
ExternalLink,
|
||||||
InputError,
|
InputError,
|
||||||
Intent,
|
Intent,
|
||||||
|
Tooltip,
|
||||||
TradingAnchorButton,
|
TradingAnchorButton,
|
||||||
TradingButton,
|
TradingButton,
|
||||||
VegaIcon,
|
VegaIcon,
|
||||||
@@ -18,34 +15,28 @@ 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 { Link, Navigate, useNavigate } from 'react-router-dom';
|
||||||
import { Routes } from '../../lib/links';
|
import { Links, Routes } from '../../lib/links';
|
||||||
import { useReferralProgram } from './hooks/use-referral-program';
|
import { useReferralProgram } from './hooks/use-referral-program';
|
||||||
|
import { useReferralSetTransaction } from '../../lib/hooks/use-referral-set-transaction';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
|
|
||||||
export const CreateCodeContainer = () => {
|
export const CreateCodeContainer = () => {
|
||||||
const { pubKey } = useVegaWallet();
|
const t = useT();
|
||||||
|
const { pubKey, isReadOnly } = useVegaWallet();
|
||||||
const isInReferralSet = useIsInReferralSet(pubKey);
|
const isInReferralSet = useIsInReferralSet(pubKey);
|
||||||
|
const openWalletDialog = useVegaWalletDialogStore(
|
||||||
|
(store) => store.openVegaWalletDialog
|
||||||
|
);
|
||||||
|
|
||||||
// Navigate to the index page when already in the referral set.
|
// Navigate to the index page when already in the referral set.
|
||||||
if (isInReferralSet) {
|
if (isInReferralSet) {
|
||||||
return <Navigate to={Routes.REFERRALS} />;
|
return <Navigate to={Routes.REFERRALS} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <CreateCodeForm />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CreateCodeForm = () => {
|
|
||||||
const t = useT();
|
|
||||||
const [dialogOpen, setDialogOpen] = useState(false);
|
|
||||||
const openWalletDialog = useVegaWalletDialogStore(
|
|
||||||
(store) => store.openVegaWalletDialog
|
|
||||||
);
|
|
||||||
const { pubKey, isReadOnly } = useVegaWallet();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="referral-create-code-form"
|
data-testid="referral-create-code-form"
|
||||||
@@ -60,22 +51,82 @@ export const CreateCodeForm = () => {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="w-full flex flex-col">
|
<div className="w-full flex flex-col gap-4 items-stretch">
|
||||||
<RainbowButton
|
{pubKey ? (
|
||||||
variant="border"
|
<CreateCodeForm />
|
||||||
disabled={isReadOnly}
|
) : (
|
||||||
onClick={() => {
|
<RainbowButton
|
||||||
if (pubKey) {
|
variant="border"
|
||||||
setDialogOpen(true);
|
disabled={isReadOnly}
|
||||||
} else {
|
onClick={openWalletDialog}
|
||||||
openWalletDialog();
|
>
|
||||||
}
|
{t('Connect wallet')}
|
||||||
}}
|
</RainbowButton>
|
||||||
>
|
)}
|
||||||
{pubKey ? t('Create a referral code') : t('Connect wallet')}
|
|
||||||
</RainbowButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CreateCodeForm = () => {
|
||||||
|
const t = useT();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [dialogOpen, setDialogOpen] = useState(false);
|
||||||
|
const { isReadOnly } = useVegaWallet();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip
|
||||||
|
description={t(
|
||||||
|
'Create a simple referral code to enjoy the referrer commission outlined in the current referral program'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<RainbowButton
|
||||||
|
variant="border"
|
||||||
|
disabled={isReadOnly}
|
||||||
|
onClick={() => setDialogOpen(true)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{t('Create a referral code')}
|
||||||
|
</RainbowButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
description={
|
||||||
|
<Trans
|
||||||
|
i18nKey={
|
||||||
|
'Make your referral code a Team to compete in Competitions with your friends, appear in leaderboards on the <0>Competitions Homepage</0>, and earn rewards'
|
||||||
|
}
|
||||||
|
components={[
|
||||||
|
<Link
|
||||||
|
key="homepage-link"
|
||||||
|
to={Links.COMPETITIONS()}
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
|
Compeitionts Homepage
|
||||||
|
</Link>,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<RainbowButton
|
||||||
|
role="link"
|
||||||
|
variant="border"
|
||||||
|
disabled={isReadOnly}
|
||||||
|
onClick={() => navigate(Links.COMPETITIONS_CREATE_TEAM())}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{t('Create a team')}
|
||||||
|
</RainbowButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
<p className="text-xs">
|
||||||
|
<Link className="underline" to={Links.COMPETITIONS()}>
|
||||||
|
{t('Go to competitions')}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
<Dialog
|
<Dialog
|
||||||
title={t('Create a referral code')}
|
title={t('Create a referral code')}
|
||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
@@ -84,7 +135,7 @@ export const CreateCodeForm = () => {
|
|||||||
>
|
>
|
||||||
<CreateCodeDialog setDialogOpen={setDialogOpen} />
|
<CreateCodeDialog setDialogOpen={setDialogOpen} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,67 +146,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();
|
} = useReferralSetTransaction();
|
||||||
|
|
||||||
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 +235,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 +249,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 +269,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 +298,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,53 +1,10 @@
|
|||||||
import { isRouteErrorResponse, useNavigate, useRouteError } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { RainbowButton } from './buttons';
|
import { RainbowButton } from '../../components/rainbow-button';
|
||||||
|
import { LayoutWithSky } from '../../components/layouts-inner';
|
||||||
import { AnimatedDudeWithWire } from './graphics/dude';
|
import { AnimatedDudeWithWire } from './graphics/dude';
|
||||||
import { LayoutWithSky } from './layout';
|
|
||||||
import { Routes } from '../../lib/links';
|
import { Routes } from '../../lib/links';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
|
|
||||||
export const ErrorBoundary = () => {
|
|
||||||
const t = useT();
|
|
||||||
const error = useRouteError();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const title = isRouteErrorResponse(error)
|
|
||||||
? `${error.status} ${error.statusText}`
|
|
||||||
: t('Something went wrong');
|
|
||||||
|
|
||||||
const code = isRouteErrorResponse(error) ? error.status : 0;
|
|
||||||
|
|
||||||
const messages: Record<number, string> = {
|
|
||||||
0: t('An unknown error occurred.'),
|
|
||||||
404: t("The page you're looking for doesn't exists."),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LayoutWithSky className="pt-32">
|
|
||||||
<div
|
|
||||||
aria-hidden
|
|
||||||
className="absolute top-64 right-[220px] md:right-[340px] max-sm:hidden"
|
|
||||||
>
|
|
||||||
<AnimatedDudeWithWire className="animate-spin" />
|
|
||||||
</div>
|
|
||||||
<h1 className="text-6xl font-alpha calt mb-10">{title}</h1>
|
|
||||||
|
|
||||||
{Object.keys(messages).includes(code.toString()) ? (
|
|
||||||
<p className="text-lg mb-10">{messages[code]}</p>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<p className="text-lg mb-10">
|
|
||||||
<RainbowButton
|
|
||||||
onClick={() => navigate('..')}
|
|
||||||
variant="border"
|
|
||||||
className="text-xs"
|
|
||||||
>
|
|
||||||
{t('Go back and try again')}
|
|
||||||
</RainbowButton>
|
|
||||||
</p>
|
|
||||||
</LayoutWithSky>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const NotFound = () => {
|
export const NotFound = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getNumberFormat } from '@vegaprotocol/utils';
|
import { formatNumber } from '@vegaprotocol/utils';
|
||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
import omit from 'lodash/omit';
|
import omit from 'lodash/omit';
|
||||||
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
|
import { useReferralProgramQuery } from './__generated__/CurrentReferralProgram';
|
||||||
@@ -107,9 +107,7 @@ export const useReferralProgram = () => {
|
|||||||
discountFactor: Number(t.referralDiscountFactor),
|
discountFactor: Number(t.referralDiscountFactor),
|
||||||
discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%',
|
discount: BigNumber(t.referralDiscountFactor).times(100).toFixed(2) + '%',
|
||||||
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
|
minimumVolume: Number(t.minimumRunningNotionalTakerVolume),
|
||||||
volume: getNumberFormat(0).format(
|
volume: formatNumber(t.minimumRunningNotionalTakerVolume, 0),
|
||||||
Number(t.minimumRunningNotionalTakerVolume)
|
|
||||||
),
|
|
||||||
epochs: Number(t.minimumEpochs),
|
epochs: Number(t.minimumEpochs),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,40 +1,42 @@
|
|||||||
|
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
import minBy from 'lodash/minBy';
|
import minBy from 'lodash/minBy';
|
||||||
import { CodeTile, StatTile } from './tile';
|
import sortBy from 'lodash/sortBy';
|
||||||
|
import compact from 'lodash/compact';
|
||||||
|
import { Trans } from 'react-i18next';
|
||||||
|
import classNames from 'classnames';
|
||||||
import {
|
import {
|
||||||
VegaIcon,
|
VegaIcon,
|
||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
truncateMiddle,
|
truncateMiddle,
|
||||||
TextChildrenTooltip as Tooltip,
|
TextChildrenTooltip as Tooltip,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
import { useVegaWallet } from '@vegaprotocol/wallet';
|
import { useVegaWallet } from '@vegaprotocol/wallet';
|
||||||
|
import {
|
||||||
|
addDecimalsFormatNumber,
|
||||||
|
formatNumber,
|
||||||
|
getDateFormat,
|
||||||
|
getDateTimeFormat,
|
||||||
|
getUserLocale,
|
||||||
|
removePaginationWrapper,
|
||||||
|
} from '@vegaprotocol/utils';
|
||||||
|
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
|
||||||
|
import { useStakeAvailable } from '../../lib/hooks/use-stake-available';
|
||||||
|
import { useT, ns } from '../../lib/use-t';
|
||||||
|
import { useTeam } from '../../lib/hooks/use-team';
|
||||||
|
import { TeamAvatar } from '../../components/competitions/team-avatar';
|
||||||
|
import { TeamStats } from '../../components/competitions/team-stats';
|
||||||
|
import { Table } from '../../components/table';
|
||||||
import {
|
import {
|
||||||
DEFAULT_AGGREGATION_DAYS,
|
DEFAULT_AGGREGATION_DAYS,
|
||||||
useReferral,
|
useReferral,
|
||||||
useUpdateReferees,
|
useUpdateReferees,
|
||||||
} from './hooks/use-referral';
|
} from './hooks/use-referral';
|
||||||
import classNames from 'classnames';
|
|
||||||
import { Table } from '../../components/table';
|
|
||||||
import {
|
|
||||||
addDecimalsFormatNumber,
|
|
||||||
getDateFormat,
|
|
||||||
getDateTimeFormat,
|
|
||||||
getNumberFormat,
|
|
||||||
getUserLocale,
|
|
||||||
removePaginationWrapper,
|
|
||||||
} from '@vegaprotocol/utils';
|
|
||||||
import { useReferralSetStatsQuery } from './hooks/__generated__/ReferralSetStats';
|
|
||||||
import compact from 'lodash/compact';
|
|
||||||
import { useReferralProgram } from './hooks/use-referral-program';
|
|
||||||
import { useStakeAvailable } from './hooks/use-stake-available';
|
|
||||||
import sortBy from 'lodash/sortBy';
|
|
||||||
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
|
|
||||||
import BigNumber from 'bignumber.js';
|
|
||||||
import { useT, ns } from '../../lib/use-t';
|
|
||||||
import { Trans } from 'react-i18next';
|
|
||||||
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
|
import { ApplyCodeForm, ApplyCodeFormContainer } from './apply-code-form';
|
||||||
|
import { useReferralProgram } from './hooks/use-referral-program';
|
||||||
|
import { useCurrentEpochInfoQuery } from './hooks/__generated__/Epoch';
|
||||||
import { QUSDTooltip } from './qusd-tooltip';
|
import { QUSDTooltip } from './qusd-tooltip';
|
||||||
|
import { CodeTile, StatTile, Tile } from './tile';
|
||||||
|
|
||||||
export const ReferralStatistics = () => {
|
export const ReferralStatistics = () => {
|
||||||
const { pubKey } = useVegaWallet();
|
const { pubKey } = useVegaWallet();
|
||||||
@@ -192,10 +194,7 @@ export const Statistics = ({
|
|||||||
nextBenefitTierEpochsValue,
|
nextBenefitTierEpochsValue,
|
||||||
} = useStats({ data, program });
|
} = useStats({ data, program });
|
||||||
|
|
||||||
const isApplyCodePreview = useMemo(
|
const isApplyCodePreview = data.referee === null;
|
||||||
() => data.referee === null,
|
|
||||||
[data.referee]
|
|
||||||
);
|
|
||||||
|
|
||||||
const { benefitTiers } = useReferralProgram();
|
const { benefitTiers } = useReferralProgram();
|
||||||
|
|
||||||
@@ -324,27 +323,10 @@ export const Statistics = ({
|
|||||||
}
|
}
|
||||||
description={<QUSDTooltip />}
|
description={<QUSDTooltip />}
|
||||||
>
|
>
|
||||||
{getNumberFormat(0).format(Number(totalCommissionValue))}
|
{formatNumber(totalCommissionValue, 0)}
|
||||||
</StatTile>
|
</StatTile>
|
||||||
);
|
);
|
||||||
|
|
||||||
const referrerTiles = (
|
|
||||||
<>
|
|
||||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
|
||||||
{baseCommissionTile}
|
|
||||||
{stakingMultiplierTile}
|
|
||||||
{finalCommissionTile}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
|
||||||
{codeTile}
|
|
||||||
{referrerVolumeTile}
|
|
||||||
{numberOfTradersTile}
|
|
||||||
{totalCommissionTile}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const currentBenefitTierTile = (
|
const currentBenefitTierTile = (
|
||||||
<StatTile
|
<StatTile
|
||||||
title={t('Current tier')}
|
title={t('Current tier')}
|
||||||
@@ -416,8 +398,41 @@ export const Statistics = ({
|
|||||||
</StatTile>
|
</StatTile>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const eligibilityWarningOverlay = as === 'referee' && !isEligible && (
|
||||||
|
<div
|
||||||
|
data-testid="referral-eligibility-warning"
|
||||||
|
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
|
||||||
|
>
|
||||||
|
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
|
||||||
|
<p>
|
||||||
|
{t(
|
||||||
|
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const referrerTiles = (
|
||||||
|
<>
|
||||||
|
<Team teamId={data.code} />
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||||
|
{baseCommissionTile}
|
||||||
|
{stakingMultiplierTile}
|
||||||
|
{finalCommissionTile}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 sm:grid-cols-2 xl:grid-cols-4">
|
||||||
|
{codeTile}
|
||||||
|
{referrerVolumeTile}
|
||||||
|
{numberOfTradersTile}
|
||||||
|
{totalCommissionTile}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const refereeTiles = (
|
const refereeTiles = (
|
||||||
<>
|
<>
|
||||||
|
<Team teamId={data.code} />
|
||||||
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
<div className="grid grid-rows-1 gap-5 grid-cols-1 md:grid-cols-3">
|
||||||
{currentBenefitTierTile}
|
{currentBenefitTierTile}
|
||||||
{runningVolumeTile}
|
{runningVolumeTile}
|
||||||
@@ -432,20 +447,6 @@ export const Statistics = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const eligibilityWarning = as === 'referee' && !isEligible && (
|
|
||||||
<div
|
|
||||||
data-testid="referral-eligibility-warning"
|
|
||||||
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-center w-1/2 lg:w-1/3"
|
|
||||||
>
|
|
||||||
<h2 className="text-2xl mb-2">{t('Referral code no longer valid')}</h2>
|
|
||||||
<p>
|
|
||||||
{t(
|
|
||||||
'Your referral code is no longer valid as the referrer no longer meets the minimum requirements. Apply a new code to continue receiving discounts.'
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="referral-statistics"
|
data-testid="referral-statistics"
|
||||||
@@ -460,8 +461,7 @@ export const Statistics = ({
|
|||||||
{as === 'referrer' && referrerTiles}
|
{as === 'referrer' && referrerTiles}
|
||||||
{as === 'referee' && refereeTiles}
|
{as === 'referee' && refereeTiles}
|
||||||
</div>
|
</div>
|
||||||
|
{eligibilityWarningOverlay}
|
||||||
{eligibilityWarning}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -563,8 +563,8 @@ export const RefereesTable = ({
|
|||||||
)
|
)
|
||||||
.map((r) => ({
|
.map((r) => ({
|
||||||
...r,
|
...r,
|
||||||
volume: getNumberFormat(0).format(r.volume),
|
volume: formatNumber(r.volume, 0),
|
||||||
commission: getNumberFormat(0).format(r.commission),
|
commission: formatNumber(r.commission, 0),
|
||||||
}))
|
}))
|
||||||
.reverse()}
|
.reverse()}
|
||||||
/>
|
/>
|
||||||
@@ -574,3 +574,19 @@ export const RefereesTable = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Team = ({ teamId }: { teamId?: string }) => {
|
||||||
|
const { team, games, members } = useTeam(teamId);
|
||||||
|
|
||||||
|
if (!team) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tile className="flex gap-3 lg:gap-4">
|
||||||
|
<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>
|
||||||
|
<TeamStats members={members} games={games} />
|
||||||
|
</div>
|
||||||
|
</Tile>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -13,11 +13,9 @@ import { useVegaWallet } from '@vegaprotocol/wallet';
|
|||||||
import { useReferral } from './hooks/use-referral';
|
import { useReferral } from './hooks/use-referral';
|
||||||
import { REFERRAL_DOCS_LINK } from './constants';
|
import { REFERRAL_DOCS_LINK } from './constants';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { usePageTitleStore } from '../../stores';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { titlefy } from '@vegaprotocol/utils';
|
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
|
||||||
const Nav = () => {
|
const Nav = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
@@ -57,13 +55,7 @@ export const Referrals = () => {
|
|||||||
const loading = refereeLoading || referrerLoading;
|
const loading = refereeLoading || referrerLoading;
|
||||||
const showNav = !loading && !error && !referrer && !referee;
|
const showNav = !loading && !error && !referrer && !referee;
|
||||||
|
|
||||||
const { updateTitle } = usePageTitleStore((store) => ({
|
usePageTitle(t('Referrals'));
|
||||||
updateTitle: store.updateTitle,
|
|
||||||
}));
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
updateTitle(titlefy([t('Referrals')]));
|
|
||||||
}, [updateTitle, t]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary feature="referrals">
|
<ErrorBoundary feature="referrals">
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
import { useEffect } from 'react';
|
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
|
||||||
import { titlefy } from '@vegaprotocol/utils';
|
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { RewardsContainer } from '../../components/rewards-container';
|
import { RewardsContainer } from '../../components/rewards-container';
|
||||||
import { usePageTitleStore } from '../../stores';
|
|
||||||
import { ErrorBoundary } from '../../components/error-boundary';
|
import { ErrorBoundary } from '../../components/error-boundary';
|
||||||
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
|
import { usePageTitle } from '../../lib/hooks/use-page-title';
|
||||||
|
|
||||||
export const Rewards = () => {
|
export const Rewards = () => {
|
||||||
const t = useT();
|
const t = useT();
|
||||||
const title = t('Rewards');
|
const title = t('Rewards');
|
||||||
const { updateTitle } = usePageTitleStore((store) => ({
|
usePageTitle(title);
|
||||||
updateTitle: store.updateTitle,
|
|
||||||
}));
|
|
||||||
useEffect(() => {
|
|
||||||
updateTitle(titlefy([title]));
|
|
||||||
}, [updateTitle, title]);
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary feature="rewards">
|
<ErrorBoundary feature="rewards">
|
||||||
<TinyScroll className="p-4 max-h-full overflow-auto">
|
<TinyScroll className="p-4 max-h-full overflow-auto">
|
||||||
<h1 className="px-4 pb-4 text-2xl">{title}</h1>
|
<h1 className="md:px-4 pb-4 text-2xl">{title}</h1>
|
||||||
<RewardsContainer />
|
<RewardsContainer />
|
||||||
</TinyScroll>
|
</TinyScroll>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export { Teams } from './teams';
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export const Teams = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1>Teams</h1>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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 = ({
|
||||||
|
children,
|
||||||
|
backgroundImage,
|
||||||
|
...props
|
||||||
|
}: HTMLAttributes<HTMLDivElement> & { backgroundImage?: string }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
className={classNames(
|
||||||
|
BORDER_COLOR,
|
||||||
|
GRADIENT,
|
||||||
|
'border rounded-lg',
|
||||||
|
'relative p-6 overflow-hidden',
|
||||||
|
props.className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{Boolean(backgroundImage?.length) && (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'pointer-events-none',
|
||||||
|
'bg-no-repeat bg-center bg-[length:500px_500px]',
|
||||||
|
'absolute top-0 left-0 w-full h-full -z-10 opacity-30 blur-lg'
|
||||||
|
)}
|
||||||
|
style={{ backgroundImage: `url("${backgroundImage}")` }}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Box } from './box';
|
||||||
|
import { type ComponentProps, type ReactElement, type ReactNode } from 'react';
|
||||||
|
import { DudeBadge } from './graphics/dude-badge';
|
||||||
|
|
||||||
|
export const CompetitionsActionsContainer = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children:
|
||||||
|
| ReactElement<typeof CompetitionsAction>
|
||||||
|
| Iterable<ReactElement<typeof CompetitionsAction>>;
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
className="grid grid-cols-1 md:grid-cols-3 grid-rows-4'
|
||||||
|
gap-6 mb-12"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const CompetitionsAction = ({
|
||||||
|
variant,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
actionElement,
|
||||||
|
}: {
|
||||||
|
variant: ComponentProps<typeof DudeBadge>['variant'];
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
actionElement: ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Box className="grid md:grid-rows-[subgrid] gap-6 row-span-4 text-center">
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<DudeBadge variant={variant} />
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl">{title}</h2>
|
||||||
|
{description && <p className="text-muted">{description}</p>}
|
||||||
|
<div className="flex justify-center">{actionElement}</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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="relative mb-4 lg:mb-20">
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="absolute top-20 right-[220px] md:right-[240px] max-sm:hidden"
|
||||||
|
>
|
||||||
|
<AnimatedDudeWithWire />
|
||||||
|
</div>
|
||||||
|
<div className="pt-6 lg:pt-20 sm:w-1/2">
|
||||||
|
<h1 className="text-3xl lg:text-6xl leading-[1em] font-alpha calt mb-2 lg:mb-10">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Splash } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { formatNumber } from '@vegaprotocol/utils';
|
||||||
|
import { type useTeams } from '../../lib/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 ? '-' : formatNumber(n, 0));
|
||||||
|
|
||||||
|
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: 'status', displayName: t('Status') },
|
||||||
|
{ name: 'volume', displayName: t('Volume') },
|
||||||
|
]}
|
||||||
|
data={data.map((td) => {
|
||||||
|
// leaderboard place or medal
|
||||||
|
let rank: number | React.ReactNode = td.rank;
|
||||||
|
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),
|
||||||
|
status: td.closed ? t('Closed') : t('Open'),
|
||||||
|
volume: num(td.totalQuantumVolume),
|
||||||
|
};
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { type TransferNode } from '@vegaprotocol/types';
|
||||||
|
import {
|
||||||
|
ActiveRewardCard,
|
||||||
|
isActiveReward,
|
||||||
|
} from '../rewards-container/active-rewards';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { useAssetsMapProvider } from '@vegaprotocol/assets';
|
||||||
|
import { useMarketsMapProvider } from '@vegaprotocol/markets';
|
||||||
|
|
||||||
|
export const GamesContainer = ({
|
||||||
|
data,
|
||||||
|
currentEpoch,
|
||||||
|
}: {
|
||||||
|
data: TransferNode[];
|
||||||
|
currentEpoch: number;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
// Re-load markets and assets in the games container to ensure that the
|
||||||
|
// the cards are updated (not grayed out) when the user navigates to the games page
|
||||||
|
const { data: assets } = useAssetsMapProvider();
|
||||||
|
const { data: markets } = useMarketsMapProvider();
|
||||||
|
|
||||||
|
const enrichedTransfers = data
|
||||||
|
.filter((node) => isActiveReward(node, currentEpoch))
|
||||||
|
.map((node) => {
|
||||||
|
if (node.transfer.kind.__typename !== 'RecurringTransfer') {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
const asset =
|
||||||
|
assets &&
|
||||||
|
assets[
|
||||||
|
node.transfer.kind.dispatchStrategy?.dispatchMetricAssetId || ''
|
||||||
|
];
|
||||||
|
|
||||||
|
const marketsInScope =
|
||||||
|
node.transfer.kind.dispatchStrategy?.marketIdsInScope?.map(
|
||||||
|
(id) => markets && markets[id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...node, asset, markets: marketsInScope };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!enrichedTransfers || !enrichedTransfers.length) return null;
|
||||||
|
|
||||||
|
if (!enrichedTransfers || enrichedTransfers.length === 0) {
|
||||||
|
return (
|
||||||
|
<p className="mb-6 text-muted">
|
||||||
|
{t('There are currently no games available.')}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-12 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{enrichedTransfers.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,63 @@
|
|||||||
|
import { isValidUrl } from '@vegaprotocol/utils';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const NUM_AVATARS = 20;
|
||||||
|
const AVATAR_PATHNAME_PATTERN = '/team-avatars/{id}.png';
|
||||||
|
|
||||||
|
export 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);
|
||||||
|
};
|
||||||
|
|
||||||
|
const useAvatar = (teamId: string, url: string) => {
|
||||||
|
const fallback = getFallbackAvatar(teamId);
|
||||||
|
const [avatar, setAvatar] = useState<string>(fallback);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isValidUrl(url)) return;
|
||||||
|
fetch(url, { cache: 'force-cache' })
|
||||||
|
.then((response) => {
|
||||||
|
if (response.ok) {
|
||||||
|
setAvatar(url);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
/** noop */
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return avatar;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TeamAvatar = ({
|
||||||
|
teamId,
|
||||||
|
imgUrl,
|
||||||
|
alt,
|
||||||
|
size = 'large',
|
||||||
|
}: {
|
||||||
|
teamId: string;
|
||||||
|
imgUrl: string;
|
||||||
|
alt?: string;
|
||||||
|
size?: 'large' | 'small';
|
||||||
|
}) => {
|
||||||
|
const img = useAvatar(teamId, imgUrl);
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
import { type TeamGame, type TeamStats } from '../../lib/hooks/use-team';
|
||||||
|
import { type TeamsFieldsFragment } from '../../lib/hooks/__generated__/Teams';
|
||||||
|
import { TeamAvatar, getFallbackAvatar } from './team-avatar';
|
||||||
|
import { FavoriteGame, Stat } from './team-stats';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { formatNumberRounded } from '@vegaprotocol/utils';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import { Box } from './box';
|
||||||
|
import { Intent, Tooltip, TradingAnchorButton } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { Links } from '../../lib/links';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
import { take } from 'lodash';
|
||||||
|
import { DispatchMetricLabels } from '@vegaprotocol/types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { UpdateTeamButton } from '../../client-pages/competitions/update-team-button';
|
||||||
|
|
||||||
|
export const TeamCard = ({
|
||||||
|
rank,
|
||||||
|
team,
|
||||||
|
stats,
|
||||||
|
games,
|
||||||
|
}: {
|
||||||
|
rank: number;
|
||||||
|
team: TeamsFieldsFragment;
|
||||||
|
stats?: TeamStats;
|
||||||
|
games?: TeamGame[];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
const lastGames = take(
|
||||||
|
orderBy(
|
||||||
|
games?.map((g) => ({
|
||||||
|
rank: g.team.rank,
|
||||||
|
metric: g.team.rewardMetric,
|
||||||
|
epoch: g.epoch,
|
||||||
|
})),
|
||||||
|
(i) => i.epoch,
|
||||||
|
'desc'
|
||||||
|
),
|
||||||
|
5
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'gap-6 grid grid-cols-1 grid-rows-1',
|
||||||
|
'md:grid-cols-3'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{/** Card */}
|
||||||
|
<Box
|
||||||
|
backgroundImage={team.avatarUrl || getFallbackAvatar(team.teamId)}
|
||||||
|
className="flex flex-col items-center gap-3 min-w-[80px] lg:min-w-[112px]"
|
||||||
|
>
|
||||||
|
<TeamAvatar teamId={team.teamId} imgUrl={team.avatarUrl} />
|
||||||
|
<h1 className="calt lg:text-2xl" data-testid="team-name">
|
||||||
|
{team.name}
|
||||||
|
</h1>
|
||||||
|
{games && <FavoriteGame games={games} noLabel />}
|
||||||
|
<TradingAnchorButton
|
||||||
|
size="extra-small"
|
||||||
|
intent={Intent.Primary}
|
||||||
|
href={Links.COMPETITIONS_TEAM(team.teamId)}
|
||||||
|
>
|
||||||
|
{t('Profile')}
|
||||||
|
</TradingAnchorButton>
|
||||||
|
<UpdateTeamButton team={team} size="extra-small" />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/** Tiles */}
|
||||||
|
<Box className="w-full md:col-span-2">
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
'grid gap-3 w-full mb-4',
|
||||||
|
'md:grid-cols-3 md:grid-rows-2',
|
||||||
|
'grid-cols-2 grid-rows-3'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Stat
|
||||||
|
className="flex flex-col-reverse"
|
||||||
|
value={rank}
|
||||||
|
label={t('Rank')}
|
||||||
|
valueTestId="team-rank"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
className="flex flex-col-reverse"
|
||||||
|
value={team.totalMembers || 0}
|
||||||
|
label={t('Members')}
|
||||||
|
valueTestId="members-count-stat"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
className="flex flex-col-reverse"
|
||||||
|
value={stats?.totalGamesPlayed || 0}
|
||||||
|
label={t('Total games')}
|
||||||
|
valueTestId="total-games-stat"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
className="flex flex-col-reverse"
|
||||||
|
value={
|
||||||
|
stats?.totalQuantumVolume
|
||||||
|
? formatNumberRounded(
|
||||||
|
new BigNumber(stats.totalQuantumVolume || 0),
|
||||||
|
'1e3'
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
label={t('Total volume')}
|
||||||
|
valueTestId="total-volume-stat"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
className="flex flex-col-reverse"
|
||||||
|
value={
|
||||||
|
stats?.totalQuantumRewards
|
||||||
|
? formatNumberRounded(
|
||||||
|
new BigNumber(stats.totalQuantumRewards || 0),
|
||||||
|
'1e3'
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
label={t('Rewards paid out')}
|
||||||
|
valueTestId="rewards-paid-stat"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl className="w-full pt-4 border-t border-vega-clight-700 dark:border-vega-cdark-700">
|
||||||
|
<dt className="mb-1 text-sm text-muted">
|
||||||
|
{t('Last {{games}} games result', {
|
||||||
|
replace: { games: lastGames.length || '' },
|
||||||
|
})}
|
||||||
|
</dt>
|
||||||
|
<dd className="flex flex-row flex-wrap gap-2">
|
||||||
|
{lastGames.length === 0 && t('None available')}
|
||||||
|
{lastGames.map((game, i) => (
|
||||||
|
<Tooltip key={i} description={DispatchMetricLabels[game.metric]}>
|
||||||
|
<button className="cursor-help text-sm bg-vega-clight-700 dark:bg-vega-cdark-700 px-2 py-1 rounded-full">
|
||||||
|
<RankLabel rank={game.rank} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the english ordinal for given rank only if the current language is set
|
||||||
|
* to english.
|
||||||
|
*/
|
||||||
|
const RankLabel = ({ rank }: { rank: number }) => {
|
||||||
|
const t = useT();
|
||||||
|
return t('place', { count: rank, ordinal: true });
|
||||||
|
};
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import { type ReactNode } from 'react';
|
||||||
|
import BigNumber from 'bignumber.js';
|
||||||
|
import countBy from 'lodash/countBy';
|
||||||
|
import {
|
||||||
|
Pill,
|
||||||
|
VegaIcon,
|
||||||
|
VegaIconNames,
|
||||||
|
Tooltip,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { formatNumberRounded } from '@vegaprotocol/utils';
|
||||||
|
import {
|
||||||
|
type TeamStats as ITeamStats,
|
||||||
|
type Member,
|
||||||
|
type TeamGame,
|
||||||
|
} from '../../lib/hooks/use-team';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import { DispatchMetricLabels, type DispatchMetric } from '@vegaprotocol/types';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
|
export const TeamStats = ({
|
||||||
|
stats,
|
||||||
|
members,
|
||||||
|
games,
|
||||||
|
}: {
|
||||||
|
stats?: ITeamStats;
|
||||||
|
members?: Member[];
|
||||||
|
games?: TeamGame[];
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StatSection>
|
||||||
|
<StatList>
|
||||||
|
<Stat
|
||||||
|
value={members ? members.length : 0}
|
||||||
|
label={t('Members')}
|
||||||
|
valueTestId="members-count-stat"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
value={stats ? stats.totalGamesPlayed : 0}
|
||||||
|
label={t('Total games')}
|
||||||
|
tooltip={t('Total number of games this team has participated in')}
|
||||||
|
valueTestId="total-games-stat"
|
||||||
|
/>
|
||||||
|
<StatSectionSeparator />
|
||||||
|
<Stat
|
||||||
|
value={
|
||||||
|
stats
|
||||||
|
? formatNumberRounded(
|
||||||
|
new BigNumber(stats.totalQuantumVolume || 0),
|
||||||
|
'1e3'
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
label={t('Total volume')}
|
||||||
|
valueTestId="total-volume-stat"
|
||||||
|
/>
|
||||||
|
<Stat
|
||||||
|
value={
|
||||||
|
stats
|
||||||
|
? formatNumberRounded(
|
||||||
|
new BigNumber(stats.totalQuantumRewards || 0),
|
||||||
|
'1e3'
|
||||||
|
)
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
label={t('Rewards paid out')}
|
||||||
|
tooltip={'Total amount of rewards paid out to this team in qUSD'}
|
||||||
|
valueTestId="rewards-paid-stat"
|
||||||
|
/>
|
||||||
|
</StatList>
|
||||||
|
</StatSection>
|
||||||
|
{games && games.length ? (
|
||||||
|
<StatSection>
|
||||||
|
<FavoriteGame games={games} />
|
||||||
|
<StatSectionSeparator />
|
||||||
|
<LatestResults games={games} />
|
||||||
|
</StatSection>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const LatestResults = ({ games }: { games: TeamGame[] }) => {
|
||||||
|
const t = useT();
|
||||||
|
const latestGames = games.slice(0, 5);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dl className="flex flex-col gap-1">
|
||||||
|
<dt className="text-muted text-sm">
|
||||||
|
{t('gameCount', { 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FavoriteGame = ({
|
||||||
|
games,
|
||||||
|
noLabel = false,
|
||||||
|
}: {
|
||||||
|
games: TeamGame[];
|
||||||
|
noLabel?: boolean;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
const rewardMetrics = games.map(
|
||||||
|
(game) => game.team.rewardMetric as DispatchMetric
|
||||||
|
);
|
||||||
|
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;
|
||||||
|
|
||||||
|
// rewardMetric is a string, should be typed as DispatchMetric
|
||||||
|
const favoriteMetricLabel =
|
||||||
|
DispatchMetricLabels[favoriteMetric as DispatchMetric];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dl className="flex flex-col gap-1">
|
||||||
|
<dt
|
||||||
|
className={classNames('text-muted text-sm', {
|
||||||
|
hidden: noLabel,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{t('Favorite game')}
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
<Pill className="inline-flex items-center gap-1 bg-transparent text-sm">
|
||||||
|
<VegaIcon
|
||||||
|
name={VegaIconNames.STAR}
|
||||||
|
className="text-vega-yellow-400 relative top-[-1px]"
|
||||||
|
/>{' '}
|
||||||
|
{favoriteMetricLabel}
|
||||||
|
</Pill>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StatSection = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<section className="flex flex-col lg:flex-row gap-4 lg:gap-8">
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StatSectionSeparator = () => {
|
||||||
|
return <div className="hidden md:block border-r border-default" />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const StatList = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<dl className="grid grid-cols-2 md:flex gap-4 md:gap-6 lg:gap-8 whitespace-nowrap">
|
||||||
|
{children}
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Stat = ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
tooltip,
|
||||||
|
valueTestId,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
value: ReactNode;
|
||||||
|
label: ReactNode;
|
||||||
|
tooltip?: string;
|
||||||
|
valueTestId?: string;
|
||||||
|
className?: classNames.Argument;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className={classNames(className)}>
|
||||||
|
<dd className="text-3xl lg:text-4xl" data-testid={valueTestId}>
|
||||||
|
{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,2 @@
|
|||||||
|
export { LayoutWithSky } from './layout-with-sky';
|
||||||
|
export { LayoutWithGradient } from './layout-with-gradient';
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { type ReactNode } from 'react';
|
||||||
|
|
||||||
|
export const LayoutWithGradient = ({ children }: { children: ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<div className="relative h-full pt-5 overflow-y-auto">
|
||||||
|
<div className="absolute top-0 left-0 w-full h-[40%] -z-10 bg-[40%_0px] bg-cover bg-no-repeat bg-local bg-[url(/cover.png)]">
|
||||||
|
<div className="absolute top-o left-0 w-full h-full bg-gradient-to-t from-white dark:from-vega-cdark-900 to-transparent from-20% to-60%" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-4 lg:gap-6 container p-4 mx-auto">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+4
-2
@@ -1,10 +1,12 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes } from 'react';
|
||||||
import { SKY_BACKGROUND } from './constants';
|
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Outlet } from 'react-router-dom';
|
||||||
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
|
import { TinyScroll } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
export const Layout = ({
|
export const SKY_BACKGROUND =
|
||||||
|
'bg-[url(/sky-light.png)] dark:bg-[url(/sky-dark.png)] bg-[37%_0px] bg-[length:1440px] bg-no-repeat bg-local';
|
||||||
|
|
||||||
|
const Layout = ({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
@@ -1 +1,2 @@
|
|||||||
export * from './layout-with-sidebar';
|
export { LayoutWithSidebar } from './layout-with-sidebar';
|
||||||
|
export { LayoutCentered } from './layout-centered';
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Outlet } from 'react-router-dom';
|
|||||||
import { Sidebar, SidebarContent, useSidebar } from '../sidebar';
|
import { Sidebar, SidebarContent, useSidebar } from '../sidebar';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||||
|
|
||||||
export const LayoutWithSidebar = ({
|
export const LayoutWithSidebar = ({
|
||||||
header,
|
header,
|
||||||
sidebar,
|
sidebar,
|
||||||
@@ -17,7 +16,7 @@ export const LayoutWithSidebar = ({
|
|||||||
const sidebarOpen = sidebarView !== null;
|
const sidebarOpen = sidebarView !== null;
|
||||||
const gridClasses = classNames(
|
const gridClasses = classNames(
|
||||||
'h-full relative z-0 grid',
|
'h-full relative z-0 grid',
|
||||||
'grid-rows-[min-content_1fr_40px]',
|
'grid-rows-[min-content_1fr_50px]',
|
||||||
'lg:grid-rows-[min-content_1fr]',
|
'lg:grid-rows-[min-content_1fr]',
|
||||||
'lg:grid-cols-[1fr_280px_40px]',
|
'lg:grid-cols-[1fr_280px_40px]',
|
||||||
'xxxl:grid-cols-[1fr_320px_40px]'
|
'xxxl:grid-cols-[1fr_320px_40px]'
|
||||||
@@ -27,10 +26,13 @@ export const LayoutWithSidebar = ({
|
|||||||
<div className={gridClasses}>
|
<div className={gridClasses}>
|
||||||
<div className="col-span-full">{header}</div>
|
<div className="col-span-full">{header}</div>
|
||||||
<main
|
<main
|
||||||
className={classNames('col-start-1 col-end-1 overflow-y-auto', {
|
className={classNames(
|
||||||
'lg:col-end-3': !sidebarOpen,
|
'col-start-1 col-end-1 overflow-hidden lg:overflow-y-auto grow lg:grow-0',
|
||||||
'hidden lg:block lg:col-end-2': sidebarOpen,
|
{
|
||||||
})}
|
'lg:col-end-3': !sidebarOpen,
|
||||||
|
'hidden lg:block lg:col-end-2': sidebarOpen,
|
||||||
|
}
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './market-header';
|
export * from './market-header';
|
||||||
|
export * from './mobile-market-header';
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { MarketSelector } from '../market-selector';
|
||||||
|
import {
|
||||||
|
Last24hPriceChange,
|
||||||
|
useMarket,
|
||||||
|
useMarketList,
|
||||||
|
} from '@vegaprotocol/markets';
|
||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useT } from '../../lib/use-t';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { MarketHeaderStats } from '../../client-pages/market/market-header-stats';
|
||||||
|
import { MarketMarkPrice } from '../market-mark-price';
|
||||||
|
/**
|
||||||
|
* This is only rendered for the mobile navigation
|
||||||
|
*/
|
||||||
|
export const MobileMarketHeader = () => {
|
||||||
|
const t = useT();
|
||||||
|
const { marketId } = useParams();
|
||||||
|
const { data } = useMarket(marketId);
|
||||||
|
const [openMarket, setOpenMarket] = useState(false);
|
||||||
|
const [openPrice, setOpenPrice] = useState(false);
|
||||||
|
|
||||||
|
// Ensure that markets are kept cached so opening the list
|
||||||
|
// shows all markets instantly
|
||||||
|
useMarketList();
|
||||||
|
|
||||||
|
if (!marketId) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pl-3 pr-2 flex justify-between gap-2 h-10 bg-vega-clight-700 dark:bg-vega-cdark-700">
|
||||||
|
<FullScreenPopover
|
||||||
|
open={openMarket}
|
||||||
|
onOpenChange={(x) => {
|
||||||
|
setOpenMarket(x);
|
||||||
|
}}
|
||||||
|
trigger={
|
||||||
|
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-base leading-3 md:text-lg whitespace-nowrap">
|
||||||
|
{data
|
||||||
|
? data.tradableInstrument.instrument.code
|
||||||
|
: t('Select market')}
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'transition-transform ease-in-out duration-300 flex',
|
||||||
|
{
|
||||||
|
'rotate-180': openMarket,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={16} />
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<MarketSelector
|
||||||
|
currentMarketId={marketId}
|
||||||
|
onSelect={() => setOpenMarket(false)}
|
||||||
|
/>
|
||||||
|
</FullScreenPopover>
|
||||||
|
<FullScreenPopover
|
||||||
|
open={openPrice}
|
||||||
|
onOpenChange={(x) => {
|
||||||
|
setOpenPrice(x);
|
||||||
|
}}
|
||||||
|
trigger={
|
||||||
|
<span className="flex gap-2 items-end md:text-md whitespace-nowrap leading-3">
|
||||||
|
{data && (
|
||||||
|
<>
|
||||||
|
<span className="text-xs">
|
||||||
|
<Last24hPriceChange
|
||||||
|
marketId={data.id}
|
||||||
|
decimalPlaces={data.decimalPlaces}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<MarketMarkPrice
|
||||||
|
marketId={data.id}
|
||||||
|
decimalPlaces={data.decimalPlaces}
|
||||||
|
/>
|
||||||
|
<VegaIcon
|
||||||
|
name={VegaIconNames.CHEVRON_DOWN}
|
||||||
|
size={16}
|
||||||
|
className={classNames(
|
||||||
|
'transition-transform ease-in-out duration-300',
|
||||||
|
{
|
||||||
|
'rotate-180': openPrice,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data && (
|
||||||
|
<div className="px-3 py-6 text-sm grid grid-cols-2 items-center gap-x-4 gap-y-6">
|
||||||
|
<MarketHeaderStats market={data} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FullScreenPopover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PopoverProps extends PopoverPrimitive.PopoverProps {
|
||||||
|
trigger: React.ReactNode | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FullScreenPopover = ({
|
||||||
|
trigger,
|
||||||
|
children,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: PopoverProps) => {
|
||||||
|
return (
|
||||||
|
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
||||||
|
<PopoverPrimitive.Trigger data-testid="popover-trigger">
|
||||||
|
{trigger}
|
||||||
|
</PopoverPrimitive.Trigger>
|
||||||
|
<PopoverPrimitive.Portal>
|
||||||
|
<PopoverPrimitive.Content
|
||||||
|
data-testid="popover-content"
|
||||||
|
className="w-screen bg-vega-clight-800 dark:bg-vega-cdark-800 border-y border-default"
|
||||||
|
sideOffset={0}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</PopoverPrimitive.Content>
|
||||||
|
</PopoverPrimitive.Portal>
|
||||||
|
</PopoverPrimitive.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,2 +1 @@
|
|||||||
export * from './navbar';
|
export * from './navbar';
|
||||||
export * from './nav-header';
|
|
||||||
|
|||||||
@@ -1,81 +0,0 @@
|
|||||||
import { VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
|
|
||||||
import { MarketSelector } from '../market-selector';
|
|
||||||
import { useMarket, useMarketList } from '@vegaprotocol/markets';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useT } from '../../lib/use-t';
|
|
||||||
import classNames from 'classnames';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is only rendered for the mobile navigation
|
|
||||||
*/
|
|
||||||
export const NavHeader = () => {
|
|
||||||
const t = useT();
|
|
||||||
const { marketId } = useParams();
|
|
||||||
const { data } = useMarket(marketId);
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
// Ensure that markets are kept cached so opening the list
|
|
||||||
// shows all markets instantly
|
|
||||||
useMarketList();
|
|
||||||
|
|
||||||
if (!marketId) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FullScreenPopover
|
|
||||||
open={open}
|
|
||||||
onOpenChange={(x) => {
|
|
||||||
setOpen(x);
|
|
||||||
}}
|
|
||||||
trigger={
|
|
||||||
<h1 className="flex gap-1 sm:gap-2 md:gap-4 items-center text-default text-lg whitespace-nowrap xl:pr-4 xl:border-r border-default">
|
|
||||||
{data ? data.tradableInstrument.instrument.code : t('Select market')}
|
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
'transition-transform ease-in-out duration-300',
|
|
||||||
{
|
|
||||||
'rotate-180': open,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={20} />
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<MarketSelector
|
|
||||||
currentMarketId={marketId}
|
|
||||||
onSelect={() => setOpen(false)}
|
|
||||||
/>
|
|
||||||
</FullScreenPopover>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface PopoverProps extends PopoverPrimitive.PopoverProps {
|
|
||||||
trigger: React.ReactNode | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FullScreenPopover = ({
|
|
||||||
trigger,
|
|
||||||
children,
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
}: PopoverProps) => {
|
|
||||||
return (
|
|
||||||
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
|
||||||
<PopoverPrimitive.Trigger data-testid="popover-trigger">
|
|
||||||
{trigger}
|
|
||||||
</PopoverPrimitive.Trigger>
|
|
||||||
<PopoverPrimitive.Portal>
|
|
||||||
<PopoverPrimitive.Content
|
|
||||||
data-testid="popover-content"
|
|
||||||
className="w-screen bg-vega-clight-800 dark:bg-vega-cdark-800 text-default border border-default"
|
|
||||||
sideOffset={5}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</PopoverPrimitive.Content>
|
|
||||||
</PopoverPrimitive.Portal>
|
|
||||||
</PopoverPrimitive.Root>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -34,13 +34,7 @@ import { supportedLngs } from '../../lib/i18n';
|
|||||||
type MenuState = 'wallet' | 'nav' | null;
|
type MenuState = 'wallet' | 'nav' | null;
|
||||||
type Theme = 'system' | 'yellow';
|
type Theme = 'system' | 'yellow';
|
||||||
|
|
||||||
export const Navbar = ({
|
export const Navbar = ({ theme = 'system' }: { theme?: Theme }) => {
|
||||||
children,
|
|
||||||
theme = 'system',
|
|
||||||
}: {
|
|
||||||
children?: ReactNode;
|
|
||||||
theme?: Theme;
|
|
||||||
}) => {
|
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const t = useT();
|
const t = useT();
|
||||||
// menu state for small screens
|
// menu state for small screens
|
||||||
@@ -75,8 +69,6 @@ export const Navbar = ({
|
|||||||
>
|
>
|
||||||
<VLogo className="w-4" />
|
<VLogo className="w-4" />
|
||||||
</NavLink>
|
</NavLink>
|
||||||
{/* Left section */}
|
|
||||||
<div className="flex items-center lg:hidden">{children}</div>
|
|
||||||
{/* Used to show header in nav on mobile */}
|
{/* Used to show header in nav on mobile */}
|
||||||
<div className="hidden lg:block">
|
<div className="hidden lg:block">
|
||||||
<NavbarMenu onClick={() => setMenu(null)} />
|
<NavbarMenu onClick={() => setMenu(null)} />
|
||||||
@@ -204,6 +196,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,35 @@
|
|||||||
|
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',
|
||||||
|
},
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...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',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
@@ -105,7 +105,7 @@ describe('ActiveRewards', () => {
|
|||||||
expect(
|
expect(
|
||||||
screen.getByText(/Liquidity provision fees received/i)
|
screen.getByText(/Liquidity provision fees received/i)
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText('Entity scope')).toBeInTheDocument();
|
expect(screen.getByText('Individual scope')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Average position')).toBeInTheDocument();
|
expect(screen.getByText('Average position')).toBeInTheDocument();
|
||||||
expect(screen.getByText('Ends in')).toBeInTheDocument();
|
expect(screen.getByText('Ends in')).toBeInTheDocument();
|
||||||
expect(screen.getByText('115431 epochs')).toBeInTheDocument();
|
expect(screen.getByText('115431 epochs')).toBeInTheDocument();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
VegaIconNames,
|
VegaIconNames,
|
||||||
TradingInput,
|
TradingInput,
|
||||||
TinyScroll,
|
TinyScroll,
|
||||||
|
truncateMiddle,
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
import { IconNames } from '@blueprintjs/icons';
|
import { IconNames } from '@blueprintjs/icons';
|
||||||
import {
|
import {
|
||||||
@@ -30,6 +31,9 @@ import {
|
|||||||
DispatchMetricLabels,
|
DispatchMetricLabels,
|
||||||
EntityScopeLabelMapping,
|
EntityScopeLabelMapping,
|
||||||
MarketState,
|
MarketState,
|
||||||
|
type DispatchStrategy,
|
||||||
|
IndividualScopeMapping,
|
||||||
|
IndividualScopeDescriptionMapping,
|
||||||
} from '@vegaprotocol/types';
|
} from '@vegaprotocol/types';
|
||||||
import { Card } from '../card/card';
|
import { Card } from '../card/card';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
@@ -148,7 +152,11 @@ export const ActiveRewards = ({ currentEpoch }: { currentEpoch: number }) => {
|
|||||||
if (!enrichedTransfers || !enrichedTransfers.length) return null;
|
if (!enrichedTransfers || !enrichedTransfers.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card title={t('Active rewards')} className="lg:col-span-full">
|
<Card
|
||||||
|
title={t('Active rewards')}
|
||||||
|
className="lg:col-span-full"
|
||||||
|
data-testid="active-rewards-card"
|
||||||
|
>
|
||||||
{enrichedTransfers.length > 1 && (
|
{enrichedTransfers.length > 1 && (
|
||||||
<TradingInput
|
<TradingInput
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
@@ -309,49 +317,29 @@ export const ActiveRewardCard = ({
|
|||||||
].includes(m.state)
|
].includes(m.state)
|
||||||
);
|
);
|
||||||
|
|
||||||
const assetInSettledMarket =
|
|
||||||
allMarkets &&
|
|
||||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
|
||||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
|
||||||
return (
|
|
||||||
m?.state &&
|
|
||||||
[
|
|
||||||
MarketState.STATE_TRADING_TERMINATED,
|
|
||||||
MarketState.STATE_SETTLED,
|
|
||||||
MarketState.STATE_CANCELLED,
|
|
||||||
MarketState.STATE_CLOSED,
|
|
||||||
].includes(m.state)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (marketSettled) {
|
if (marketSettled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gray out the cards that are related to suspended markets
|
const assetInActiveMarket =
|
||||||
const suspended = transferNode.markets?.some(
|
allMarkets &&
|
||||||
|
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
||||||
|
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
||||||
|
return m?.state && MarketState.STATE_ACTIVE === m.state;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const marketSuspended = transferNode.markets?.some(
|
||||||
(m) =>
|
(m) =>
|
||||||
m?.state === MarketState.STATE_SUSPENDED ||
|
m?.state === MarketState.STATE_SUSPENDED ||
|
||||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
||||||
);
|
);
|
||||||
|
|
||||||
const assetInSuspendedMarket =
|
|
||||||
allMarkets &&
|
|
||||||
Object.values(allMarkets).some((m: MarketFieldsFragment | null) => {
|
|
||||||
if (m && getAsset(m).id === dispatchStrategy.dispatchMetricAssetId) {
|
|
||||||
return (
|
|
||||||
m?.state === MarketState.STATE_SUSPENDED ||
|
|
||||||
m?.state === MarketState.STATE_SUSPENDED_VIA_GOVERNANCE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gray out the cards that are related to suspended markets
|
// Gray out the cards that are related to suspended markets
|
||||||
|
// Or settlement assets in markets that are not active and eligible for rewards
|
||||||
const { gradientClassName, mainClassName } =
|
const { gradientClassName, mainClassName } =
|
||||||
suspended || assetInSuspendedMarket || assetInSettledMarket
|
marketSuspended || !assetInActiveMarket
|
||||||
? {
|
? {
|
||||||
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
|
gradientClassName: 'from-vega-cdark-500 to-vega-clight-400',
|
||||||
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
|
mainClassName: 'from-vega-cdark-400 dark:from-vega-cdark-600 to-20%',
|
||||||
@@ -359,6 +347,7 @@ export const ActiveRewardCard = ({
|
|||||||
: getGradientClasses(dispatchStrategy.dispatchMetric);
|
: getGradientClasses(dispatchStrategy.dispatchMetric);
|
||||||
|
|
||||||
const entityScope = dispatchStrategy.entityScope;
|
const entityScope = dispatchStrategy.entityScope;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -367,6 +356,7 @@ export const ActiveRewardCard = ({
|
|||||||
'rounded-lg',
|
'rounded-lg',
|
||||||
gradientClassName
|
gradientClassName
|
||||||
)}
|
)}
|
||||||
|
data-testid="active-rewards-card"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -378,7 +368,7 @@ export const ActiveRewardCard = ({
|
|||||||
<div className="flex flex-col gap-2 items-center text-center">
|
<div className="flex flex-col gap-2 items-center text-center">
|
||||||
<EntityIcon transfer={transfer} />
|
<EntityIcon transfer={transfer} />
|
||||||
{entityScope && (
|
{entityScope && (
|
||||||
<span className="text-muted text-xs">
|
<span className="text-muted text-xs" data-testid="entity-scope">
|
||||||
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
|
{EntityScopeLabelMapping[entityScope] || t('Unspecified')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -386,7 +376,7 @@ export const ActiveRewardCard = ({
|
|||||||
|
|
||||||
<div className="flex flex-col gap-2 items-center text-center">
|
<div className="flex flex-col gap-2 items-center text-center">
|
||||||
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
|
<h3 className="flex flex-col gap-1 text-2xl shrink-1 text-center">
|
||||||
<span className="font-glitch">
|
<span className="font-glitch" data-testid="reward-value">
|
||||||
{addDecimalsFormatNumber(
|
{addDecimalsFormatNumber(
|
||||||
transferNode.transfer.amount,
|
transferNode.transfer.amount,
|
||||||
transferNode.transfer.asset?.decimals || 0,
|
transferNode.transfer.asset?.decimals || 0,
|
||||||
@@ -407,7 +397,7 @@ export const ActiveRewardCard = ({
|
|||||||
)}
|
)}
|
||||||
underline={true}
|
underline={true}
|
||||||
>
|
>
|
||||||
<span className="text-xs">
|
<span className="text-xs" data-testid="distribution-strategy">
|
||||||
{
|
{
|
||||||
DistributionStrategyMapping[
|
DistributionStrategyMapping[
|
||||||
dispatchStrategy.distributionStrategy
|
dispatchStrategy.distributionStrategy
|
||||||
@@ -425,7 +415,10 @@ export const ActiveRewardCard = ({
|
|||||||
'Number of epochs after distribution to delay vesting of rewards by'
|
'Number of epochs after distribution to delay vesting of rewards by'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="text-muted text-xs whitespace-nowrap">
|
<span
|
||||||
|
className="text-muted text-xs whitespace-nowrap"
|
||||||
|
data-testid="locked-for"
|
||||||
|
>
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
{t('numberEpochs', '{{count}} epochs', {
|
||||||
count: kind.dispatchStrategy?.lockPeriod,
|
count: kind.dispatchStrategy?.lockPeriod,
|
||||||
})}
|
})}
|
||||||
@@ -434,15 +427,15 @@ export const ActiveRewardCard = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="border-[0.5px] border-gray-700" />
|
<span className="border-[0.5px] border-gray-700" />
|
||||||
<span>
|
<span data-testid="dispatch-metric-info">
|
||||||
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
{DispatchMetricLabels[dispatchStrategy.dispatchMetric]} •{' '}
|
||||||
<Tooltip
|
<Tooltip
|
||||||
underline={suspended}
|
underline={marketSuspended}
|
||||||
description={
|
description={
|
||||||
(suspended || assetInSuspendedMarket) &&
|
(marketSuspended || !assetInActiveMarket) &&
|
||||||
(specificMarkets
|
(specificMarkets
|
||||||
? t('Eligible market(s) currently suspended')
|
? t('Eligible market(s) currently suspended')
|
||||||
: assetInSuspendedMarket
|
: !assetInActiveMarket
|
||||||
? t('Currently no markets eligible for reward')
|
? t('Currently no markets eligible for reward')
|
||||||
: '')
|
: '')
|
||||||
}
|
}
|
||||||
@@ -454,8 +447,8 @@ export const ActiveRewardCard = ({
|
|||||||
<div className="flex items-center gap-8 flex-wrap">
|
<div className="flex items-center gap-8 flex-wrap">
|
||||||
{kind.endEpoch && (
|
{kind.endEpoch && (
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className="text-muted text-xs">{t('Ends in')}</span>
|
<span className="text-muted text-xs">{t('Ends in')} </span>
|
||||||
<span>
|
<span data-testid="ends-in">
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
{t('numberEpochs', '{{count}} epochs', {
|
||||||
count: kind.endEpoch - currentEpoch,
|
count: kind.endEpoch - currentEpoch,
|
||||||
})}
|
})}
|
||||||
@@ -466,7 +459,7 @@ export const ActiveRewardCard = ({
|
|||||||
{
|
{
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
<span className="text-muted text-xs">{t('Assessed over')}</span>
|
||||||
<span>
|
<span data-testid="assessed-over">
|
||||||
{t('numberEpochs', '{{count}} epochs', {
|
{t('numberEpochs', '{{count}} epochs', {
|
||||||
count: dispatchStrategy.windowLength,
|
count: dispatchStrategy.windowLength,
|
||||||
})}
|
})}
|
||||||
@@ -474,83 +467,130 @@ export const ActiveRewardCard = ({
|
|||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{dispatchStrategy?.dispatchMetric && (
|
{dispatchStrategy?.dispatchMetric && (
|
||||||
<span className="text-muted text-sm h-[2rem]">
|
<span className="text-muted text-sm h-[3rem]">
|
||||||
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
|
{t(DispatchMetricDescription[dispatchStrategy?.dispatchMetric])}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<span className="border-[0.5px] border-gray-700" />
|
<span className="border-[0.5px] border-gray-700" />
|
||||||
|
{kind.dispatchStrategy && (
|
||||||
<div className="flex justify-between flex-wrap items-center gap-3 text-xs">
|
<RewardRequirements
|
||||||
<span className="flex flex-col gap-1">
|
dispatchStrategy={kind.dispatchStrategy}
|
||||||
<span className="flex items-center gap-1 text-muted">
|
assetDecimalPlaces={transfer.asset?.decimals}
|
||||||
{t('Entity scope')}{' '}
|
/>
|
||||||
</span>
|
)}
|
||||||
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
{kind.dispatchStrategy?.teamScope && (
|
|
||||||
<Tooltip
|
|
||||||
description={
|
|
||||||
<span>{kind.dispatchStrategy?.teamScope}</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="flex items-center p-1 rounded-full border border-gray-600">
|
|
||||||
{<VegaIcon name={VegaIconNames.TEAM} size={16} />}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{kind.dispatchStrategy?.individualScope && (
|
|
||||||
<Tooltip
|
|
||||||
description={
|
|
||||||
<span>{kind.dispatchStrategy?.individualScope}</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="flex items-center p-1 rounded-full border border-gray-600">
|
|
||||||
{<VegaIcon name={VegaIconNames.MAN} size={16} />}
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
)}
|
|
||||||
{/* Shows transfer status */}
|
|
||||||
{/* <StatusIndicator
|
|
||||||
status={transfer.status}
|
|
||||||
reason={transfer.reason}
|
|
||||||
/> */}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="flex flex-col gap-1">
|
|
||||||
<span className="flex items-center gap-1 text-muted">
|
|
||||||
{t('Staked VEGA')}{' '}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
{addDecimalsFormatNumber(
|
|
||||||
kind.dispatchStrategy?.stakingRequirement || 0,
|
|
||||||
transfer.asset?.decimals || 0
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span className="flex flex-col gap-1">
|
|
||||||
<span className="flex items-center gap-1 text-muted">
|
|
||||||
{t('Average position')}{' '}
|
|
||||||
</span>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
{addDecimalsFormatNumber(
|
|
||||||
kind.dispatchStrategy
|
|
||||||
?.notionalTimeWeightedAveragePositionRequirement || 0,
|
|
||||||
transfer.asset?.decimals || 0
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RewardRequirements = ({
|
||||||
|
dispatchStrategy,
|
||||||
|
assetDecimalPlaces = 0,
|
||||||
|
}: {
|
||||||
|
dispatchStrategy: DispatchStrategy;
|
||||||
|
assetDecimalPlaces: number | undefined;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<dl className="flex justify-between flex-wrap items-center gap-3 text-xs">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<dt className="flex items-center gap-1 text-muted">
|
||||||
|
{t('{{entity}} scope', {
|
||||||
|
entity: EntityScopeLabelMapping[dispatchStrategy.entityScope],
|
||||||
|
})}
|
||||||
|
</dt>
|
||||||
|
<dd className="flex items-center gap-1" data-testid="scope">
|
||||||
|
<RewardEntityScope dispatchStrategy={dispatchStrategy} />
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<dt className="flex items-center gap-1 text-muted">
|
||||||
|
{t('Staked VEGA')}
|
||||||
|
</dt>
|
||||||
|
<dd
|
||||||
|
className="flex items-center gap-1"
|
||||||
|
data-testid="staking-requirement"
|
||||||
|
>
|
||||||
|
{addDecimalsFormatNumber(
|
||||||
|
dispatchStrategy?.stakingRequirement || 0,
|
||||||
|
assetDecimalPlaces
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<dt className="flex items-center gap-1 text-muted">
|
||||||
|
{t('Average position')}
|
||||||
|
</dt>
|
||||||
|
<dd className="flex items-center gap-1" data-testid="average-position">
|
||||||
|
{addDecimalsFormatNumber(
|
||||||
|
dispatchStrategy?.notionalTimeWeightedAveragePositionRequirement ||
|
||||||
|
0,
|
||||||
|
assetDecimalPlaces
|
||||||
|
)}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const RewardEntityScope = ({
|
||||||
|
dispatchStrategy,
|
||||||
|
}: {
|
||||||
|
dispatchStrategy: DispatchStrategy;
|
||||||
|
}) => {
|
||||||
|
const t = useT();
|
||||||
|
|
||||||
|
if (dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_TEAMS) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
description={
|
||||||
|
dispatchStrategy.teamScope?.length ? (
|
||||||
|
<div className="text-xs">
|
||||||
|
<p className="mb-1">{t('Eligible teams')}</p>
|
||||||
|
<ul>
|
||||||
|
{dispatchStrategy.teamScope.map((teamId) => {
|
||||||
|
if (!teamId) return null;
|
||||||
|
return <li key={teamId}>{truncateMiddle(teamId)}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
t('All teams are eligible')
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{dispatchStrategy.teamScope?.length
|
||||||
|
? t('Some teams')
|
||||||
|
: t('All teams')}
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
dispatchStrategy.entityScope === EntityScope.ENTITY_SCOPE_INDIVIDUALS &&
|
||||||
|
dispatchStrategy.individualScope
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<Tooltip
|
||||||
|
description={
|
||||||
|
IndividualScopeDescriptionMapping[dispatchStrategy.individualScope]
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>{IndividualScopeMapping[dispatchStrategy.individualScope]}</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
const getGradientClasses = (d: DispatchMetric | undefined) => {
|
const getGradientClasses = (d: DispatchMetric | undefined) => {
|
||||||
switch (d) {
|
switch (d) {
|
||||||
case DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION:
|
case DispatchMetric.DISPATCH_METRIC_AVERAGE_POSITION:
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ export const Settings = () => {
|
|||||||
intent={Intent.Primary}
|
intent={Intent.Primary}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
sessionStorage.clear();
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
|
|||||||
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
|
||||||
import { useT } from '../../lib/use-t';
|
import { useT } from '../../lib/use-t';
|
||||||
import { ErrorBoundary } from '../error-boundary';
|
import { ErrorBoundary } from '../error-boundary';
|
||||||
|
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
|
||||||
|
|
||||||
export enum ViewType {
|
export enum ViewType {
|
||||||
Order = 'Order',
|
Order = 'Order',
|
||||||
@@ -26,9 +27,10 @@ export enum ViewType {
|
|||||||
Transfer = 'Transfer',
|
Transfer = 'Transfer',
|
||||||
Settings = 'Settings',
|
Settings = 'Settings',
|
||||||
ViewAs = 'ViewAs',
|
ViewAs = 'ViewAs',
|
||||||
|
Close = 'Close',
|
||||||
}
|
}
|
||||||
|
|
||||||
type SidebarView =
|
export type BarView =
|
||||||
| {
|
| {
|
||||||
type: ViewType.Deposit;
|
type: ViewType.Deposit;
|
||||||
assetId?: string;
|
assetId?: string;
|
||||||
@@ -49,6 +51,9 @@ type SidebarView =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: ViewType.Settings;
|
type: ViewType.Settings;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ViewType.Close;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Sidebar = ({ options }: { options?: ReactNode }) => {
|
export const Sidebar = ({ options }: { options?: ReactNode }) => {
|
||||||
@@ -57,26 +62,52 @@ export const Sidebar = ({ options }: { options?: ReactNode }) => {
|
|||||||
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
|
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
|
||||||
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
|
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
|
||||||
const { pubKeys } = useVegaWallet();
|
const { pubKeys } = useVegaWallet();
|
||||||
|
const { isMobile } = useScreenDimensions();
|
||||||
|
const { getView } = useSidebar((store) => ({
|
||||||
|
setViews: store.setViews,
|
||||||
|
getView: store.getView,
|
||||||
|
}));
|
||||||
|
const currView = getView(currentRouteId);
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar">
|
<div className="flex h-full lg:flex-col gap-1" data-testid="sidebar">
|
||||||
{options && <nav className={navClasses}>{options}</nav>}
|
{options && (
|
||||||
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
|
<nav className={classNames(navClasses, 'flex grow')}>{options}</nav>
|
||||||
<SidebarButton
|
)}
|
||||||
view={ViewType.ViewAs}
|
<nav
|
||||||
onClick={() => {
|
className={classNames(
|
||||||
setViewAsDialogOpen(true);
|
navClasses,
|
||||||
}}
|
'ml-auto lg:mt-auto lg:ml-0 shrink-0'
|
||||||
icon={VegaIconNames.EYE}
|
)}
|
||||||
tooltip={t('View as party')}
|
>
|
||||||
disabled={Boolean(pubKeys)}
|
{!isMobile ? (
|
||||||
routeId={currentRouteId}
|
<>
|
||||||
/>
|
<SidebarButton
|
||||||
<SidebarButton
|
view={ViewType.ViewAs}
|
||||||
view={ViewType.Settings}
|
onClick={() => {
|
||||||
icon={VegaIconNames.COG}
|
setViewAsDialogOpen(true);
|
||||||
tooltip={t('Settings')}
|
}}
|
||||||
routeId={currentRouteId}
|
icon={VegaIconNames.EYE}
|
||||||
/>
|
tooltip={t('View as party')}
|
||||||
|
disabled={Boolean(pubKeys)}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
<SidebarButton
|
||||||
|
view={ViewType.Settings}
|
||||||
|
icon={VegaIconNames.COG}
|
||||||
|
tooltip={t('Settings')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
currView && (
|
||||||
|
<SidebarButton
|
||||||
|
view={ViewType.Close}
|
||||||
|
icon={VegaIconNames.ARROW_LEFT}
|
||||||
|
tooltip={t('Back')}
|
||||||
|
routeId={currentRouteId}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
<NodeHealthContainer />
|
<NodeHealthContainer />
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +134,7 @@ export const SidebarButton = ({
|
|||||||
getView: store.getView,
|
getView: store.getView,
|
||||||
}));
|
}));
|
||||||
const currView = getView(routeId);
|
const currView = getView(routeId);
|
||||||
const onSelect = (view: SidebarView['type']) => {
|
const onSelect = (view: BarView['type']) => {
|
||||||
if (view === currView?.type) {
|
if (view === currView?.type) {
|
||||||
setViews(null, routeId);
|
setViews(null, routeId);
|
||||||
} else {
|
} else {
|
||||||
@@ -133,7 +164,7 @@ export const SidebarButton = ({
|
|||||||
<button
|
<button
|
||||||
className={buttonClasses}
|
className={buttonClasses}
|
||||||
data-testid={view}
|
data-testid={view}
|
||||||
onClick={onClick || (() => onSelect(view as SidebarView['type']))}
|
onClick={onClick || (() => onSelect(view as BarView['type']))}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<VegaIcon name={icon} size={20} />
|
<VegaIcon name={icon} size={20} />
|
||||||
@@ -180,6 +211,10 @@ export const SidebarContent = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (view.type === ViewType.Close) {
|
||||||
|
return <CloseSidebar />;
|
||||||
|
}
|
||||||
|
|
||||||
if (view.type === ViewType.Info) {
|
if (view.type === ViewType.Info) {
|
||||||
if (params.marketId) {
|
if (params.marketId) {
|
||||||
return (
|
return (
|
||||||
@@ -267,9 +302,9 @@ const CloseSidebar = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const useSidebar = create<{
|
export const useSidebar = create<{
|
||||||
views: { [key: string]: SidebarView | null };
|
views: { [key: string]: BarView | null };
|
||||||
setViews: (view: SidebarView | null, routeId: string) => void;
|
setViews: (view: BarView | null, routeId: string) => void;
|
||||||
getView: (routeId: string) => SidebarView | null | undefined;
|
getView: (routeId: string) => BarView | null | undefined;
|
||||||
}>()((set, get) => ({
|
}>()((set, get) => ({
|
||||||
views: {},
|
views: {},
|
||||||
setViews: (x, routeId) =>
|
setViews: (x, routeId) =>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
CONSOLE_IMAGE_NAME=vegaprotocol/trading:latest
|
||||||
VEGA_VERSION=v0.74.0-preview.7
|
VEGA_VERSION=v0.74.0-preview.10
|
||||||
LOCAL_SERVER=false
|
LOCAL_SERVER=false
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ def init_vega(request=None):
|
|||||||
f"Container {container.id} started",
|
f"Container {container.id} started",
|
||||||
extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")},
|
extra={"worker_id": os.environ.get("PYTEST_XDIST_WORKER")},
|
||||||
)
|
)
|
||||||
|
vega.container = container
|
||||||
yield vega
|
yield vega
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
logger.info(f"Container creation failed.")
|
logger.info(f"Container creation failed.")
|
||||||
@@ -177,10 +178,34 @@ def init_page(vega: VegaServiceNull, browser: Browser, request: pytest.FixtureRe
|
|||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def vega(request):
|
def vega(request):
|
||||||
with init_vega(request) as vega:
|
with init_vega(request) as vega_instance:
|
||||||
yield vega
|
request.addfinalizer(lambda: cleanup_container(vega_instance))
|
||||||
|
yield vega_instance
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup_container(vega_instance):
|
||||||
|
try:
|
||||||
|
# Attempt to stop the container if it's still running
|
||||||
|
if vega_instance.container.status == 'running':
|
||||||
|
print(f"Stopping container {vega_instance.container.id}")
|
||||||
|
vega_instance.container.stop()
|
||||||
|
else:
|
||||||
|
print(f"Container {vega_instance.container.id} is not running.")
|
||||||
|
except docker.errors.NotFound:
|
||||||
|
print(f"Container {vega_instance.container.id} not found, may have been stopped and removed.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during cleanup: {str(e)}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Attempt to remove the container
|
||||||
|
vega_instance.container.remove()
|
||||||
|
print(f"Container {vega_instance.container.id} removed.")
|
||||||
|
except docker.errors.NotFound:
|
||||||
|
print(f"Container {vega_instance.container.id} not found, may have been removed.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during container removal: {str(e)}")
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def page(vega, browser, request):
|
def page(vega, browser, request):
|
||||||
with init_page(vega, browser, request) as page_instance:
|
with init_page(vega, browser, request) as page_instance:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import pytest
|
|||||||
from playwright.sync_api import Page, expect
|
from playwright.sync_api import Page, expect
|
||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from conftest import init_vega
|
from conftest import init_vega, cleanup_container
|
||||||
from fixtures.market import setup_continuous_market
|
from fixtures.market import setup_continuous_market
|
||||||
from actions.utils import wait_for_toast_confirmation
|
from actions.utils import wait_for_toast_confirmation
|
||||||
|
|
||||||
@@ -17,8 +17,10 @@ expire = "expire"
|
|||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def vega(request):
|
def vega(request):
|
||||||
with init_vega(request) as vega:
|
with init_vega(request) as vega_instance:
|
||||||
yield vega
|
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
|
||||||
|
yield vega_instance
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ def test_switch_cross_isolated_margin(
|
|||||||
expect(page.get_by_test_id("toast-content")).to_have_text(
|
expect(page.get_by_test_id("toast-content")).to_have_text(
|
||||||
"ConfirmedYour transaction has been confirmedView in block explorerUpdate margin modeBTC:DAI_2023Isolated margin mode, leverage: 1.0x")
|
"ConfirmedYour transaction has been confirmedView in block explorerUpdate margin modeBTC:DAI_2023Isolated margin mode, leverage: 1.0x")
|
||||||
expect(page.locator(margin_row).nth(1)
|
expect(page.locator(margin_row).nth(1)
|
||||||
).to_have_text("11,109.99996Isolated1.0x")
|
).to_have_text("22,109.99996Isolated1.0x")
|
||||||
# tbd - tooltip is not visible without this wait
|
# tbd - tooltip is not visible without this wait
|
||||||
page.wait_for_timeout(1000)
|
page.wait_for_timeout(1000)
|
||||||
page.get_by_test_id(tab_positions).get_by_text("Isolated").hover()
|
page.get_by_test_id(tab_positions).get_by_text("Isolated").hover()
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from playwright.sync_api import Page, expect
|
|||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from actions.vega import submit_order
|
from actions.vega import submit_order
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from conftest import init_vega
|
from conftest import init_vega, cleanup_container
|
||||||
from fixtures.market import setup_continuous_market
|
from fixtures.market import setup_continuous_market
|
||||||
|
|
||||||
stop_order_btn = "order-type-Stop"
|
stop_order_btn = "order-type-Stop"
|
||||||
@@ -259,9 +259,10 @@ def test_submit_stop_limit_order_cancel(
|
|||||||
|
|
||||||
class TestStopOcoValidation:
|
class TestStopOcoValidation:
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
def vega(self, request):
|
def vega(request):
|
||||||
with init_vega(request) as vega:
|
with init_vega(request) as vega_instance:
|
||||||
yield vega
|
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
|
||||||
|
yield vega_instance
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
def continuous_market(self, vega):
|
def continuous_market(self, vega):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import pytest
|
|||||||
from playwright.sync_api import Page, expect
|
from playwright.sync_api import Page, expect
|
||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from actions.utils import change_keys
|
from actions.utils import change_keys
|
||||||
from conftest import init_vega
|
from conftest import init_vega, cleanup_container
|
||||||
from fixtures.market import setup_continuous_market
|
from fixtures.market import setup_continuous_market
|
||||||
|
|
||||||
order_size = "order-size"
|
order_size = "order-size"
|
||||||
@@ -14,8 +14,9 @@ deal_ticket_deposit_dialog_button = "deal-ticket-deposit-dialog-button"
|
|||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def vega(request):
|
def vega(request):
|
||||||
with init_vega(request) as vega:
|
with init_vega(request) as vega_instance:
|
||||||
yield vega
|
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
|
||||||
|
yield vega_instance
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
@@ -23,7 +24,6 @@ def continuous_market(vega):
|
|||||||
return setup_continuous_market(vega)
|
return setup_continuous_market(vega)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip("marked id issue #5681")
|
|
||||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||||
def test_should_display_info_and_button_for_deposit(continuous_market, page: Page):
|
def test_should_display_info_and_button_for_deposit(continuous_market, page: Page):
|
||||||
page.goto(f"/#/markets/{continuous_market}")
|
page.goto(f"/#/markets/{continuous_market}")
|
||||||
@@ -34,7 +34,7 @@ def test_should_display_info_and_button_for_deposit(continuous_market, page: Pag
|
|||||||
"You may not have enough margin available to open this position.")
|
"You may not have enough margin available to open this position.")
|
||||||
page.get_by_test_id(deal_ticket_warning_margin).hover()
|
page.get_by_test_id(deal_ticket_warning_margin).hover()
|
||||||
expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text(
|
expect(page.get_by_test_id("tooltip-content").nth(0)).to_have_text(
|
||||||
"1,661,896.6317 tDAI is currently required.You have only 1,000,000.00.Deposit tDAI")
|
"1,661,888.12901 tDAI is currently required.You have only 999,991.49731.Deposit tDAI")
|
||||||
page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
|
page.get_by_test_id(deal_ticket_deposit_dialog_button).nth(0).click()
|
||||||
expect(page.get_by_test_id("sidebar-content")
|
expect(page.get_by_test_id("sidebar-content")
|
||||||
).to_contain_text("DepositFrom")
|
).to_contain_text("DepositFrom")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from playwright.sync_api import Page, expect
|
|||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from actions.vega import submit_order
|
from actions.vega import submit_order
|
||||||
from wallet_config import MM_WALLET
|
from wallet_config import MM_WALLET
|
||||||
from conftest import init_vega, init_page, auth_setup
|
from conftest import init_vega, init_page, auth_setup, cleanup_container
|
||||||
from actions.utils import next_epoch, change_keys, forward_time
|
from actions.utils import next_epoch, change_keys, forward_time
|
||||||
from fixtures.market import market_exists, setup_continuous_market
|
from fixtures.market import market_exists, setup_continuous_market
|
||||||
|
|
||||||
@@ -82,31 +82,36 @@ def market_ids():
|
|||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def vega_volume_discount_tier_1(request):
|
def vega_volume_discount_tier_1(request):
|
||||||
with init_vega(request) as vega_volume_discount_tier_1:
|
with init_vega(request) as vega_volume_discount_tier_1:
|
||||||
yield vega_volume_discount_tier_1
|
request.addfinalizer(lambda: cleanup_container(vega_volume_discount_tier_1)) # Register the cleanup function
|
||||||
|
yield vega_volume_discount_tier_1
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def vega_volume_discount_tier_2(request):
|
def vega_volume_discount_tier_2(request):
|
||||||
with init_vega(request) as vega_volume_discount_tier_2:
|
with init_vega(request) as vega_volume_discount_tier_2:
|
||||||
yield vega_volume_discount_tier_2
|
request.addfinalizer(lambda: cleanup_container(vega_volume_discount_tier_2)) # Register the cleanup function
|
||||||
|
yield vega_volume_discount_tier_2
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def vega_referral_discount_tier_1(request):
|
def vega_referral_discount_tier_1(request):
|
||||||
with init_vega(request) as vega_referral_discount_tier_1:
|
with init_vega(request) as vega_referral_discount_tier_1:
|
||||||
yield vega_referral_discount_tier_1
|
request.addfinalizer(lambda: cleanup_container(vega_referral_discount_tier_1)) # Register the cleanup function
|
||||||
|
yield vega_referral_discount_tier_1
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def vega_referral_discount_tier_2(request):
|
def vega_referral_discount_tier_2(request):
|
||||||
with init_vega(request) as vega_referral_discount_tier_2:
|
with init_vega(request) as vega_referral_discount_tier_2:
|
||||||
yield vega_referral_discount_tier_2
|
request.addfinalizer(lambda: cleanup_container(vega_referral_discount_tier_2)) # Register the cleanup function
|
||||||
|
yield vega_referral_discount_tier_2
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def vega_referral_and_volume_discount(request):
|
def vega_referral_and_volume_discount(request):
|
||||||
with init_vega(request) as vega_referral_and_volume_discount:
|
with init_vega(request) as vega_referral_and_volume_discount:
|
||||||
yield vega_referral_and_volume_discount
|
request.addfinalizer(lambda: cleanup_container(vega_referral_and_volume_discount)) # Register the cleanup function
|
||||||
|
yield vega_referral_and_volume_discount
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from playwright.sync_api import expect, Page
|
|||||||
import json
|
import json
|
||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from fixtures.market import setup_simple_market
|
from fixtures.market import setup_simple_market
|
||||||
from conftest import init_vega
|
from conftest import init_vega, cleanup_container
|
||||||
from actions.vega import submit_order
|
from actions.vega import submit_order
|
||||||
from wallet_config import MM_WALLET, TERMINATE_WALLET, wallets
|
from wallet_config import MM_WALLET, TERMINATE_WALLET, wallets
|
||||||
import logging
|
import logging
|
||||||
@@ -12,9 +12,10 @@ logger = logging.getLogger()
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
def vega():
|
def vega(request):
|
||||||
with init_vega() as vega:
|
with init_vega(request) as vega_instance:
|
||||||
yield vega
|
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
|
||||||
|
yield vega_instance
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ import pytest
|
|||||||
from playwright.sync_api import expect, Page
|
from playwright.sync_api import expect, Page
|
||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from actions.vega import submit_order
|
from actions.vega import submit_order
|
||||||
from conftest import init_vega
|
|
||||||
from fixtures.market import setup_continuous_market
|
|
||||||
from wallet_config import MM_WALLET2
|
from wallet_config import MM_WALLET2
|
||||||
|
|
||||||
def hover_and_assert_tooltip(page: Page, element_text):
|
def hover_and_assert_tooltip(page: Page, element_text):
|
||||||
@@ -11,39 +9,30 @@ def hover_and_assert_tooltip(page: Page, element_text):
|
|||||||
element.hover()
|
element.hover()
|
||||||
expect(page.get_by_role("tooltip")).to_be_visible()
|
expect(page.get_by_role("tooltip")).to_be_visible()
|
||||||
|
|
||||||
class TestIcebergOrdersValidations:
|
|
||||||
@pytest.fixture(scope="class")
|
|
||||||
def vega(self, request):
|
|
||||||
with init_vega(request) as vega:
|
|
||||||
yield vega
|
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||||
def continuous_market(self, vega):
|
def test_iceberg_submit(continuous_market, vega: VegaServiceNull, page: Page):
|
||||||
return setup_continuous_market(vega)
|
page.goto(f"/#/markets/{continuous_market}")
|
||||||
|
page.get_by_test_id("iceberg").click()
|
||||||
|
page.get_by_test_id("order-peak-size").type("2")
|
||||||
|
page.get_by_test_id("order-minimum-size").type("1")
|
||||||
|
page.get_by_test_id("order-size").type("3")
|
||||||
|
page.get_by_test_id("order-price").type("107")
|
||||||
|
page.get_by_test_id("place-order").click()
|
||||||
|
|
||||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
expect(page.get_by_test_id("toast-content")).to_have_text(
|
||||||
def test_iceberg_submit(self, continuous_market, vega: VegaServiceNull, page: Page):
|
"Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer"
|
||||||
page.goto(f"/#/markets/{continuous_market}")
|
)
|
||||||
page.get_by_test_id("iceberg").click()
|
|
||||||
page.get_by_test_id("order-peak-size").type("2")
|
|
||||||
page.get_by_test_id("order-minimum-size").type("1")
|
|
||||||
page.get_by_test_id("order-size").type("3")
|
|
||||||
page.get_by_test_id("order-price").type("107")
|
|
||||||
page.get_by_test_id("place-order").click()
|
|
||||||
|
|
||||||
expect(page.get_by_test_id("toast-content")).to_have_text(
|
vega.wait_fn(1)
|
||||||
"Awaiting confirmationPlease wait for your transaction to be confirmedView in block explorer"
|
vega.wait_for_total_catchup()
|
||||||
)
|
expect(page.get_by_test_id("toast-content")).to_have_text(
|
||||||
|
"Order filledYour transaction has been confirmedView in block explorerSubmit order - filledBTC:DAI_2023+3 @ 107.00 tDAI"
|
||||||
vega.wait_fn(1)
|
)
|
||||||
vega.wait_for_total_catchup()
|
page.get_by_test_id("All").click()
|
||||||
expect(page.get_by_test_id("toast-content")).to_have_text(
|
expect(
|
||||||
"Order filledYour transaction has been confirmedView in block explorerSubmit order - filledBTC:DAI_2023+3 @ 107.00 tDAI"
|
(page.get_by_role("row").locator('[col-id="type"]')).nth(1)
|
||||||
)
|
).to_have_text("Limit (Iceberg)")
|
||||||
page.get_by_test_id("All").click()
|
|
||||||
expect(
|
|
||||||
(page.get_by_role("row").locator('[col-id="type"]')).nth(1)
|
|
||||||
).to_have_text("Limit (Iceberg)")
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("auth", "risk_accepted")
|
@pytest.mark.usefixtures("auth", "risk_accepted")
|
||||||
def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page):
|
def test_iceberg_open_order(continuous_market, vega: VegaServiceNull, page: Page):
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import pytest
|
import pytest
|
||||||
from playwright.sync_api import Page, expect
|
from playwright.sync_api import Page, expect
|
||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from conftest import init_vega
|
from conftest import init_vega, cleanup_container
|
||||||
from fixtures.market import setup_continuous_market
|
from fixtures.market import setup_continuous_market
|
||||||
from actions.utils import next_epoch, truncate_middle, change_keys
|
from actions.utils import next_epoch, truncate_middle, change_keys
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def vega(request):
|
def vega(request):
|
||||||
with init_vega(request) as vega:
|
with init_vega(request) as vega_instance:
|
||||||
yield vega
|
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
|
||||||
|
yield vega_instance
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import vega_sim.api.governance as governance
|
|||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from playwright.sync_api import Page, expect
|
from playwright.sync_api import Page, expect
|
||||||
from fixtures.market import setup_continuous_market
|
from fixtures.market import setup_continuous_market
|
||||||
from conftest import init_vega
|
from conftest import init_vega, cleanup_container
|
||||||
from actions.utils import next_epoch
|
from actions.utils import next_epoch
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
def vega():
|
def vega(request):
|
||||||
with init_vega() as vega:
|
with init_vega(request) as vega_instance:
|
||||||
yield vega
|
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
|
||||||
|
yield vega_instance
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="class")
|
@pytest.fixture(scope="class")
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ import pytest
|
|||||||
from playwright.sync_api import Page, expect
|
from playwright.sync_api import Page, expect
|
||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from fixtures.market import setup_continuous_market
|
from fixtures.market import setup_continuous_market
|
||||||
from conftest import init_page, init_vega, risk_accepted_setup
|
from conftest import init_page, init_vega, risk_accepted_setup, cleanup_container
|
||||||
|
|
||||||
market_title_test_id = "accordion-title"
|
market_title_test_id = "accordion-title"
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def vega():
|
def vega(request):
|
||||||
with init_vega() as vega:
|
with init_vega(request) as vega_instance:
|
||||||
yield vega
|
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
|
||||||
|
yield vega_instance
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import vega_sim.api.governance as governance
|
|||||||
import re
|
import re
|
||||||
from playwright.sync_api import Page, expect
|
from playwright.sync_api import Page, expect
|
||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from conftest import init_vega
|
from conftest import init_vega, cleanup_container
|
||||||
from fixtures.market import setup_simple_market
|
from fixtures.market import setup_simple_market
|
||||||
from wallet_config import MM_WALLET
|
from wallet_config import MM_WALLET
|
||||||
|
|
||||||
@@ -13,8 +13,10 @@ col_market_id = '[col-id="market"] [data-testid="stack-cell-primary"]'
|
|||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def vega(request):
|
def vega(request):
|
||||||
with init_vega(request) as vega:
|
with init_vega(request) as vega_instance:
|
||||||
yield vega
|
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
|
||||||
|
yield vega_instance
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from playwright.sync_api import Page, expect
|
|||||||
from vega_sim.null_service import VegaServiceNull
|
from vega_sim.null_service import VegaServiceNull
|
||||||
from actions.vega import submit_order
|
from actions.vega import submit_order
|
||||||
from fixtures.market import setup_simple_market
|
from fixtures.market import setup_simple_market
|
||||||
from conftest import init_vega
|
from conftest import init_vega, cleanup_container
|
||||||
from actions.utils import wait_for_toast_confirmation, change_keys
|
from actions.utils import wait_for_toast_confirmation, change_keys
|
||||||
from wallet_config import MM_WALLET, MM_WALLET2
|
from wallet_config import MM_WALLET, MM_WALLET2
|
||||||
|
|
||||||
@@ -15,8 +15,10 @@ COL_ID_FEE = ".ag-center-cols-container [col-id='fee'] .ag-cell-value"
|
|||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def vega(request):
|
def vega(request):
|
||||||
with init_vega(request) as vega:
|
with init_vega(request) as vega_instance:
|
||||||
yield vega
|
request.addfinalizer(lambda: cleanup_container(vega_instance)) # Register the cleanup function
|
||||||
|
yield vega_instance
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user