Feat/224 move token app into monorepo (#229)

* moved TFE into monorepo with some remaining errors

* moved TFE into monorepo with some remaining errors - further files

* add tailwind config, use etherscan link from toolkit, use web3 from lib

* make app compatible with react router 6

* remove vega keys from app state and use from state from lib

* comment out crowdin script injection

* convert all buttons to use ui toolkit buttons

* remove blueprint inputs and selects and replace with ui-toolkit

* remove css resets

* tidy button styles in wallet replace splash-screen with version from ui-toolkit

* various style fixes

* tidy up proposal list

* add valid key to route children

* Set custom port and config for token e2e tests

* added env title e2e test

* started some styling fixes - nav and home route

* Added 'h-auto' to button height regex check

* Added 'h-auto' to regex check to allow desired TFE button height

* Removed scss and used tailwind for heading component

* Woff files not woof :)

* Proper nav h1 font size

* Wallet card headings

* Vega wallet button styles

* Set project to use static hosted alpha font (cors being fixed separately)

* Eth wallet button styles (unfinished)

* Home route styles

* Staking route styles and title calculation

* Rewards route styles

* Vega wallet container button style

* Eth wallet disconnect button

* Connect dialog title colour and spacing

* Splash screen layout

* Fixed a bunch of linting errors

* Used 'Object.entries' instead of 'Array.from' to create iterable from object in 'use-search-params'

* Removed use of 'any' from 'use-search-params'

* Better simplification of 'use-search-params'

* Removed package.json duplication errors, set most up-to-date version from duplicate options

* Elvis for possible undefined in 'use-add-asset-to-wallet'

* Removed redundant files

* Removed old todo

* Removed package.json redundant packages

* Added dark class for dialog h1 text colour (required as the current scss gives a wrong default for this element)

* update useAddAsset to use new provider

* Ensure Jest has required methods

* tidy up package.json

* remove ts-ignores and use casts for dynamic grid imports

* remove unused code from token-e2e

* update to latest types from react 17

* Removed vegag wallet not running component as it should be handled by wallet lib

* fix typing of contract addresses

* type cast network string to Network enum in reduce

* remove comment, issue 270 created instead

* default associated wallet amounts to zero

* update comment

* delete unused staking-overview component, add note about withTranslation types to comment

* re add proposal dates

* enable source maps for build

* add rest of env files for networks

* remove crowdin script tags from index.html

* add testing-library/jest-dom to types in test tsconfig

* setup i18n for tests so that translations are used, proposal change table test

* delete unused translation files and config

* set sentry release to use commit ref

* delete dex liquidity pages

* remove unused useVegaLPStaking hook

* use found id so no non null assertion needed

* remove mocked graphql provider

* remove commented out breadcrumb component

* add comment and link to issue for syntax highlighter changes

* fix any types in token-input, add link to ui-toolkit input changes

* dont default allowance to zero as it affects rendering logic

* fix spacing between callouts on associate page

* adjust spacing between callout for association callout

* fix alignment of ethereum splash screen

* use ethereum connect dialog state for connect dialog

* add infura provider as default

* change from infura provider to JsonRpcProvider

* remove unused Ethereum config

* add custom webpack config to inject sentry plugin

* delete commented out code for pending stake

* add comment linking input elements issue for eth-address-input

* move useEagerConnect to libs/wallet, add logic for connecting state so token app can load after connection has succeeded or failed

* remove unused storage files, update web3 connector to render children if not actively connected

Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
Sam Keen 2022-04-20 20:37:44 +01:00 committed by GitHub
parent f0e4aded3a
commit 9591687a80
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
394 changed files with 22969 additions and 1611 deletions

View File

@ -1,7 +1,7 @@
@font-face {
font-family: AlphaLyrae;
src: url('alpha-lyrae/AlphaLyrae-Medium.woff2') format('woof2'),
url('alpha-lyrae/AlphaLyrae-Medium.woff') format('woof'),
src: url('alpha-lyrae/AlphaLyrae-Medium.woff2') format('woff2'),
url('alpha-lyrae/AlphaLyrae-Medium.woff') format('woff'),
url('alpha-lyrae/AlphaLyrae-Medium.ttf') format('truetype'),
url('alpha-lyrae/AlphaLyrae-Medium.eot') format('embedded-opentype'),
url('alpha-lyrae/AlphaLyrae-Medium.otf') format('opentype');

View File

@ -0,0 +1,10 @@
{
"extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
}
]
}

View File

@ -0,0 +1,14 @@
{
"baseUrl": "http://localhost:4210",
"projectId": "et4snf",
"fileServerFolder": ".",
"fixturesFolder": "./src/fixtures",
"integrationFolder": "./src/integration",
"modifyObstructiveCode": false,
"supportFile": "./src/support/index.ts",
"pluginsFile": false,
"video": true,
"videosFolder": "../../dist/cypress/apps/token-e2e/videos",
"screenshotsFolder": "../../dist/cypress/apps/token-e2e/screenshots",
"chromeWebSecurity": false
}

View File

@ -0,0 +1,28 @@
{
"root": "apps/token-e2e",
"sourceRoot": "apps/token-e2e/src",
"projectType": "application",
"targets": {
"e2e": {
"executor": "@nrwl/cypress:cypress",
"options": {
"cypressConfig": "apps/token-e2e/cypress.json",
"devServerTarget": "token:serve"
},
"configurations": {
"production": {
"devServerTarget": "token:serve:production"
}
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/token-e2e/**/*.{js,ts}"]
}
}
},
"tags": [],
"implicitDependencies": ["token"]
}

View File

@ -0,0 +1,4 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io"
}

View File

@ -0,0 +1,12 @@
const fairgroundSet = Cypress.env('FAIRGROUND');
describe('token', () => {
beforeEach(() => cy.visit('/'));
it('should always have an header title based on environment', () => {
cy.get('.nav h1').should(
'have.text',
`${fairgroundSet ? 'Fairground token' : '$VEGA TOKEN'}`
);
});
});

View File

@ -0,0 +1 @@
import '@vegaprotocol/cypress';

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"sourceMap": false,
"outDir": "../../dist/out-tsc",
"allowJs": true,
"types": ["cypress", "node"]
},
"include": ["src/**/*.ts", "src/**/*.js"]
}

11
apps/token/.babelrc Normal file
View File

@ -0,0 +1,11 @@
{
"presets": [
[
"@nrwl/react/babel",
{
"runtime": "automatic"
}
]
],
"plugins": []
}

View File

@ -0,0 +1,16 @@
# This file is used by:
# 1. autoprefixer to adjust CSS to support the below specified browsers
# 2. babel preset-env to adjust included polyfills
#
# For additional information regarding the format and rule options, please see:
# https://github.com/browserslist/browserslist#queries
#
# If you need to support different browsers in production, you may tweak the list below.
last 1 Chrome version
last 1 Firefox version
last 2 Edge major versions
last 2 Safari major version
last 2 iOS major versions
Firefox ESR
not IE 9-11 # For IE 9-11 support, remove 'not'.

30
apps/token/.env Normal file
View File

@ -0,0 +1,30 @@
# React Environment Variables
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
# Netlify Environment Variables
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
REACT_APP_VERSION=$npm_package_version
REACT_APP_REPOSITORY_URL=$REPOSITORY_URL
REACT_APP_BRANCH=$BRANCH
REACT_APP_PULL_REQUEST=$PULL_REQUEST
REACT_APP_HEAD=$HEAD
REACT_APP_COMMIT_REF=$COMMIT_REF
REACT_APP_CONTEXT=$CONTEXT
REACT_APP_REVIEW_ID=$REVIEW_ID
REACT_APP_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
REACT_APP_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
REACT_APP_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
REACT_APP_URL=$URL
REACT_APP_DEPLOY_URL=$DEPLOY_URL
REACT_APP_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
# App configuration variables
NX_VEGA_ENV = "TESTNET"
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
NX_ETHEREUM_CHAIN_ID = 3
NX_ETHEREUM_PROVIDER_URL = "https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8"
NX_ETHERSCAN_URL = "https://ropsten.etherscan.io"
NX_FAIRGROUND = false
#Test configuration variables
CYPRESS_FAIRGROUND = false

6
apps/token/.env.devent Normal file
View File

@ -0,0 +1,6 @@
# App configuration variables
NX_VEGA_ENV = "DEVNET"
NX_VEGA_URL = "https://n04.d.vega.xyz/query"
NX_ETHEREUM_CHAIN_ID = 3
NX_ETHEREUM_PROVIDER_URL = "https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8"
NX_ETHERSCAN_URL = "https://ropsten.etherscan.io"

6
apps/token/.env.mainnet Normal file
View File

@ -0,0 +1,6 @@
# App configuration variables
NX_VEGA_ENV = "MAINNET"
NX_VEGA_URL = "https://api.token.vega.xyz/query"
NX_ETHEREUM_CHAIN_ID = 1
NX_ETHEREUM_PROVIDER_URL = "https://mainnet.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8"
NX_ETHERSCAN_URL = "https://etherscan.io"

6
apps/token/.env.stagnet1 Normal file
View File

@ -0,0 +1,6 @@
# App configuration variables
NX_VEGA_ENV = "STAGNET"
NX_VEGA_URL = "https://n03.s.vega.xyz/query"
NX_ETHEREUM_CHAIN_ID = 3
NX_ETHEREUM_PROVIDER_URL = "https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8"
NX_ETHERSCAN_URL = "https://ropsten.etherscan.io"

6
apps/token/.env.stagnet2 Normal file
View File

@ -0,0 +1,6 @@
# App configuration variables
NX_VEGA_ENV = "STAGNET2"
NX_VEGA_URL = "https://n03.stagnet2.vega.xyz/query"
NX_ETHEREUM_CHAIN_ID = 3
NX_ETHEREUM_PROVIDER_URL = "https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8"
NX_ETHERSCAN_URL = "https://ropsten.etherscan.io"

6
apps/token/.env.testnet Normal file
View File

@ -0,0 +1,6 @@
# App configuration variables
NX_VEGA_ENV = "TESTNET"
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
NX_ETHEREUM_CHAIN_ID = 3
NX_ETHEREUM_PROVIDER_URL = "https://ropsten.infura.io/v3/4f846e79e13f44d1b51bbd7ed9edefb8"
NX_ETHERSCAN_URL = "https://ropsten.etherscan.io"

18
apps/token/.eslintrc.json Normal file
View File

@ -0,0 +1,18 @@
{
"extends": ["plugin:@nrwl/nx/react", "../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}

11
apps/token/jest.config.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
displayName: 'token',
preset: '../../jest.preset.js',
transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
'^.+\\.[tj]sx?$': 'babel-jest',
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/apps/token',
setupFilesAfterEnv: ['./src/setup-tests.ts'],
};

View File

@ -0,0 +1,10 @@
const { join } = require('path');
module.exports = {
plugins: {
tailwindcss: {
config: join(__dirname, 'tailwind.config.js'),
},
autoprefixer: {},
},
};

71
apps/token/project.json Normal file
View File

@ -0,0 +1,71 @@
{
"root": "apps/token",
"sourceRoot": "apps/token/src",
"projectType": "application",
"targets": {
"build": {
"executor": "@nrwl/web:webpack",
"outputs": ["{options.outputPath}"],
"defaultConfiguration": "production",
"options": {
"compiler": "babel",
"outputPath": "dist/apps/token",
"index": "apps/token/src/index.html",
"baseHref": "/",
"main": "apps/token/src/main.tsx",
"polyfills": "apps/token/src/polyfills.ts",
"tsConfig": "apps/token/tsconfig.app.json",
"assets": ["apps/token/src/favicon.ico", "apps/token/src/assets"],
"styles": ["apps/token/src/styles.scss"],
"scripts": [],
"webpackConfig": "apps/explorer/webpack.config.js"
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "apps/token/src/environments/environment.ts",
"with": "apps/token/src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"sourceMap": true,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": false
}
}
},
"serve": {
"executor": "@nrwl/web:dev-server",
"options": {
"port": 4210,
"buildTarget": "token:build",
"hmr": true
},
"configurations": {
"production": {
"buildTarget": "token:build:production",
"hmr": false
}
}
},
"lint": {
"executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/token/**/*.{ts,tsx,js,jsx}"]
}
},
"test": {
"executor": "@nrwl/jest:jest",
"outputs": ["coverage/apps/token"],
"options": {
"jestConfig": "apps/token/jest.config.js",
"passWithNoTests": true
}
}
},
"tags": []
}

View File

@ -0,0 +1,120 @@
/* tslint:disable */
/* eslint-disable */
// @generated
// This file was automatically generated and should not be edited.
//==============================================================
// START Enums and Input Objects
//==============================================================
/**
* The various account types we have (used by collateral)
*/
export enum AccountType {
Bond = "Bond",
External = "External",
FeeInfrastructure = "FeeInfrastructure",
FeeLiquidity = "FeeLiquidity",
FeeMaker = "FeeMaker",
General = "General",
GlobalInsurance = "GlobalInsurance",
GlobalReward = "GlobalReward",
Insurance = "Insurance",
LockWithdraw = "LockWithdraw",
Margin = "Margin",
PendingTransfers = "PendingTransfers",
RewardLpReceivedFees = "RewardLpReceivedFees",
RewardMakerReceivedFees = "RewardMakerReceivedFees",
RewardMarketProposers = "RewardMarketProposers",
RewardTakerPaidFees = "RewardTakerPaidFees",
Settlement = "Settlement",
}
export enum NodeStatus {
NonValidator = "NonValidator",
Validator = "Validator",
}
/**
* Reason for the proposal being rejected by the core node
*/
export enum ProposalRejectionReason {
CloseTimeTooLate = "CloseTimeTooLate",
CloseTimeTooSoon = "CloseTimeTooSoon",
CouldNotInstantiateMarket = "CouldNotInstantiateMarket",
EnactTimeTooLate = "EnactTimeTooLate",
EnactTimeTooSoon = "EnactTimeTooSoon",
IncompatibleTimestamps = "IncompatibleTimestamps",
InsufficientTokens = "InsufficientTokens",
InvalidAsset = "InvalidAsset",
InvalidAssetDetails = "InvalidAssetDetails",
InvalidFeeAmount = "InvalidFeeAmount",
InvalidFutureMaturityTimestamp = "InvalidFutureMaturityTimestamp",
InvalidFutureProduct = "InvalidFutureProduct",
InvalidInstrumentSecurity = "InvalidInstrumentSecurity",
InvalidRiskParameter = "InvalidRiskParameter",
InvalidShape = "InvalidShape",
MajorityThresholdNotReached = "MajorityThresholdNotReached",
MarketMissingLiquidityCommitment = "MarketMissingLiquidityCommitment",
MissingBuiltinAssetField = "MissingBuiltinAssetField",
MissingCommitmentAmount = "MissingCommitmentAmount",
MissingERC20ContractAddress = "MissingERC20ContractAddress",
NetworkParameterInvalidKey = "NetworkParameterInvalidKey",
NetworkParameterInvalidValue = "NetworkParameterInvalidValue",
NetworkParameterValidationFailed = "NetworkParameterValidationFailed",
NoProduct = "NoProduct",
NoRiskParameters = "NoRiskParameters",
NoTradingMode = "NoTradingMode",
NodeValidationFailed = "NodeValidationFailed",
OpeningAuctionDurationTooLarge = "OpeningAuctionDurationTooLarge",
OpeningAuctionDurationTooSmall = "OpeningAuctionDurationTooSmall",
ParticipationThresholdNotReached = "ParticipationThresholdNotReached",
ProductMaturityIsPassed = "ProductMaturityIsPassed",
UnsupportedProduct = "UnsupportedProduct",
UnsupportedTradingMode = "UnsupportedTradingMode",
}
/**
* Various states a proposal can transition through:
* Open ->
* - Passed -> Enacted.
* - Rejected.
* Proposal can enter Failed state from any other state.
*/
export enum ProposalState {
Declined = "Declined",
Enacted = "Enacted",
Failed = "Failed",
Open = "Open",
Passed = "Passed",
Rejected = "Rejected",
WaitingForNodeVote = "WaitingForNodeVote",
}
/**
* The status of the stake linking
*/
export enum StakeLinkingStatus {
Accepted = "Accepted",
Pending = "Pending",
Rejected = "Rejected",
}
export enum VoteValue {
No = "No",
Yes = "Yes",
}
/**
* The status of a withdrawal
*/
export enum WithdrawalStatus {
Finalized = "Finalized",
Open = "Open",
Rejected = "Rejected",
}
//==============================================================
// END Enums and Input Objects
//==============================================================

View File

@ -0,0 +1,153 @@
import * as Sentry from '@sentry/react';
import { Splash } from '@vegaprotocol/ui-toolkit';
import { useVegaWallet, useEagerConnect } from '@vegaprotocol/wallet';
import { useWeb3React } from '@web3-react/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { SplashError } from './components/splash-error';
import { SplashLoader } from './components/splash-loader';
import { Flags } from './config';
import {
AppStateActionType,
useAppState,
} from './contexts/app-state/app-state-context';
import { useContracts } from './contexts/contracts/contracts-context';
import { useRefreshAssociatedBalances } from './hooks/use-refresh-associated-balances';
import { getDataNodeUrl } from './lib/get-data-node-url';
import { Connectors } from './lib/vega-connectors';
export const AppLoader = ({ children }: { children: React.ReactElement }) => {
const { t } = useTranslation();
const { account } = useWeb3React();
const { keypair } = useVegaWallet();
const { appDispatch } = useAppState();
const { token, staking, vesting } = useContracts();
const setAssociatedBalances = useRefreshAssociatedBalances();
const [balancesLoaded, setBalancesLoaded] = React.useState(false);
const vegaConnecting = useEagerConnect(Connectors);
const loaded = balancesLoaded && !vegaConnecting;
React.useEffect(() => {
const run = async () => {
try {
const [
supply,
totalAssociatedWallet,
totalAssociatedVesting,
decimals,
] = await Promise.all([
token.totalSupply(),
staking.totalStaked(),
vesting.totalStaked(),
token.decimals(),
]);
appDispatch({
type: AppStateActionType.SET_TOKEN,
decimals,
totalSupply: supply,
totalAssociated: totalAssociatedWallet.plus(totalAssociatedVesting),
});
setBalancesLoaded(true);
} catch (err) {
Sentry.captureException(err);
}
};
if (!Flags.NETWORK_DOWN) {
run();
}
}, [token, appDispatch, staking, vesting]);
React.useEffect(() => {
if (account && keypair) {
setAssociatedBalances(account, keypair.pub);
}
}, [setAssociatedBalances, account, keypair]);
React.useEffect(() => {
const { base } = getDataNodeUrl();
const networkLimitsEndpoint = new URL('/network/limits', base).href;
const statsEndpoint = new URL('/statistics', base).href;
// eslint-disable-next-line
let interval: any = null;
const getNetworkLimits = async () => {
try {
const [networkLimits, stats] = await Promise.all([
fetch(networkLimitsEndpoint).then((res) => res.json()),
fetch(statsEndpoint).then((res) => res.json()),
]);
const restoreBlock = Number(
networkLimits.networkLimits.bootstrapBlockCount
);
const currentBlock = Number(stats.statistics.blockHeight);
if (currentBlock <= restoreBlock) {
appDispatch({
type: AppStateActionType.SET_BANNER_MESSAGE,
message: t('networkRestoring', {
bootstrapBlockCount: restoreBlock,
}),
});
if (!interval) {
startPoll();
}
} else {
appDispatch({
type: AppStateActionType.SET_BANNER_MESSAGE,
message: '',
});
if (interval) {
stopPoll();
}
}
} catch (err) {
Sentry.captureException(err);
}
};
const stopPoll = () => {
clearInterval(interval);
interval = null;
};
const startPoll = () => {
interval = setInterval(() => {
getNetworkLimits();
}, 10000);
};
// Only begin polling if network limits flag is set, as this is a new API not yet on mainnet 7/3/22
if (Flags.NETWORK_LIMITS) {
getNetworkLimits();
}
return () => {
stopPoll();
};
}, [appDispatch, t]);
if (Flags.NETWORK_DOWN) {
return (
<Splash>
<SplashError />
</Splash>
);
}
if (!loaded) {
return (
<Splash>
<SplashLoader />
</Splash>
);
}
return children;
};

14
apps/token/src/app.scss Normal file
View File

@ -0,0 +1,14 @@
@import "./styles/colors";
.app {
max-width: 1300px;
margin: 0 auto;
display: grid;
grid-template-rows: min-content 1fr min-content;
min-height: 100%;
@media (min-width: 960px) {
border-left: 1px solid $white;
border-right: 1px solid $white;
}
}

58
apps/token/src/app.tsx Normal file
View File

@ -0,0 +1,58 @@
import './i18n';
import './app.scss';
import React from 'react';
import { BrowserRouter as Router } from 'react-router-dom';
import { AppLoader } from './app-loader';
import { AppBanner } from './components/app-banner';
import { AppFooter } from './components/app-footer';
import { BalanceManager } from './components/balance-manager';
import { EthWallet } from './components/eth-wallet';
import { GraphQlProvider } from './components/graphql-provider';
import { TemplateSidebar } from './components/page-templates/template-sidebar';
import { TransactionModal } from './components/transactions-modal';
import { VegaWallet } from './components/vega-wallet';
import { Web3Connector } from './components/web3-connector';
import { AppStateProvider } from './contexts/app-state/app-state-provider';
import { ContractsProvider } from './contexts/contracts/contracts-provider';
import { AppRouter } from './routes';
import { Web3Provider } from '@vegaprotocol/web3';
import { Connectors } from './lib/web3-connectors';
import { VegaWalletDialogs } from './components/vega-wallet-dialogs';
import { VegaWalletProvider } from '@vegaprotocol/wallet';
function App() {
const sideBar = React.useMemo(() => [<EthWallet />, <VegaWallet />], []);
return (
<GraphQlProvider>
<Router>
<AppStateProvider>
<Web3Provider connectors={Connectors}>
<Web3Connector>
<VegaWalletProvider>
<ContractsProvider>
<AppLoader>
<BalanceManager>
<div className="app dark">
<AppBanner />
<TemplateSidebar sidebar={sideBar}>
<AppRouter />
</TemplateSidebar>
<AppFooter />
</div>
<VegaWalletDialogs />
<TransactionModal />
</BalanceManager>
</AppLoader>
</ContractsProvider>
</VegaWalletProvider>
</Web3Connector>
</Web3Provider>
</AppStateProvider>
</Router>
</GraphQlProvider>
);
}
export default App;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,20 @@
{
"short_name": "Mainnet Stats",
"name": "Vega Mainnet statistics",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,22 @@
.add-locked-token-address {
p {
display: flex;
justify-content: center;
}
&__or-divider {
display: flex;
margin: 12px 0px;
hr {
flex: 1;
margin-top: 12px;
&:first-child {
margin-right: 12px;
}
&:last-child {
margin-left: 12px;
}
}
}
}

View File

@ -0,0 +1,48 @@
import './add-locked-token.scss';
import { useTranslation } from 'react-i18next';
import { ADDRESSES } from '../../config';
import { useAddAssetSupported } from '../../hooks/use-add-asset-to-wallet';
import vegaVesting from '../../images/vega_vesting.png';
import { AddTokenButtonLink } from '../add-token-button/add-token-button';
import { Callout } from '@vegaprotocol/ui-toolkit';
export const AddLockedTokenAddress = () => {
const { t } = useTranslation();
const addSupported = useAddAssetSupported();
return (
<Callout
title={t(
'Keep track of locked tokens in your wallet with the VEGA (VESTING) token.'
)}
>
<div className="add-locked-token-address">
{addSupported ? (
<>
<p>
<AddTokenButtonLink
address={ADDRESSES.lockedAddress}
symbol="VEGA🔒"
decimals={18}
image={vegaVesting}
/>
</p>
<div className="add-locked-token-address__or-divider">
<hr />
{t('Or')} <hr />
</div>
</>
) : null}
<p className="mb-0">
{t(
'The token address is {{address}}. Hit the add token button in your ERC20 wallet and enter this address.',
{
address: ADDRESSES.lockedAddress,
}
)}
</p>
</div>
</Callout>
);
};

View File

@ -0,0 +1 @@
export { AddLockedTokenAddress } from "./add-locked-token";

View File

@ -0,0 +1,68 @@
import { Button } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import { useAddAssetToWallet } from '../../hooks/use-add-asset-to-wallet';
export const AddTokenButtonLink = ({
address,
symbol,
decimals,
image,
}: {
address: string;
symbol: string;
decimals: number;
image: string;
}) => {
const { t } = useTranslation();
const { add, addSupported } = useAddAssetToWallet(
address,
symbol,
decimals,
image
);
if (!addSupported) {
return null;
}
return (
<Button variant="inline-link" className="add-token-button" onClick={add}>
{t('addTokenToWallet')}
</Button>
);
};
export const AddTokenButton = ({
address,
symbol,
decimals,
image,
size = 32,
className = '',
}: {
address: string;
symbol: string;
decimals: number;
image: string;
size?: number;
className?: string;
}) => {
const { add, addSupported } = useAddAssetToWallet(
address,
symbol,
decimals,
image
);
if (!addSupported) {
return null;
}
return (
<Button variant="inline-link" className="add-token-button" onClick={add}>
<img
className={className}
style={{ width: size, height: size }}
alt="token-logo"
src={image}
/>
</Button>
);
};

View File

@ -0,0 +1 @@
export { AddTokenButton, AddTokenButtonLink } from "./add-token-button";

View File

@ -0,0 +1,19 @@
@import "../../styles/colors";
.app-banner {
background: $white;
padding: 10px;
color: $text-color-inverse;
p {
margin: 0;
}
&__icon {
position: relative;
top: 1px;
color: $vega-red3;
font-size: 14px;
margin-right: 5px;
}
}

View File

@ -0,0 +1,24 @@
import "./app-banner.scss";
import { useAppState } from "../../contexts/app-state/app-state-context";
import { Error } from "../icons";
export const AppBanner = () => {
const {
appState: { bannerMessage },
} = useAppState();
// Return empty div so that grid placement remains correct
if (!bannerMessage) return <div />;
return (
<div className="app-banner" role="alert">
<p>
<span className="app-banner__icon">
<Error />
</span>
{bannerMessage}
</p>
</div>
);
};

View File

@ -0,0 +1 @@
export * from "./app-banner";

View File

@ -0,0 +1,12 @@
.app-footer {
padding: 20px;
font-size: 14px;
> p {
margin-bottom: 7px;
&:last-child {
margin: 0;
}
}
}

View File

@ -0,0 +1,24 @@
import './app-footer.scss';
import { Trans } from 'react-i18next';
import { Links } from '../../config';
export const AppFooter = () => {
return (
<footer className="app-footer">
<p>Version: {process.env['NX_COMMIT_REF'] || 'development'}</p>
<p>
<Trans
i18nKey="footerLinksText"
components={{
/* eslint-disable */
feedbackLink: <a href={Links.FEEDBACK} />,
githubLink: <a href={Links.GITHUB} />,
/* eslint-enable */
}}
/>
</p>
</footer>
);
};

View File

@ -0,0 +1 @@
export * from "./app-footer";

View File

@ -0,0 +1,70 @@
import * as Sentry from "@sentry/react";
import { useWeb3React } from "@web3-react/core";
import React from "react";
import { ADDRESSES } from "../../config";
import {
AppStateActionType,
useAppState,
} from "../../contexts/app-state/app-state-context";
import { useContracts } from "../../contexts/contracts/contracts-context";
import { useGetAssociationBreakdown } from "../../hooks/use-get-association-breakdown";
import { useGetUserTrancheBalances } from "../../hooks/use-get-user-tranche-balances";
import { BigNumber } from "../../lib/bignumber";
export const BalanceManager = ({ children }: any) => {
const contracts = useContracts();
const { account } = useWeb3React();
const { appDispatch } = useAppState();
const getUserTrancheBalances = useGetUserTrancheBalances(
account || "",
contracts?.vesting
);
const getAssociationBreakdown = useGetAssociationBreakdown(
account || "",
contracts?.staking,
contracts?.vesting
);
// update balances on connect to Ethereum
React.useEffect(() => {
const updateBalances = async () => {
if (!account) return;
try {
const [balance, walletBalance, lien, allowance] = await Promise.all([
contracts.vesting.getUserBalanceAllTranches(account),
contracts.token.balanceOf(account),
contracts.vesting.getLien(account),
contracts.token.allowance(account, ADDRESSES.stakingBridge),
]);
appDispatch({
type: AppStateActionType.UPDATE_ACCOUNT_BALANCES,
balance: new BigNumber(balance),
walletBalance,
lien,
allowance,
});
} catch (err) {
Sentry.captureException(err);
}
};
updateBalances();
}, [appDispatch, contracts?.token, contracts?.vesting, account]);
// This use effect hook is very expensive and is kept separate to prevent expensive reloading of data.
React.useEffect(() => {
if (account) {
getUserTrancheBalances();
}
}, [account, getUserTrancheBalances]);
React.useEffect(() => {
if (account) {
getAssociationBreakdown();
}
}, [account, getAssociationBreakdown]);
return children;
};

View File

@ -0,0 +1 @@
export * from "./balance-manager";

View File

@ -0,0 +1,20 @@
@import "../../styles/colors";
.bullet-header {
font-size: 16px;
border-top: 1px solid $white;
padding: 8px 0 25px 0;
text-transform: uppercase;
font-weight: 300;
margin: 35px 0 0 0;
width: 100%;
&::before {
content: "";
display: inline-block;
width: 12px;
height: 12px;
margin-right: 10px;
background-color: $white;
}
}

View File

@ -0,0 +1,17 @@
import "./bullet-header.scss";
import React from "react";
interface BulletHeaderProps {
tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
children: React.ReactNode;
style?: React.CSSProperties;
}
export const BulletHeader = ({ tag, children, style }: BulletHeaderProps) => {
return React.createElement(
tag,
{ className: "bullet-header", style },
children
);
};

View File

@ -0,0 +1 @@
export * from "./bullet-header";

View File

@ -0,0 +1,5 @@
@import "../../styles/colors";
.connected-vega-key {
color: $white;
}

View File

@ -0,0 +1,17 @@
import "./connected-vega-key.scss";
import { useTranslation } from "react-i18next";
import { ConnectToVega } from "../../routes/staking/connect-to-vega";
export const ConnectedVegaKey = ({ pubKey }: { pubKey: string | null }) => {
const { t } = useTranslation();
return (
<section className="connected-vega-key">
<strong data-testid="connected-vega-key-label">
{pubKey ? t("Connected Vega key") : <ConnectToVega />}
</strong>
<p data-testid="connected-vega-key">{pubKey}</p>
</section>
);
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,65 @@
import { MenuItem } from '@blueprintjs/core';
import type { ItemPredicate } from '@blueprintjs/select';
import { Suggest } from '@blueprintjs/select';
import type { ICountry } from '../../routes/claim/claim-form';
import countryData from './country-data';
const CountrySuggest = Suggest.ofType<ICountry>();
export const filterCountry: ItemPredicate<ICountry> = (
query,
country,
_index,
exactMatch
) => {
const normalizedTitle = country.name.toLowerCase();
const normalizedQuery = query.toLowerCase();
if (exactMatch) {
return normalizedTitle === normalizedQuery;
} else if (query.length === 2) {
return normalizedQuery === country.code.toLowerCase();
} else {
return normalizedTitle.indexOf(normalizedQuery) >= 0;
}
};
export interface CountrySelectorProps {
onSelectCountry: (countryCode: string) => void;
code: string | null;
}
export const CountrySelector = ({
onSelectCountry,
code,
}: CountrySelectorProps) => {
return (
<div data-testid="country-selector">
<CountrySuggest
selectedItem={
countryData.find((c) => c.code === code) || countryData[0]
}
items={countryData}
itemRenderer={(item, { handleClick, modifiers }) => (
<MenuItem
data-testid={item.code}
key={item.code}
text={item.name}
active={modifiers.active}
disabled={modifiers.disabled}
onClick={handleClick}
/>
)}
onItemSelect={(item) => {
onSelectCountry(item.code);
}}
inputValueRenderer={(item) => item.name}
popoverProps={{ minimal: true }}
noResults={<MenuItem disabled={true} text="No results." />}
itemPredicate={filterCountry}
fill={true}
/>
</div>
);
};

View File

@ -0,0 +1 @@
export { CountrySelector } from "./country-selector";

View File

@ -0,0 +1,50 @@
@import '../../styles/colors';
.epoch-countdown {
h3 {
font-size: 16px;
font-weight: normal;
margin: 0 0 5px;
}
p {
font-size: 12px;
margin: 5px 0 0;
}
.bp3-progress-bar {
border: 1px solid $white;
border-radius: 0;
height: 21px;
.bp3-progress-meter {
border-radius: 0;
.bp3-dark & {
background-color: $white;
}
}
}
&__title {
display: flex;
h3:first-child {
flex: 1;
}
}
&__arrow {
flex: 1;
text-align: center;
img {
display: inline-block;
width: 5px;
transform: rotate(180deg);
}
}
&__time-range {
display: flex;
}
}

View File

@ -0,0 +1,93 @@
import './epoch-countdown.scss';
import { Intent, ProgressBar } from '@blueprintjs/core';
import { format, formatDistanceStrict } from 'date-fns';
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import arrow from '../../images/back.png';
import { DATE_FORMAT_DETAILED } from '../../lib/date-formats';
export interface EpochCountdownProps {
id: string;
startDate: Date;
endDate: Date;
containerClass?: string;
}
export function EpochCountdown({
id,
startDate,
endDate,
containerClass,
}: EpochCountdownProps) {
const { t } = useTranslation();
const [now, setNow] = React.useState(Date.now());
// number between 0 and 1 for percentage progress
const progress = React.useMemo(() => {
const start = startDate.getTime();
const end = endDate.getTime();
if (now > end) {
return 1;
}
// round it to make testing easier
return Number(((now - start) / (end - start)).toFixed(2));
}, [startDate, endDate, now]);
// format end date into readable 'time until' text
const endsIn = React.useMemo(() => {
if (endDate.getTime() > now) {
return formatDistanceStrict(now, endDate);
}
return 0;
}, [now, endDate]);
// start interval updating current time stamp until
// its passed the end date
React.useEffect(() => {
const interval = setInterval(() => {
const d = Date.now();
setNow(d);
if (d > endDate.getTime()) {
clearInterval(interval);
}
}, 1000);
return () => clearInterval(interval);
}, [endDate]);
return (
<div
data-testid="epoch-countdown"
className={`${containerClass} epoch-countdown`}
>
<div className="epoch-countdown__title">
<h3>
{t('Epoch')} {id}
</h3>
<p>
{endsIn
? t('Next epoch in {{endText}}', { endText: endsIn })
: t('Awaiting next epoch')}
</p>
</div>
<ProgressBar
animate={false}
value={progress}
stripes={false}
intent={Intent.NONE}
/>
<div className="epoch-countdown__time-range">
<p>{format(startDate, DATE_FORMAT_DETAILED)}</p>
<div className="epoch-countdown__arrow">
<img alt="arrow" src={arrow} />
</div>
<p>{format(endDate, DATE_FORMAT_DETAILED)}</p>
</div>
</div>
);
}

View File

@ -0,0 +1 @@
export * from "./epoch-countdown";

View File

@ -0,0 +1,33 @@
import { Button } from '@vegaprotocol/ui-toolkit';
import { useTranslation } from 'react-i18next';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
interface EthConnectPrompProps {
children?: React.ReactNode;
}
export const EthConnectPrompt = ({ children }: EthConnectPrompProps) => {
const { t } = useTranslation();
const { appDispatch } = useAppState();
return (
<>
{children}
<Button
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
className="fill"
data-testid="connect-to-eth-btn"
>
{t('connectEthWallet')}
</Button>
</>
);
};

View File

@ -0,0 +1 @@
export * from "./eth-connect-promp";

View File

@ -0,0 +1,10 @@
.eth-wallet-container {
flex-direction: row;
display: inline-flex;
align-items: center;
justify-content: center;
svg.icon {
width: 1.5rem;
}
}

View File

@ -0,0 +1,42 @@
import './eth-wallet-container.scss';
import { useWeb3React } from '@web3-react/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import { Ethereum } from '../icons';
import { Button } from '@vegaprotocol/ui-toolkit';
interface EthWalletContainerProps {
children: (address: string) => React.ReactElement;
}
export const EthWalletContainer = ({ children }: EthWalletContainerProps) => {
const { t } = useTranslation();
const { appDispatch } = useAppState();
const { account } = useWeb3React();
if (!account) {
return (
<Button
className="eth-wallet-container fill"
data-testid="connect-to-eth-btn"
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
>
<div>{t('connectEthWallet')}</div>
<Ethereum />
</Button>
);
}
return children(account);
};

View File

@ -0,0 +1 @@
export * from "./eth-wallet-container";

View File

@ -0,0 +1,25 @@
@import "../../styles/colors";
@import "../../styles/fonts";
.eth-wallet {
&__pending-tx-button {
display: flex;
gap: 5px;
justify-content: space-between;
padding: 5px;
background: $black;
color: $white;
flex-wrap: nowrap;
white-space: nowrap;
&:hover {
background: $black;
}
}
&__curr-key {
font-family: $font-mono;
padding: 0 8px;
text-align: right;
}
}

View File

@ -0,0 +1,244 @@
import './eth-wallet.scss';
import { useWeb3React } from '@web3-react/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import { Colors } from '../../config';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import { usePendingTransactions } from '../../hooks/use-pending-transactions';
import vegaVesting from '../../images/vega_vesting.png';
import vegaWhite from '../../images/vega_white.png';
import { BigNumber } from '../../lib/bignumber';
import { truncateMiddle } from '../../lib/truncate-middle';
import { Routes } from '../../routes/router-config';
import { LockedProgress } from '../locked-progress';
import {
WalletCard,
WalletCardActions,
WalletCardAsset,
WalletCardContent,
WalletCardHeader,
WalletCardRow,
} from '../wallet-card';
import { Button, Loader } from '@vegaprotocol/ui-toolkit';
const removeLeadingAddressSymbol = (key: string) => {
if (key && key.length > 2 && key.slice(0, 2) === '0x') {
return truncateMiddle(key.substring(2));
}
return truncateMiddle(key);
};
const AssociatedAmounts = ({
associations,
notAssociated,
}: {
associations: { [key: string]: BigNumber };
notAssociated: BigNumber;
}) => {
const { t } = useTranslation();
const vestingAssociationByVegaKey = React.useMemo(
() =>
Object.entries(associations).filter(([, amount]) =>
amount.isGreaterThan(0)
),
[associations]
);
const associationAmounts = React.useMemo(() => {
const totals = vestingAssociationByVegaKey.map(([, amount]) => amount);
const associated = BigNumber.sum.apply(null, [new BigNumber(0), ...totals]);
return {
total: associated.plus(notAssociated),
associated,
notAssociated,
};
}, [notAssociated, vestingAssociationByVegaKey]);
return (
<>
<LockedProgress
locked={associationAmounts.associated}
unlocked={associationAmounts.notAssociated}
total={associationAmounts.total}
leftLabel={t('associated')}
rightLabel={t('notAssociated')}
leftColor={Colors.WHITE}
rightColor={Colors.BLACK}
light={true}
/>
{vestingAssociationByVegaKey.length ? (
<>
<hr style={{ borderStyle: 'dashed', color: Colors.TEXT }} />
<WalletCardRow label="Associated with Vega keys" bold={true} />
{vestingAssociationByVegaKey.map(([key, amount]) => {
return (
<WalletCardRow
key={key}
label={removeLeadingAddressSymbol(key)}
value={amount}
/>
);
})}
</>
) : null}
</>
);
};
const ConnectedKey = () => {
const { t } = useTranslation();
const { appState } = useAppState();
const { walletBalance, totalLockedBalance, totalVestedBalance } = appState;
const totalInVestingContract = React.useMemo(() => {
return totalLockedBalance.plus(totalVestedBalance);
}, [totalLockedBalance, totalVestedBalance]);
const notAssociatedInContract = React.useMemo(() => {
const totals = Object.values(
appState.associationBreakdown.vestingAssociations
);
const associated = BigNumber.sum.apply(null, [new BigNumber(0), ...totals]);
return totalInVestingContract.minus(associated);
}, [
appState.associationBreakdown.vestingAssociations,
totalInVestingContract,
]);
const walletWithAssociations = React.useMemo(() => {
const totals = Object.values(
appState.associationBreakdown.stakingAssociations
);
const associated = BigNumber.sum.apply(null, [new BigNumber(0), ...totals]);
return walletBalance.plus(associated);
}, [appState.associationBreakdown.stakingAssociations, walletBalance]);
return (
<>
{totalVestedBalance.plus(totalLockedBalance).isEqualTo(0) ? null : (
<>
<WalletCardAsset
image={vegaVesting}
decimals={appState.decimals}
name="VEGA"
symbol="In vesting contract"
balance={totalInVestingContract}
/>
<LockedProgress
locked={totalLockedBalance}
unlocked={totalVestedBalance}
total={totalVestedBalance.plus(totalLockedBalance)}
leftLabel={t('Locked')}
rightLabel={t('Unlocked')}
light={true}
/>
</>
)}
{!Object.keys(appState.associationBreakdown.vestingAssociations)
.length ? null : (
<AssociatedAmounts
associations={appState.associationBreakdown.vestingAssociations}
notAssociated={notAssociatedInContract}
/>
)}
<WalletCardAsset
image={vegaWhite}
decimals={appState.decimals}
name="VEGA"
symbol="In Wallet"
balance={walletWithAssociations}
/>
{!Object.keys(
appState.associationBreakdown.stakingAssociations
) ? null : (
<AssociatedAmounts
associations={appState.associationBreakdown.stakingAssociations}
notAssociated={walletBalance}
/>
)}
<WalletCardActions>
<Link style={{ flex: 1 }} to={`${Routes.STAKING}/associate`}>
<Button variant="primary" className="h-auto py-12 w-full">
{t('associate')}
</Button>
</Link>
<Link style={{ flex: 1 }} to={`${Routes.STAKING}/disassociate`}>
<Button variant="primary" className="h-auto py-12 w-full">
{t('disassociate')}
</Button>
</Link>
</WalletCardActions>
</>
);
};
export const EthWallet = () => {
const { t } = useTranslation();
const { appDispatch } = useAppState();
const { account, connector } = useWeb3React();
const pendingTxs = usePendingTransactions();
return (
<WalletCard>
<WalletCardHeader>
<h1 className="text-h3">{t('ethereumKey')}</h1>
{account && (
<div className="eth-wallet__curr-key">
<div>{truncateMiddle(account)}</div>
{pendingTxs && (
<div>
<Button
className="eth-wallet__pending-tx-button"
data-testid="pending-transactions-btn"
onClick={() =>
appDispatch({
type: AppStateActionType.SET_TRANSACTION_OVERLAY,
isOpen: true,
})
}
>
<Loader size="small" />
{t('pendingTransactions')}
</Button>
</div>
)}
</div>
)}
</WalletCardHeader>
<WalletCardContent>
{account ? (
<ConnectedKey />
) : (
<Button
className="fill button-secondary--inverted"
onClick={() =>
appDispatch({
type: AppStateActionType.SET_ETH_WALLET_OVERLAY,
isOpen: true,
})
}
data-test-id="connect-to-eth-wallet-button"
>
{t('connectEthWalletToAssociate')}
</Button>
)}
{account && (
<WalletCardActions>
<button
className="mt-4 underline"
onClick={() => connector.deactivate()}
>
{t('disconnect')}
</button>
</WalletCardActions>
)}
</WalletCardContent>
</WalletCard>
);
};

View File

@ -0,0 +1 @@
export * from "./eth-wallet";

View File

@ -0,0 +1,12 @@
import { ApolloProvider } from "@apollo/client";
import React from "react";
import { client } from "../../lib/apollo-client";
export const GraphQlProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
return <ApolloProvider client={client}>{children}</ApolloProvider>;
};

View File

@ -0,0 +1 @@
export * from "./graphql-provider";

View File

@ -0,0 +1,15 @@
export interface HeadingProps {
title?: string;
}
export const Heading = ({ title }: HeadingProps) => {
if (!title) return null;
return (
<header className="my-0 mx-auto">
<h1 className="font-alpha font-normal text-h3 uppercase m-0 mb-4">
{title}
</h1>
</header>
);
};

View File

@ -0,0 +1 @@
export { Heading } from "./heading";

View File

@ -0,0 +1,8 @@
export const Error = () => (
<svg className="icon" viewBox="0 0 16 16">
<path
d="M7.99-0.01c-4.42,0-8,3.58-8,8s3.58,8,8,8s8-3.58,8-8S12.41-0.01,7.99-0.01z
M8.99,12.99h-2v-2h2V12.99z M8.99,9.99h-2v-7h2V9.99z"
/>
</svg>
);

View File

@ -0,0 +1,16 @@
export function Ethereum() {
return (
<svg
data-testid="icon-ethereum"
className="icon icon-ethereum"
viewBox="0 0 50 50"
>
<path d="M25.3999 0.160156L40.1999 24.6102L25.4499 18.3102L25.3999 0.160156Z" />
<path d="M10.24 24.6102L25 0.160156L25.05 18.3102L10.24 24.6102Z" />
<path d="M25 34.1305L10.24 25.1105L25.05 18.8105L25 34.1305Z" />
<path d="M40.1999 25.1105L25.4499 18.8105L25.3999 34.1305L40.1999 25.1105Z" />
<path d="M25.3999 37.23L40.1999 28.46L25.3999 49.31V37.23Z" />
<path d="M25 37.23L10.24 28.46L25 49.31V37.23Z" />
</svg>
);
}

View File

@ -0,0 +1,10 @@
export const HandUp = () => (
<svg className="icon" viewBox="0 0 16 16">
<path
d="M13.65,6.19c-0.34,0-0.64,0.11-0.88,0.29C12.6,6,12.09,5.64,11.48,5.64
c-0.41,0-0.78,0.16-1.03,0.42c-0.23-0.37-0.67-0.63-1.19-0.63c-0.57,0-1.05,0.31-1.25,0.74L8,5.55V1.5C8,0.67,7.33,0,6.5,0
S5,0.67,5,1.5v6.61C4.42,7.7,3.45,6.9,2.52,6.81C0.96,6.67,0.7,7.88,1.28,8.13c1.54,0.67,2.99,2.68,3.7,3.95
C5.89,14.05,6.07,16,9.86,16c2.09,0,3.43-0.61,4.22-2.12C14.72,12.64,15,10.79,15,8.17V7.38C15,6.73,14.4,6.19,13.65,6.19z"
/>
</svg>
);

View File

@ -0,0 +1,4 @@
svg.icon {
fill: currentColor;
width: 1em;
}

View File

@ -0,0 +1,6 @@
import "./icons.scss";
export * from "./error";
export * from "./tick";
export * from "./hand-up";
export * from "./ethereum";

View File

@ -0,0 +1,9 @@
export const Tick = () => (
<svg className="icon" viewBox="0 0 16 16">
<path
d="M14,3c-0.28,0-0.53,0.11-0.71,0.29L6,10.59L2.71,7.29C2.53,7.11,2.28,7,2,7
C1.45,7,1,7.45,1,8c0,0.28,0.11,0.53,0.29,0.71l4,4C5.47,12.89,5.72,13,6,13s0.53-0.11,0.71-0.29l8-8C14.89,4.53,15,4.28,15,4
C15,3.45,14.55,3,14,3z"
/>
</svg>
);

View File

@ -0,0 +1 @@
export { KeyValueTable, KeyValueTableRow } from './key-value-table'

View File

@ -0,0 +1,85 @@
@import "../../styles/colors";
@import "../../styles/fonts";
$ns: "key-value-table";
.#{$ns}__header,
.#{$ns}__footer {
margin: 10px 0 5px;
}
.#{$ns}__header {
font-size: 16px;
font-weight: 500;
margin-bottom: 7px;
}
.#{$ns}__row {
@media (max-width: 640px) {
display: flex;
flex-direction: column;
}
}
.#{$ns} {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
margin-bottom: 10px;
word-break: break-all;
tr {
border-bottom: 1px solid $white;
&:first-child {
border-top: 1px solid $white;
}
}
th {
word-break: break-word;
text-align: left;
font-weight: 500;
color: $white;
text-transform: uppercase;
}
th,
td {
vertical-align: top;
padding: 5px;
}
td {
color: $text-color;
text-align: right;
}
.bp3-tag {
margin: 0 5px 5px 0;
}
&.#{$ns}--numerical {
td {
font-family: $font-mono;
& button {
font-family: $font-main;
}
}
}
&.#{$ns}--muted {
tr {
border-color: $gray1;
&:first-child {
border-top: none;
}
&:last-child {
border-bottom: none;
}
}
}
}

View File

@ -0,0 +1,66 @@
import { render, screen } from "@testing-library/react";
import { KeyValueTable, KeyValueTableRow } from "./key-value-table";
const props = {
title: "Title",
};
it("Renders the correct elements", () => {
const { container } = render(
<KeyValueTable {...props}>
<KeyValueTableRow>
<td>My label</td>
<td>My value</td>
</KeyValueTableRow>
<KeyValueTableRow>
<td>My label 2</td>
<td>My value 2</td>
</KeyValueTableRow>
</KeyValueTable>
);
expect(screen.getByText(props.title)).toBeInTheDocument();
expect(container.querySelector(".key-value-table")).toBeInTheDocument();
expect(container.querySelectorAll(".key-value-table__row")).toHaveLength(2);
const rows = container.querySelectorAll(".key-value-table__row");
// Row 1
expect(rows[0].firstChild).toHaveTextContent("My label");
expect(rows[0].children[1]).toHaveTextContent("My value");
// Row 2
expect(rows[1].firstChild).toHaveTextContent("My label 2");
expect(rows[1].children[1]).toHaveTextContent("My value 2");
});
it("Applies numeric class if prop is passed", () => {
render(
<KeyValueTable numerical={true} {...props}>
<KeyValueTableRow>
<td>My label</td>
<td>My value</td>
</KeyValueTableRow>
</KeyValueTable>
);
expect(screen.getByTestId("key-value-table")).toHaveClass(
"key-value-table--numerical"
);
});
it("Applies muted class if prop is passed", () => {
render(
<KeyValueTable muted={true} {...props}>
<KeyValueTableRow>
<td>My label</td>
<td>My value</td>
</KeyValueTableRow>
</KeyValueTable>
);
expect(screen.getByTestId("key-value-table")).toHaveClass(
"key-value-table--muted"
);
});

View File

@ -0,0 +1,58 @@
import "./key-value-table.scss";
import * as React from "react";
export interface KeyValueTableProps
extends React.HTMLAttributes<HTMLTableElement> {
title?: string;
numerical?: boolean; // makes all values monospace
children: React.ReactNode;
muted?: boolean;
}
export const KeyValueTable = ({
title,
numerical,
children,
muted,
className,
...rest
}: KeyValueTableProps) => {
return (
<React.Fragment>
{title && <h3 className="key-value-table__header">{title}</h3>}
<table
data-testid="key-value-table"
{...rest}
className={`key-value-table ${className ? className : ""} ${
numerical ? "key-value-table--numerical" : ""
}
${muted ? "key-value-table--muted" : ""}`}
>
<tbody>{children}</tbody>
</table>
</React.Fragment>
);
};
export interface KeyValueTableRowProps
extends React.HTMLAttributes<HTMLTableRowElement> {
children: [React.ReactNode, React.ReactNode];
className?: string;
}
export const KeyValueTableRow = ({
children,
className,
...rest
}: KeyValueTableRowProps) => {
return (
<tr
{...rest}
className={`key-value-table__row ${className ? className : ""}`}
>
{children[0]}
{children[1]}
</tr>
);
};

View File

@ -0,0 +1 @@
export * from "./loader";

View File

@ -0,0 +1,20 @@
.loader {
display: flex;
flex-flow: row wrap;
width: 15px;
height: 15px;
span {
display: block;
width: 5px;
height: 5px;
background: white;
opacity: 0;
}
&--dark {
span {
background: black;
}
}
}

View File

@ -0,0 +1,35 @@
import "./loader.scss";
import React from "react";
interface LoaderProps {
invert?: boolean;
}
export const Loader = ({ invert = false }: LoaderProps) => {
const [, forceRender] = React.useState(false);
const className = ["loader", invert ? "loader--dark" : ""].join(" ");
React.useEffect(() => {
const interval = setInterval(() => {
forceRender((x) => !x);
}, 100);
return () => clearInterval(interval);
}, []);
return (
<span className={className}>
{new Array(9).fill(null).map((_, i) => {
return (
<span
key={i}
style={{
opacity: Math.random() > 0.5 ? 1 : 0,
}}
/>
);
})}
</span>
);
};

View File

@ -0,0 +1 @@
export { LockedProgress } from "./locked-progress";

View File

@ -0,0 +1,59 @@
@import "../../styles/colors";
@import "../../styles/fonts";
.tranche-item {
&__progress {
border-left: $white;
border-left-style: solid;
border-left-width: 1px;
border-right: $white;
border-right-style: solid;
border-right-width: 1px;
&-contents {
display: flex;
justify-content: space-between;
gap: 0 5px;
font-family: $font-mono;
padding: 2px 5px 2px 5px;
color: $text-color-deemphasise;
&-indicator {
display: inline-block;
width: 12px;
height: 12px;
border: 1px solid $black;
&--left {
margin-right: 8px;
}
&--right {
margin-left: 8px;
}
}
&--light {
gap: 0;
padding: 2px 0;
color: $black;
}
}
}
&__progress-bar {
display: flex;
&--light {
border: 1px solid $black;
}
&--locked {
height: 16px;
}
&--unlocked {
height: 16px;
}
}
}

View File

@ -0,0 +1,98 @@
import './locked-progress.scss';
import React from 'react';
import { Colors } from '../../config';
import { formatNumber } from '../../lib/format-number';
import type { BigNumber } from '../../lib/bignumber';
export interface LockedProgressProps {
total: BigNumber;
locked: BigNumber;
unlocked: BigNumber;
leftLabel: string;
rightLabel: string;
leftColor?: string;
rightColor?: string;
light?: boolean;
}
export const LockedProgress = ({
total,
locked,
unlocked,
leftLabel,
rightLabel,
leftColor = Colors.PINK,
rightColor = Colors.GREEN,
light = false,
}: LockedProgressProps) => {
const lockedPercentage = React.useMemo(() => {
return locked.div(total).times(100);
}, [total, locked]);
const unlockedPercentage = React.useMemo(() => {
return unlocked.div(total).times(100);
}, [total, unlocked]);
return (
<div className="tranche-item__progress">
<div
className={`tranche-item__progress-bar ${
light ? 'tranche-item__progress-bar--light' : ''
}`}
>
<div
className="tranche-item__progress-bar--locked"
style={{
flex: isNaN(lockedPercentage.toNumber())
? 0
: lockedPercentage.toNumber(),
backgroundColor: leftColor,
}}
></div>
<div
className="tranche-item__progress-bar--unlocked"
style={{
flex: isNaN(unlockedPercentage.toNumber())
? 0
: unlockedPercentage.toNumber(),
backgroundColor: rightColor,
}}
></div>
</div>
<div
className={`tranche-item__progress-contents ${
light ? 'tranche-item__progress-contents--light' : ''
}`}
>
<span>
<div
className="tranche-item__progress-contents-indicator tranche-item__progress-contents-indicator--left"
style={{
backgroundColor: leftColor,
}}
></div>
{leftLabel}
</span>
<span>
{rightLabel}
<div
className="tranche-item__progress-contents-indicator tranche-item__progress-contents-indicator--right"
style={{
backgroundColor: rightColor,
}}
></div>
</span>
</div>
<div
className={`tranche-item__progress-contents ${
light ? 'tranche-item__progress-contents--light' : ''
}`}
>
<span>{formatNumber(locked, 2)}</span>
<span>{formatNumber(unlocked, 2)}</span>
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export * from "./modal";

View File

@ -0,0 +1,59 @@
@import "../../styles/colors";
.modal {
left: calc(50vw - 200px);
margin: 10vh 0;
top: 0;
width: 400px;
background: $white;
color: $black;
h1,
h2,
h3,
h4,
h5,
h6,
a {
color: $black;
}
a:hover {
color: $black;
text-decoration: underline;
}
&__title {
text-align: center;
border-bottom: solid 1px $white;
padding-bottom: 10px;
margin-top: 10px;
}
&__content {
padding: 0 20px 20px 20px;
> :first-child {
margin-top: 0;
}
> :last-child {
margin-bottom: 0;
}
}
&--dark {
background-color: $black;
color: $white;
border: 1px solid $white;
h1,
h2,
h3,
h4,
h5,
h6,
a {
color: $white;
}
}
}

View File

@ -0,0 +1,15 @@
import "./modal.scss";
import React from "react";
interface ModalProps {
children: React.ReactNode;
title: string;
}
export const Modal = ({ children, title }: ModalProps) => (
<div>
<h2 className="modal__title">{title}</h2>
<div className="modal__content">{children}</div>
</div>
);

View File

@ -0,0 +1 @@
export * from "./nav";

View File

@ -0,0 +1,212 @@
@import "../../styles/colors";
.nav {
padding: 20px;
padding: 10px !important;
border-bottom: 1px solid $white;
position: sticky;
top: 0;
background-color: $black;
z-index: 15;
h1 {
padding: 0;
padding-left: 8px;
margin: 0;
color: $white;
font-weight: 400;
display: flex;
flex-direction: column;
justify-content: center;
}
a {
display: flex;
flex-direction: column;
justify-content: center;
}
&--inverted {
border-bottom: none;
background: $vega-yellow3 url("../../images/clouds.png");
background-repeat: no-repeat;
background-size: cover;
background-position: 0 -300px;
h1 {
color: $black;
}
}
&__inner {
margin: 4px auto 0;
display: flex;
gap: 10px;
justify-content: space-between;
align-items: flex-start;
@media (min-width: 960px) {
justify-content: flex-start;
margin-left: 8px;
}
}
&__actions {
display: flex;
gap: 10px;
@media (min-width: 960px) {
flex: 1 1 auto;
}
> button {
background: none;
padding: 0;
}
}
&__logo {
width: 30px;
&-container {
text-transform: uppercase;
height: 30px;
display: inline-flex;
margin-left: 8px;
@media (min-width: 960px) {
height: 40px;
}
}
@media (min-width: 960px) {
width: 40px;
}
}
&__drawer {
background-image: url("../../images/banner.png");
display: flex;
flex-flow: column nowrap;
justify-content: space-between;
height: 100%;
overflow-y: scroll;
}
&__drawer-section {
padding: 12px;
&:first-child {
border-top: 0;
}
}
&__drawer-button {
display: flex;
flex-flow: column nowrap;
gap: 4px;
&:focus,
&:hover {
background: none;
outline: none;
}
span {
display: block;
width: 30px;
height: 4px;
background: $white;
}
&--inverted {
span {
background: $black;
}
}
}
&__wallets-container {
display: flex;
gap: 10px;
> button {
padding: 3px 10px;
font-size: 14px;
}
}
}
.nav-links {
display: flex;
.active {
background: $vega-yellow3;
color: $black;
}
> a.active {
&:hover {
color: $black;
}
}
> a {
color: $white;
text-decoration: none;
font-size: 16px;
}
&--row {
flex-direction: row;
text-transform: uppercase;
> a {
background: transparent;
padding: 3px 10px;
color: $white;
}
}
&--column {
border-top: 1px solid $white;
flex-direction: column;
grid-gap: 0;
gap: 0;
.active {
background: $vega-yellow3;
color: $black;
}
> a {
display: block;
background: $black;
color: $white;
padding: 20px;
}
> a:not(:first-child) {
border-top: 1px solid $white;
}
}
&--inverted {
.active {
background: $black;
color: $white;
}
> a {
color: $black;
text-decoration: none;
font-size: 16px;
}
> a.active {
&:hover {
color: $white;
}
}
}
}

View File

@ -0,0 +1,219 @@
import './nav.scss';
import { Drawer } from '@blueprintjs/core';
import debounce from 'lodash/debounce';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Link, NavLink } from 'react-router-dom';
import { Flags } from '../../config';
import {
AppStateActionType,
useAppState,
} from '../../contexts/app-state/app-state-context';
import vegaWhite from '../../images/vega_white.png';
import { Routes } from '../../routes/router-config';
import { EthWallet } from '../eth-wallet';
import { VegaWallet } from '../vega-wallet';
export const Nav = () => {
const [windowWidth, setWindowWidth] = React.useState(window.innerWidth);
const isDesktop = windowWidth > 959;
const inverted = Flags.FAIRGROUND;
React.useEffect(() => {
const handleResizeDebounced = debounce(() => {
setWindowWidth(window.innerWidth);
}, 300);
window.addEventListener('resize', handleResizeDebounced);
return () => {
window.removeEventListener('resize', handleResizeDebounced);
};
}, []);
return (
<div
className={`nav nav-${isDesktop ? 'large' : 'small'} ${
inverted ? 'nav--inverted' : ''
}`}
>
{isDesktop && <NavHeader fairground={inverted} />}
<div className="nav__inner">
{!isDesktop && <NavHeader fairground={inverted} />}
<div className="nav__actions">
{isDesktop ? (
<NavLinks inverted={inverted} isDesktop={isDesktop} />
) : (
<NavDrawer inverted={inverted} />
)}
</div>
</div>
</div>
);
};
const NavHeader = ({ fairground }: { fairground: boolean }) => {
const { t } = useTranslation();
return (
<div className="nav__logo-container">
<Link to="/">
{fairground ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="33"
height="20"
viewBox="0 0 200 116"
fill="none"
data-testid="fairground-icon"
>
<g clip-path="url(#clip0)">
<path
d="M70.5918 32.8569L70.5918 22.7932L60.5254 22.7932L60.5254 32.8569L70.5918 32.8569Z"
fill="black"
></path>
<path
d="M80.6641 83.2006L80.6641 73.1377L70.5977 73.1377L70.5977 83.2006L80.6641 83.2006Z"
fill="black"
></path>
<path
d="M70.5918 93.2409L70.5918 83.1772L60.5254 83.1772L60.5254 93.2409L70.5918 93.2409Z"
fill="black"
></path>
<path
d="M100.797 93.2636L100.797 73.1377L90.7305 73.1377L90.7305 93.2636L100.797 93.2636Z"
fill="black"
></path>
<path
d="M90.7285 103.33L90.7285 93.2671L80.662 93.2671L80.662 103.33L90.7285 103.33Z"
fill="black"
></path>
<path
d="M90.7285 22.8026L90.7285 12.74L80.662 12.74L80.662 22.8026L90.7285 22.8026Z"
fill="black"
></path>
<path
d="M110.869 12.6108L110.869 2.54785L100.803 2.54785L100.803 12.6108L110.869 12.6108Z"
fill="black"
></path>
<path
d="M120.934 103.326L120.934 73.1377L110.867 73.1377L110.867 103.326L120.934 103.326Z"
fill="black"
></path>
<path
d="M110.869 113.384L110.869 103.321L100.803 103.321L100.803 113.384L110.869 113.384Z"
fill="black"
></path>
<path
d="M161.328 52.9855L161.328 42.9226L151.262 42.9226L151.262 52.9855L161.328 52.9855Z"
fill="black"
></path>
<path
d="M20.133 83.187L30.3354 83.187L30.3354 73.124L40.4017 73.124L40.4017 63.0613L50.4681 63.0613L50.4681 73.124L60.5345 73.124L60.5345 63.0613L70.6008 63.0613L80.6672 63.0613L131.135 63.0613L131.135 113.376L161.334 113.376L161.334 103.313L171.4 103.313L171.4 93.25L181.467 93.25L181.467 83.187L191.533 83.187L191.533 63.0613L181.467 63.0613L181.467 73.1241L171.4 73.1241L171.4 83.187L161.334 83.187L161.334 73.1241L171.4 73.1241L171.4 63.0613L161.334 63.0613L151.268 63.0613L141.201 63.0613L141.201 52.9983L141.201 32.8726L161.334 32.8726L171.4 32.8726L171.4 63.0613L181.467 63.0613L181.467 52.9983L191.533 52.9983L191.533 32.8726L181.467 32.8726L181.467 22.8096L171.4 22.8096L171.4 12.7467L161.334 12.7467L161.334 2.54785L141.201 2.54785L131.135 2.54785L131.135 52.9983L120.933 52.9983L120.933 12.7467L110.866 12.7467L110.866 52.9983L100.8 52.9983L100.8 22.8096L90.7336 22.8096L90.7336 52.9983L80.6672 52.9983L80.6672 32.8726L70.6008 32.8726L70.6008 52.9983L60.5345 52.9983L60.5345 42.9354L50.4681 42.9354L50.4681 52.9983L40.4017 52.9983L40.4017 42.9354L30.3354 42.9354L30.3354 32.8726L20.133 32.8726L20.133 22.8096L0.000308081 22.8096L0.000307201 42.9354L10.0666 42.9354L10.0666 52.9983L20.133 52.9983L20.133 63.0613L10.0666 63.0613L10.0666 73.124L0.000305881 73.124L0.000305002 93.25L20.133 93.25L20.133 83.187Z"
fill="black"
></path>
</g>
<defs>
<clipPath id="clip0">
<rect width="200" height="116" fill="none"></rect>
</clipPath>
</defs>
</svg>
) : (
<img alt="Vega" src={vegaWhite} className="nav__logo" />
)}
</Link>
<h1 className="text-h3">
{fairground ? t('fairgroundTitle') : t('title')}
</h1>
</div>
);
};
const NavDrawer = ({ inverted }: { inverted: boolean }) => {
const { appState, appDispatch } = useAppState();
return (
<>
<button
onClick={() =>
appDispatch({
type: AppStateActionType.SET_DRAWER,
isOpen: true,
})
}
className={`nav__drawer-button ${
inverted ? 'nav__drawer-button--inverted' : ''
}`}
>
<span />
<span />
<span />
</button>
<Drawer
isOpen={appState.drawerOpen}
onClose={() =>
appDispatch({
type: AppStateActionType.SET_DRAWER,
isOpen: false,
})
}
size="80%"
style={{ maxWidth: 420, border: '1px solid white' }}
>
<div className="nav__drawer">
<div>
<div className="nav__drawer-section">
<EthWallet />
</div>
<div className="nav__drawer-section">
<VegaWallet />
</div>
</div>
<NavLinks inverted={false} isDesktop={false} />
</div>
</Drawer>
</>
);
};
const NavLinks = ({
isDesktop,
inverted,
}: {
isDesktop: boolean;
inverted: boolean;
}) => {
const { appDispatch } = useAppState();
const { t } = useTranslation();
const linkProps = {
onClick: () =>
appDispatch({ type: AppStateActionType.SET_DRAWER, isOpen: false }),
};
return (
<nav
className={`nav-links nav-links--${isDesktop ? 'row' : 'column'}
${inverted ? 'nav-links--inverted' : ''}`}
>
<NavLink {...linkProps} to={Routes.HOME}>
{t('Home')}
</NavLink>
<NavLink {...linkProps} to={Routes.VESTING}>
{t('Vesting')}
</NavLink>
<NavLink {...linkProps} to={Routes.STAKING}>
{t('Staking')}
</NavLink>
<NavLink {...linkProps} to={Routes.REWARDS}>
{t('Rewards')}
</NavLink>
<NavLink {...linkProps} to={Routes.WITHDRAW}>
{t('Withdraw')}
</NavLink>
<NavLink {...linkProps} to={Routes.GOVERNANCE}>
{t('Governance')}
</NavLink>
</nav>
);
};

View File

@ -0,0 +1,53 @@
@import "../../styles/colors";
.template-sidebar {
border-bottom: 1px solid $white;
@media (min-width: 960px) {
display: grid;
grid-template-rows: auto minmax(600px, 1fr);
grid-template-columns: 1fr 450px;
}
main {
padding: 20px;
}
aside {
padding: 20px;
background-image: url("../../images/banner.png");
background-size: 100% auto;
}
aside {
border-left: 1px solid white;
}
header {
grid-column: 1 / 3;
grid-row: 1 / 1;
}
main {
grid-column: 1 / 2;
grid-row: 2 / 3;
}
aside {
display: none;
@media (min-width: 960px) {
display: block;
grid-column: 2 / 3;
grid-row: 1 / 3;
}
section {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
}
}

View File

@ -0,0 +1,24 @@
import "./template-sidebar.scss";
import React from "react";
import { Nav } from "../nav";
export interface TemplateSidebarProps {
children: React.ReactNode;
sidebar: React.ReactNode[];
}
export function TemplateSidebar({ children, sidebar }: TemplateSidebarProps) {
return (
<div className="template-sidebar">
<Nav />
<main>{children}</main>
<aside>
{sidebar.map((Component, i) => (
<section key={i}>{Component}</section>
))}
</aside>
</div>
);
}

View File

@ -0,0 +1 @@
export * from "./splash-error";

View File

@ -0,0 +1,23 @@
@import "../../styles/colors";
.splash-error__icon {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
margin-bottom: 20px;
span {
display: block;
width: 10px;
background: $white;
&:first-child {
height: 30px;
}
&:last-child {
height: 10px;
}
}
}

View File

@ -0,0 +1,16 @@
import "./splash-error.scss";
import { useTranslation } from "react-i18next";
export const SplashError = () => {
const { t } = useTranslation();
return (
<div>
<div className="splash-error__icon">
<span />
<span />
</div>
{t("networkDown")}
</div>
);
};

View File

@ -0,0 +1 @@
export * from "./splash-loader";

View File

@ -0,0 +1,22 @@
@import "../../styles/colors";
.loading {
display: flex;
flex-direction: column;
align-items: center;
&__animation {
display: flex;
flex-wrap: wrap;
width: 50px;
height: 50px;
margin-bottom: 20px;
div {
width: 10px;
height: 10px;
background: white;
opacity: 0;
}
}
}

View File

@ -0,0 +1,32 @@
import "./splash-loader.scss";
import React from "react";
export const SplashLoader = ({ text = "Loading" }: { text?: string }) => {
const [, forceRender] = React.useState(false);
React.useEffect(() => {
const interval = setInterval(() => {
forceRender((x) => !x);
}, 100);
return () => clearInterval(interval);
}, []);
return (
<div className="loading" data-testid="splash-loader">
<div className="loading__animation">
{new Array(25).fill(null).map((_, i) => {
return (
<div
key={i}
style={{
opacity: Math.random() > 0.75 ? 1 : 0,
}}
/>
);
})}
</div>
<div>{text}</div>
</div>
);
};

View File

@ -0,0 +1,39 @@
import { Radio, RadioGroup } from '@blueprintjs/core';
import React from 'react';
import { useTranslation } from 'react-i18next';
export enum StakingMethod {
Contract = 'Contract',
Wallet = 'Wallet',
}
export const StakingMethodRadio = ({
setSelectedStakingMethod,
selectedStakingMethod,
}: {
selectedStakingMethod: string;
setSelectedStakingMethod: React.Dispatch<any>;
}) => {
const { t } = useTranslation();
return (
<RadioGroup
inline={true}
onChange={(e) => {
// @ts-ignore can't recognise .value
setSelectedStakingMethod(e.target.value);
}}
selectedValue={selectedStakingMethod}
>
<Radio
data-testid="associate-radio-contract"
label={t('Vesting contract')}
value={StakingMethod.Contract}
/>
<Radio
data-testid="associate-radio-wallet"
label={t('Wallet')}
value={StakingMethod.Wallet}
/>
</RadioGroup>
);
};

View File

@ -0,0 +1 @@
export * from "./stateful-button";

View File

@ -0,0 +1,12 @@
@import "../../styles/colors";
.stateful-button {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
&:disabled {
cursor: default;
}
}

View File

@ -0,0 +1,11 @@
import './stateful-button.scss';
import type { ButtonHTMLAttributes } from 'react';
import { Button } from '@vegaprotocol/ui-toolkit';
export const StatefulButton = (
props: ButtonHTMLAttributes<HTMLButtonElement>
) => {
const classProp = props.className || '';
return <Button {...props} className={`stateful-button fill ${classProp}`} />;
};

View File

@ -0,0 +1 @@
export { SyntaxHighlighter } from "./syntax-highlighter";

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