feat(2093): token sub nav (#2549)
* feat: implement subnav using exisitng DD component * chore: make nav as per design * chore: fix conflicts * style: make css for trigger match designs * chore: add border to subnav card * chore: reogranise files * chore: remove unused imports * fix: minor rendering errors * test: add first tests * chore: fix failing tests from nav update * test: add unit tests for nav * style: lint * style: make navbar theme work properly * style: lint * chore: remove unnecessary click on header * fix: move route config to ts only file * test: tidy up test logic * feat: add nav drop down to ui toolkit * fix: broken routing in places * style: lint Co-authored-by: Joe <joe@vega.xyz>
This commit is contained in:
parent
d474a502a8
commit
b8ff3d3050
@ -33,6 +33,7 @@ context(
|
|||||||
beforeEach('Navigate to withdrawal page', function () {
|
beforeEach('Navigate to withdrawal page', function () {
|
||||||
cy.reload();
|
cy.reload();
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
|
cy.wait_for_spinner();
|
||||||
cy.navigate_to('withdraw');
|
cy.navigate_to('withdraw');
|
||||||
cy.connectVegaWallet();
|
cy.connectVegaWallet();
|
||||||
cy.ethereum_wallet_connect();
|
cy.ethereum_wallet_connect();
|
||||||
@ -52,6 +53,7 @@ context(
|
|||||||
it('Unable to submit withdrawal with invalid fields', function () {
|
it('Unable to submit withdrawal with invalid fields', function () {
|
||||||
cy.getByTestId(withdraw).should('be.visible').click();
|
cy.getByTestId(withdraw).should('be.visible').click();
|
||||||
cy.getByTestId(selectAsset).select(usdtName);
|
cy.getByTestId(selectAsset).select(usdtName);
|
||||||
|
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
|
||||||
cy.getByTestId(submitWithdrawalButton).click();
|
cy.getByTestId(submitWithdrawalButton).click();
|
||||||
cy.getByTestId(formValidationError).should('have.length', 1);
|
cy.getByTestId(formValidationError).should('have.length', 1);
|
||||||
cy.getByTestId(amountInput).clear().click().type('0.0000001');
|
cy.getByTestId(amountInput).clear().click().type('0.0000001');
|
||||||
@ -74,7 +76,7 @@ context(
|
|||||||
cy.getByTestId(withdraw).should('be.visible').click();
|
cy.getByTestId(withdraw).should('be.visible').click();
|
||||||
cy.getByTestId(selectAsset).select(usdtName);
|
cy.getByTestId(selectAsset).select(usdtName);
|
||||||
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
|
cy.getByTestId(balanceAvailable, txTimeout).should('exist');
|
||||||
cy.getByTestId(withdrawalThreshold).should('have.text', '100,000.00T');
|
cy.getByTestId(withdrawalThreshold).should('have.text', '100,000.00000T');
|
||||||
cy.getByTestId(delayTime).should('have.text', 'None');
|
cy.getByTestId(delayTime).should('have.text', 'None');
|
||||||
cy.getByTestId(amountInput).click().type('100');
|
cy.getByTestId(amountInput).click().type('100');
|
||||||
cy.getByTestId(submitWithdrawalButton).click();
|
cy.getByTestId(submitWithdrawalButton).click();
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
const navSection = 'nav';
|
const navSection = 'nav';
|
||||||
const navVesting = '[href="/token/tranches"]';
|
const navSupply = '[href="/token/tranches"]';
|
||||||
const navToken = '[href="/token"]';
|
const navToken = '[href="/token"]';
|
||||||
const navStaking = '[href="/validators"]';
|
const navStaking = '[href="/validators"]';
|
||||||
const navRewards = '[href="/rewards"]';
|
const navRewards = '[href="/rewards"]';
|
||||||
const navWithdraw = '[href="/token/withdraw"]';
|
const navWithdraw = '[href="/token/withdraw"]';
|
||||||
const navGovernance = '[href="/proposals"]';
|
const navGovernance = '[href="/proposals"]';
|
||||||
|
const navRedeem = '[href="/token/redeem"]';
|
||||||
|
|
||||||
const tokenDetailsTable = '.token-details';
|
const tokenDetailsTable = '.token-details';
|
||||||
const address = '[data-testid="token-address"]';
|
const address = '[data-testid="token-address"]';
|
||||||
@ -34,34 +35,39 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Navigation tabs', function () {
|
describe('Navigation tabs', function () {
|
||||||
it('should have TOKEN tab', function () {
|
it('should have proposals tab', function () {
|
||||||
cy.get(navSection).within(() => {
|
cy.get(navSection).within(() => {
|
||||||
cy.get(navToken).should('be.visible');
|
cy.get(navGovernance).should('be.visible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should have VESTING tab', function () {
|
it('should have validators tab', function () {
|
||||||
cy.get(navSection).within(() => {
|
|
||||||
cy.get(navVesting).should('be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
it('should have STAKING tab', function () {
|
|
||||||
cy.get(navSection).within(() => {
|
cy.get(navSection).within(() => {
|
||||||
cy.get(navStaking).should('be.visible');
|
cy.get(navStaking).should('be.visible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should have REWARDS tab', function () {
|
it('should have rewards tab', function () {
|
||||||
cy.get(navSection).within(() => {
|
cy.get(navSection).within(() => {
|
||||||
cy.get(navRewards).should('be.visible');
|
cy.get(navRewards).should('be.visible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should have WITHDRAW tab', function () {
|
|
||||||
cy.get(navSection).within(() => {
|
describe('Token dropdown', function () {
|
||||||
|
before('click on token dropdown', function () {
|
||||||
|
cy.get(navSection).within(() => {
|
||||||
|
cy.getByTestId('state-trigger').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should have token dropdown', function () {
|
||||||
|
cy.get(navToken).should('be.visible');
|
||||||
|
});
|
||||||
|
it('should have supply & vesting dropdown', function () {
|
||||||
|
cy.get(navSupply).should('be.visible');
|
||||||
|
});
|
||||||
|
it('should have withdraw dropdown', function () {
|
||||||
cy.get(navWithdraw).should('be.visible');
|
cy.get(navWithdraw).should('be.visible');
|
||||||
});
|
});
|
||||||
});
|
it('should have redeem dropdown', function () {
|
||||||
it('should have GOVERNANCE tab', function () {
|
cy.get(navRedeem).should('be.visible');
|
||||||
cy.get(navSection).within(() => {
|
|
||||||
cy.get(navGovernance).should('be.visible');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@ context(
|
|||||||
cy.visit('/').navigate_to('vesting');
|
cy.visit('/').navigate_to('vesting');
|
||||||
});
|
});
|
||||||
it('should have vesting tab highlighted', function () {
|
it('should have vesting tab highlighted', function () {
|
||||||
cy.verify_tab_highlighted('vesting');
|
cy.verify_tab_highlighted('token');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have VESTING header visible', function () {
|
it('should have VESTING header visible', function () {
|
||||||
@ -37,9 +37,7 @@ context(
|
|||||||
// 1005-VEST-001
|
// 1005-VEST-001
|
||||||
// 1005-VEST-002
|
// 1005-VEST-002
|
||||||
it('Able to view tranches', function () {
|
it('Able to view tranches', function () {
|
||||||
cy.get('[href="/token/tranches"]')
|
cy.navigate_to('supply');
|
||||||
.should('have.text', 'Supply & Vesting')
|
|
||||||
.click();
|
|
||||||
cy.url().should('include', '/token/tranches');
|
cy.url().should('include', '/token/tranches');
|
||||||
cy.get('h1').should('contain.text', 'Vesting tranches');
|
cy.get('h1').should('contain.text', 'Vesting tranches');
|
||||||
});
|
});
|
||||||
|
@ -10,7 +10,7 @@ context(
|
|||||||
|
|
||||||
describe('with wallets disconnected', function () {
|
describe('with wallets disconnected', function () {
|
||||||
it('should have withdraw tab highlighted', function () {
|
it('should have withdraw tab highlighted', function () {
|
||||||
cy.verify_tab_highlighted('withdraw');
|
cy.verify_tab_highlighted('token');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have WITHDRAW header visible', function () {
|
it('should have WITHDRAW header visible', function () {
|
||||||
|
@ -14,13 +14,25 @@ const navigation = {
|
|||||||
withdraw: '[href="/token/withdraw"]',
|
withdraw: '[href="/token/withdraw"]',
|
||||||
proposals: '[href="/proposals"]',
|
proposals: '[href="/proposals"]',
|
||||||
pageSpinner: '[data-testid="splash-loader"]',
|
pageSpinner: '[data-testid="splash-loader"]',
|
||||||
|
supply: '[href="/token/tranches"]',
|
||||||
token: '[href="/token"]',
|
token: '[href="/token"]',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const topLevelRoutes = ['proposals', 'validators', 'rewards'];
|
||||||
|
|
||||||
Cypress.Commands.add('navigate_to', (page) => {
|
Cypress.Commands.add('navigate_to', (page) => {
|
||||||
return cy.get(navigation.section, { timeout: 10000 }).within(() => {
|
const tokenDropDown = 'state-trigger';
|
||||||
cy.get(navigation[page]).click({ force: true });
|
|
||||||
});
|
if (!topLevelRoutes.includes(page)) {
|
||||||
|
cy.getByTestId(tokenDropDown, { timeout: 10000 }).click();
|
||||||
|
cy.getByTestId('token-dropdown').within(() => {
|
||||||
|
cy.get(navigation[page]).click();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return cy.get(navigation.section, { timeout: 10000 }).within(() => {
|
||||||
|
cy.get(navigation[page]).click();
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('verify_tab_highlighted', (page) => {
|
Cypress.Commands.add('verify_tab_highlighted', (page) => {
|
||||||
|
@ -16,6 +16,7 @@ const stakeValidatorListTotalStake = '[col-id="stake"] > div > span';
|
|||||||
const stakeValidatorListTotalShare = '[col-id="stakeShare"] > div > span';
|
const stakeValidatorListTotalShare = '[col-id="stakeShare"] > div > span';
|
||||||
const stakeValidatorListName = '[col-id="validator"]';
|
const stakeValidatorListName = '[col-id="validator"]';
|
||||||
const vegaKeySelector = '#vega-key-selector';
|
const vegaKeySelector = '#vega-key-selector';
|
||||||
|
const dialogCloseButton = '[data-testid="dialog-close"]';
|
||||||
|
|
||||||
const txTimeout = Cypress.env('txTimeout');
|
const txTimeout = Cypress.env('txTimeout');
|
||||||
const epochTimeout = Cypress.env('epochTimeout');
|
const epochTimeout = Cypress.env('epochTimeout');
|
||||||
@ -39,6 +40,7 @@ Cypress.Commands.add('staking_validator_page_add_stake', (stake) => {
|
|||||||
.and('contain', `Add ${stake} $VEGA tokens`)
|
.and('contain', `Add ${stake} $VEGA tokens`)
|
||||||
.and('be.visible')
|
.and('be.visible')
|
||||||
.click();
|
.click();
|
||||||
|
cy.get(dialogCloseButton).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('staking_validator_page_remove_stake', (stake) => {
|
Cypress.Commands.add('staking_validator_page_remove_stake', (stake) => {
|
||||||
@ -51,6 +53,7 @@ Cypress.Commands.add('staking_validator_page_remove_stake', (stake) => {
|
|||||||
.and('contain', `Remove ${stake} $VEGA tokens at the end of epoch`)
|
.and('contain', `Remove ${stake} $VEGA tokens at the end of epoch`)
|
||||||
.and('be.visible')
|
.and('be.visible')
|
||||||
.click();
|
.click();
|
||||||
|
cy.get(dialogCloseButton).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('staking_page_associate_tokens', (amount, options) => {
|
Cypress.Commands.add('staking_page_associate_tokens', (amount, options) => {
|
||||||
|
@ -7,6 +7,7 @@ import {
|
|||||||
import * as Dialog from '@radix-ui/react-dialog';
|
import * as Dialog from '@radix-ui/react-dialog';
|
||||||
import { EthWallet } from '../eth-wallet';
|
import { EthWallet } from '../eth-wallet';
|
||||||
import { VegaWallet } from '../vega-wallet';
|
import { VegaWallet } from '../vega-wallet';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
interface Route {
|
interface Route {
|
||||||
name: string;
|
name: string;
|
||||||
@ -29,6 +30,7 @@ const DrawerNavLinks = ({
|
|||||||
routes: Route[];
|
routes: Route[];
|
||||||
}) => {
|
}) => {
|
||||||
const { appDispatch } = useAppState();
|
const { appDispatch } = useAppState();
|
||||||
|
const { t } = useTranslation();
|
||||||
const linkProps = {
|
const linkProps = {
|
||||||
end: true,
|
end: true,
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
@ -55,7 +57,7 @@ const DrawerNavLinks = ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{name}
|
{t(name)}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
49
apps/token/src/components/nav/nav-dropdown.tsx
Normal file
49
apps/token/src/components/nav/nav-dropdown.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import Routes, { TOKEN_DROPDOWN_ROUTES } from '../../routes/routes';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import type { NavbarTheme } from './nav-link';
|
||||||
|
import { AppNavLink } from './nav-link';
|
||||||
|
import {
|
||||||
|
NavDropdownMenu,
|
||||||
|
NavDropdownMenuContent,
|
||||||
|
NavDropdownMenuItem,
|
||||||
|
NavDropdownMenuTrigger,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
export const NavDropDown = ({ navbarTheme }: { navbarTheme: NavbarTheme }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [isOpen, setOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<NavDropdownMenu open={isOpen} onOpenChange={(open) => setOpen(open)}>
|
||||||
|
<AppNavLink
|
||||||
|
name={
|
||||||
|
<NavDropdownMenuTrigger
|
||||||
|
className="w-auto text-capMenu"
|
||||||
|
data-testid="state-trigger"
|
||||||
|
onClick={() => setOpen(!isOpen)}
|
||||||
|
>
|
||||||
|
{t('Token')}
|
||||||
|
</NavDropdownMenuTrigger>
|
||||||
|
}
|
||||||
|
testId="token-dd"
|
||||||
|
path={Routes.TOKEN}
|
||||||
|
navbarTheme={navbarTheme}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<NavDropdownMenuContent data-testid="token-dropdown">
|
||||||
|
{TOKEN_DROPDOWN_ROUTES.map((r) => (
|
||||||
|
<NavDropdownMenuItem key={r.name} onClick={() => setOpen(false)}>
|
||||||
|
<AppNavLink
|
||||||
|
testId={r.name}
|
||||||
|
name={t(r.name)}
|
||||||
|
path={r.path}
|
||||||
|
navbarTheme={'inherit'}
|
||||||
|
end={true}
|
||||||
|
fullWidth={true}
|
||||||
|
/>
|
||||||
|
</NavDropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</NavDropdownMenuContent>
|
||||||
|
</NavDropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
52
apps/token/src/components/nav/nav-link.tsx
Normal file
52
apps/token/src/components/nav/nav-link.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import type { HTMLAttributeAnchorTarget, ReactNode } from 'react';
|
||||||
|
import { getNavLinkClassNames } from '@vegaprotocol/ui-toolkit';
|
||||||
|
|
||||||
|
export type NavbarTheme = 'inherit' | 'dark' | 'yellow';
|
||||||
|
|
||||||
|
interface AppNavLinkProps {
|
||||||
|
name: ReactNode | string;
|
||||||
|
path: string;
|
||||||
|
navbarTheme: NavbarTheme;
|
||||||
|
testId?: string;
|
||||||
|
target?: HTMLAttributeAnchorTarget;
|
||||||
|
end?: boolean;
|
||||||
|
fullWidth?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppNavLink = ({
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
navbarTheme,
|
||||||
|
target,
|
||||||
|
testId,
|
||||||
|
end = false,
|
||||||
|
fullWidth = false,
|
||||||
|
}: AppNavLinkProps) => {
|
||||||
|
const borderClasses = classNames('absolute h-1 w-full bottom-[-1px] left-0', {
|
||||||
|
'bg-black dark:bg-vega-yellow': navbarTheme !== 'yellow',
|
||||||
|
'bg-black': navbarTheme === 'yellow',
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<NavLink
|
||||||
|
key={path}
|
||||||
|
data-testid={testId}
|
||||||
|
to={{ pathname: path }}
|
||||||
|
className={getNavLinkClassNames(navbarTheme, fullWidth)}
|
||||||
|
target={target}
|
||||||
|
end={end}
|
||||||
|
>
|
||||||
|
{({ isActive }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{name}
|
||||||
|
{isActive && (
|
||||||
|
<span data-testid="link-active" className={borderClasses} />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</NavLink>
|
||||||
|
);
|
||||||
|
};
|
62
apps/token/src/components/nav/nav.spec.tsx
Normal file
62
apps/token/src/components/nav/nav.spec.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { render, screen, within } from '@testing-library/react';
|
||||||
|
import { EnvironmentProvider, Networks } from '@vegaprotocol/environment';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
import { Nav } from './nav';
|
||||||
|
|
||||||
|
jest.mock('@vegaprotocol/environment', () => ({
|
||||||
|
...jest.requireActual('@vegaprotocol/environment'),
|
||||||
|
NetworkSwitcher: () => <div data-testid="network-switcher" />,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const renderComponent = (initialEntries?: string[]) => {
|
||||||
|
return render(
|
||||||
|
<EnvironmentProvider
|
||||||
|
definitions={{ VEGA_ENV: Networks.MAINNET }}
|
||||||
|
config={{ hosts: [] }}
|
||||||
|
>
|
||||||
|
<MemoryRouter initialEntries={initialEntries}>
|
||||||
|
<Nav />
|
||||||
|
</MemoryRouter>
|
||||||
|
</EnvironmentProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('nav', () => {
|
||||||
|
it('Renders title and logo with link to home', () => {
|
||||||
|
renderComponent();
|
||||||
|
expect(screen.getByText('Governance')).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId('logo-link')).toHaveProperty(
|
||||||
|
'href',
|
||||||
|
'http://localhost/'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('Renders network switcher', () => {
|
||||||
|
renderComponent();
|
||||||
|
expect(screen.getByTestId('network-switcher')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Renders all top level routes', () => {
|
||||||
|
renderComponent();
|
||||||
|
expect(screen.getByTestId('Proposals')).toHaveProperty(
|
||||||
|
'href',
|
||||||
|
'http://localhost/proposals'
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('Validators')).toHaveProperty(
|
||||||
|
'href',
|
||||||
|
'http://localhost/validators'
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('Rewards')).toHaveProperty(
|
||||||
|
'href',
|
||||||
|
'http://localhost/rewards'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('Shows active state on dropdown trigger when on home route for subroutes', () => {
|
||||||
|
const { getByTestId } = renderComponent(['/token']);
|
||||||
|
const dd = getByTestId('token-dd');
|
||||||
|
expect(within(dd).getByTestId('link-active')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('Shows active state on dropdown trigger when on sub route of dropdown', () => {
|
||||||
|
const { getByTestId } = renderComponent(['/token/withdraw']);
|
||||||
|
const dd = getByTestId('token-dd');
|
||||||
|
expect(within(dd).getByTestId('link-active')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -1,17 +1,14 @@
|
|||||||
import classNames from 'classnames';
|
import { Link } from 'react-router-dom';
|
||||||
import { NavLink, Link } from 'react-router-dom';
|
|
||||||
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
import { NetworkSwitcher } from '@vegaprotocol/environment';
|
||||||
import type { HTMLAttributeAnchorTarget } from 'react';
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Routes from '../../routes/routes';
|
import { TOP_LEVEL_ROUTES } from '../../routes/routes';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import vegaWhite from '../../images/vega_white.png';
|
import vegaWhite from '../../images/vega_white.png';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { NavDrawer } from './nav-draw';
|
import { NavDrawer } from './nav-draw';
|
||||||
import {
|
import { Nav as ToolkitNav } from '@vegaprotocol/ui-toolkit';
|
||||||
getNavLinkClassNames,
|
import { AppNavLink } from './nav-link';
|
||||||
Nav as ToolkitNav,
|
import { NavDropDown } from './nav-dropdown';
|
||||||
} from '@vegaprotocol/ui-toolkit';
|
|
||||||
|
|
||||||
const useDebouncedResize = () => {
|
const useDebouncedResize = () => {
|
||||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||||
@ -43,42 +40,12 @@ export const Nav = ({ navbarTheme = 'inherit' }: NavbarProps) => {
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const isYellow = navbarTheme === 'yellow';
|
const isYellow = navbarTheme === 'yellow';
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
name: t('Proposals'),
|
|
||||||
path: Routes.PROPOSALS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t('Validators'),
|
|
||||||
path: Routes.VALIDATORS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t('Rewards'),
|
|
||||||
path: Routes.REWARDS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t('Token'),
|
|
||||||
path: Routes.TOKEN,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t('Redeem'),
|
|
||||||
path: Routes.REDEEM,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t('Withdraw'),
|
|
||||||
path: Routes.WITHDRAWALS,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: t('Supply & Vesting'),
|
|
||||||
path: Routes.TRANCHES,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolkitNav
|
<ToolkitNav
|
||||||
navbarTheme={navbarTheme}
|
navbarTheme={navbarTheme}
|
||||||
icon={
|
icon={
|
||||||
<Link to="/">
|
<Link to="/" data-testid="logo-link">
|
||||||
<img alt="Vega" src={vegaWhite} height={30} width={30} />
|
<img alt="Vega" src={vegaWhite} height={30} width={30} />
|
||||||
</Link>
|
</Link>
|
||||||
}
|
}
|
||||||
@ -87,54 +54,22 @@ export const Nav = ({ navbarTheme = 'inherit' }: NavbarProps) => {
|
|||||||
>
|
>
|
||||||
{isDesktop ? (
|
{isDesktop ? (
|
||||||
<nav className="flex items-center flex-1 px-2">
|
<nav className="flex items-center flex-1 px-2">
|
||||||
{routes.map((r) => (
|
{TOP_LEVEL_ROUTES.map((r) => (
|
||||||
<AppNavLink {...r} navbarTheme={navbarTheme} />
|
<AppNavLink
|
||||||
|
key={r.path}
|
||||||
|
testId={r.name}
|
||||||
|
name={t(r.name)}
|
||||||
|
path={r.path}
|
||||||
|
navbarTheme={navbarTheme}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
|
<NavDropDown navbarTheme={navbarTheme} />
|
||||||
</nav>
|
</nav>
|
||||||
) : (
|
) : (
|
||||||
<nav className="flex items-center flex-1 px-2 justify-end">
|
<nav className="flex items-center flex-1 px-2 justify-end">
|
||||||
<NavDrawer inverted={isYellow} routes={routes} />
|
<NavDrawer inverted={isYellow} routes={TOP_LEVEL_ROUTES} />
|
||||||
</nav>
|
</nav>
|
||||||
)}
|
)}
|
||||||
</ToolkitNav>
|
</ToolkitNav>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface AppNavLinkProps {
|
|
||||||
name: string;
|
|
||||||
path: string;
|
|
||||||
navbarTheme: NavbarTheme;
|
|
||||||
testId?: string;
|
|
||||||
target?: HTMLAttributeAnchorTarget;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppNavLink = ({
|
|
||||||
name,
|
|
||||||
path,
|
|
||||||
navbarTheme,
|
|
||||||
target,
|
|
||||||
testId = name,
|
|
||||||
}: AppNavLinkProps) => {
|
|
||||||
const borderClasses = classNames('absolute h-1 w-full bottom-[-1px] left-0', {
|
|
||||||
'bg-black dark:bg-vega-yellow': navbarTheme !== 'yellow',
|
|
||||||
'bg-black': navbarTheme === 'yellow',
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<NavLink
|
|
||||||
data-testid={testId}
|
|
||||||
to={{ pathname: path }}
|
|
||||||
className={getNavLinkClassNames(navbarTheme)}
|
|
||||||
target={target}
|
|
||||||
end={true}
|
|
||||||
>
|
|
||||||
{({ isActive }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{name}
|
|
||||||
{isActive && <span className={borderClasses} />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</NavLink>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -78,7 +78,7 @@ const HomeNodes = ({
|
|||||||
<Heading title={t('Validators')} />
|
<Heading title={t('Validators')} />
|
||||||
<h3 className="mb-6">{t('homeValidatorsIntro')}</h3>
|
<h3 className="mb-6">{t('homeValidatorsIntro')}</h3>
|
||||||
<div className="flex items-center mb-8 gap-8">
|
<div className="flex items-center mb-8 gap-8">
|
||||||
<Link to={Routes.STAKING}>
|
<Link to={Routes.VALIDATORS}>
|
||||||
<Button size="md">{t('homeValidatorsButtonText')}</Button>
|
<Button size="md">{t('homeValidatorsButtonText')}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ const HomeNodes = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeNodes.length > nodesToShow && (
|
{activeNodes.length > nodesToShow && (
|
||||||
<Link to={Routes.STAKING}>
|
<Link to={Routes.VALIDATORS}>
|
||||||
<span className="underline">
|
<span className="underline">
|
||||||
And {activeNodes.length - nodesToShow} more...
|
And {activeNodes.length - nodesToShow} more...
|
||||||
</span>
|
</span>
|
||||||
|
@ -59,7 +59,7 @@ export const RedemptionInformation = () => {
|
|||||||
i18nKey="noVestingTokens"
|
i18nKey="noVestingTokens"
|
||||||
components={{
|
components={{
|
||||||
tranchesLink: (
|
tranchesLink: (
|
||||||
<Link className="underline text-white" to={Routes.TRANCHES} />
|
<Link className="underline text-white" to={Routes.SUPPLY} />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -81,7 +81,7 @@ const RedemptionRouter = () => {
|
|||||||
<Callout>
|
<Callout>
|
||||||
<p>{t('You have no VEGA tokens currently vesting.')}</p>
|
<p>{t('You have no VEGA tokens currently vesting.')}</p>
|
||||||
</Callout>
|
</Callout>
|
||||||
<Link to={RoutesConfig.TRANCHES}>{t('viewAllTranches')}</Link>
|
<Link to={RoutesConfig.SUPPLY}>{t('viewAllTranches')}</Link>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -110,7 +110,7 @@ export const TrancheTable = ({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<TrancheItem
|
<TrancheItem
|
||||||
link={`${Routes.TRANCHES}/${tranche.tranche_id}`}
|
link={`${Routes.SUPPLY}/${tranche.tranche_id}`}
|
||||||
tranche={tranche}
|
tranche={tranche}
|
||||||
locked={locked}
|
locked={locked}
|
||||||
unlocked={vested}
|
unlocked={vested}
|
||||||
|
@ -78,7 +78,7 @@ export const RedeemFromTranche = () => {
|
|||||||
i18nKey="noVestingTokens"
|
i18nKey="noVestingTokens"
|
||||||
components={{
|
components={{
|
||||||
tranchesLink: (
|
tranchesLink: (
|
||||||
<Link className="underline text-white" to={Routes.TRANCHES} />
|
<Link className="underline text-white" to={Routes.SUPPLY} />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -202,12 +202,12 @@ const LazyWithdrawals = React.lazy(
|
|||||||
|
|
||||||
const redirects = [
|
const redirects = [
|
||||||
{
|
{
|
||||||
path: Routes.STAKING,
|
path: Routes.VALIDATORS,
|
||||||
element: <Navigate to={Routes.VALIDATORS} replace />,
|
element: <Navigate to={Routes.VALIDATORS} replace />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/tranches',
|
path: '/tranches',
|
||||||
element: <Navigate to={Routes.TRANCHES} replace />,
|
element: <Navigate to={Routes.SUPPLY} replace />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/withdrawals',
|
path: '/withdrawals',
|
||||||
@ -286,7 +286,7 @@ const routerConfig = [
|
|||||||
index: true,
|
index: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: Routes.TRANCHES,
|
path: Routes.SUPPLY,
|
||||||
element: <LazyTranches name="Tranches" />,
|
element: <LazyTranches name="Tranches" />,
|
||||||
children: [
|
children: [
|
||||||
{ index: true, element: <LazyTranchesTranches /> },
|
{ index: true, element: <LazyTranchesTranches /> },
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export default {
|
const Routes = {
|
||||||
HOME: '/',
|
HOME: '/',
|
||||||
CLAIM: '/claim',
|
CLAIM: '/claim',
|
||||||
VALIDATORS: '/validators',
|
VALIDATORS: '/validators',
|
||||||
@ -8,10 +8,54 @@ export default {
|
|||||||
NOT_PERMITTED: '/not-permitted',
|
NOT_PERMITTED: '/not-permitted',
|
||||||
NOT_FOUND: '/not-found',
|
NOT_FOUND: '/not-found',
|
||||||
CONTRACTS: '/contracts',
|
CONTRACTS: '/contracts',
|
||||||
STAKING: '/staking',
|
|
||||||
TOKEN: '/token',
|
TOKEN: '/token',
|
||||||
REDEEM: '/token/redeem',
|
REDEEM: '/token/redeem',
|
||||||
WITHDRAWALS: '/token/withdraw',
|
WITHDRAWALS: '/token/withdraw',
|
||||||
TRANCHES: '/token/tranches',
|
SUPPLY: '/token/tranches',
|
||||||
ASSOCIATE: '/token/associate',
|
ASSOCIATE: '/validators/associate',
|
||||||
|
DISASSOCIATE: '/validators/disassociate',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default Routes;
|
||||||
|
|
||||||
|
export const TOP_LEVEL_ROUTES = [
|
||||||
|
{
|
||||||
|
name: 'Proposals',
|
||||||
|
path: Routes.PROPOSALS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Validators',
|
||||||
|
path: Routes.VALIDATORS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rewards',
|
||||||
|
path: Routes.REWARDS,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TOKEN_DROPDOWN_ROUTES = [
|
||||||
|
{
|
||||||
|
name: 'Token',
|
||||||
|
path: Routes.TOKEN,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Supply & Vesting',
|
||||||
|
path: Routes.SUPPLY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Withdraw',
|
||||||
|
path: Routes.WITHDRAWALS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Redeem',
|
||||||
|
path: Routes.REDEEM,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Associate',
|
||||||
|
path: Routes.ASSOCIATE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Disassociate',
|
||||||
|
path: Routes.DISASSOCIATE,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
@ -45,7 +45,7 @@ const Home = ({ name }: RouteChildProps) => {
|
|||||||
trancheLink: (
|
trancheLink: (
|
||||||
<Link
|
<Link
|
||||||
data-testid="tranches-link"
|
data-testid="tranches-link"
|
||||||
to={Routes.TRANCHES}
|
to={Routes.SUPPLY}
|
||||||
className="underline text-white"
|
className="underline text-white"
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
@ -19,6 +19,7 @@ export * from './link';
|
|||||||
export * from './loader';
|
export * from './loader';
|
||||||
export * from './lozenge';
|
export * from './lozenge';
|
||||||
export * from './nav';
|
export * from './nav';
|
||||||
|
export * from './nav-dropdown';
|
||||||
export * from './popover';
|
export * from './popover';
|
||||||
export * from './price-change';
|
export * from './price-change';
|
||||||
export * from './progress-bar';
|
export * from './progress-bar';
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
import type { ComponentMeta } from '@storybook/react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
NavDropdownMenu,
|
||||||
|
NavDropdownMenuContent,
|
||||||
|
NavDropdownMenuItem,
|
||||||
|
NavDropdownMenuTrigger,
|
||||||
|
} from './dropdown-menu';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'NavDropdownMenu',
|
||||||
|
} as ComponentMeta<typeof NavDropdownMenu>;
|
||||||
|
|
||||||
|
export const RadioItems = () => {
|
||||||
|
return (
|
||||||
|
<div style={{ textAlign: 'center', padding: 50 }}>
|
||||||
|
<NavDropdownMenu>
|
||||||
|
<NavDropdownMenuTrigger>
|
||||||
|
<span>Open</span>
|
||||||
|
</NavDropdownMenuTrigger>
|
||||||
|
<NavDropdownMenuContent>
|
||||||
|
<NavDropdownMenuItem onSelect={() => console.log('minimize')}>
|
||||||
|
Minimize window
|
||||||
|
</NavDropdownMenuItem>
|
||||||
|
<NavDropdownMenuItem onSelect={() => console.log('zoom')}>
|
||||||
|
Zoom
|
||||||
|
</NavDropdownMenuItem>
|
||||||
|
<NavDropdownMenuItem onSelect={() => console.log('smaller')}>
|
||||||
|
Smaller
|
||||||
|
</NavDropdownMenuItem>
|
||||||
|
</NavDropdownMenuContent>
|
||||||
|
</NavDropdownMenu>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,74 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import { Icon } from '../icon';
|
||||||
|
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
const itemClass = classNames(
|
||||||
|
'relative flex items-center justify-between rounded-sm p-2 text-sm',
|
||||||
|
'cursor-default hover:cursor-pointer',
|
||||||
|
'hover:white dark:hover:white',
|
||||||
|
'focus:white dark:focus:white',
|
||||||
|
'select-none',
|
||||||
|
'whitespace-nowrap'
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains all the parts of a dropdown menu.
|
||||||
|
*/
|
||||||
|
export const NavDropdownMenu = DropdownMenuPrimitive.Root;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The button that toggles the dropdown menu.
|
||||||
|
* By default, the {@link NavDropdownMenuContent} will position itself against the trigger.
|
||||||
|
*/
|
||||||
|
export const NavDropdownMenuTrigger = forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Trigger>,
|
||||||
|
React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, forwardedRef) => {
|
||||||
|
const triggerClasses = classNames(
|
||||||
|
className,
|
||||||
|
'bg-transparent whitespace-nowrap'
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<DropdownMenuPrimitive.Trigger
|
||||||
|
asChild={true}
|
||||||
|
ref={forwardedRef}
|
||||||
|
className={triggerClasses}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="h-full">
|
||||||
|
{children} <Icon name="arrow-down" className="ml-2" />
|
||||||
|
</span>
|
||||||
|
</DropdownMenuPrimitive.Trigger>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component that pops out when the dropdown menu is open.
|
||||||
|
*/
|
||||||
|
export const NavDropdownMenuContent = forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentProps<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, ...contentProps }, forwardedRef) => (
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
{...contentProps}
|
||||||
|
ref={forwardedRef}
|
||||||
|
className="min-w-[290px] bg-neutral-200 dark:bg-neutral-900 mt-4 p-2 rounded-xl border border-neutral-700 text-white"
|
||||||
|
align="start"
|
||||||
|
sideOffset={10}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The component that contains the dropdown menu items.
|
||||||
|
*/
|
||||||
|
export const NavDropdownMenuItem = forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentProps<typeof DropdownMenuPrimitive.Item>
|
||||||
|
>(({ className, ...itemProps }, forwardedRef) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
{...itemProps}
|
||||||
|
ref={forwardedRef}
|
||||||
|
className={classNames(itemClass, className)}
|
||||||
|
/>
|
||||||
|
));
|
1
libs/ui-toolkit/src/components/nav-dropdown/index.ts
Normal file
1
libs/ui-toolkit/src/components/nav-dropdown/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './dropdown-menu';
|
@ -2,16 +2,18 @@ import classNames from 'classnames';
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
export function getNavLinkClassNames(
|
export function getNavLinkClassNames(
|
||||||
navbarTheme: string
|
navbarTheme: string,
|
||||||
|
fullWidth = false
|
||||||
): (props: { isActive?: boolean }) => string | undefined {
|
): (props: { isActive?: boolean }) => string | undefined {
|
||||||
return ({ isActive = false }) => {
|
return ({ isActive = false }) => {
|
||||||
return getActiveNavLinkClassNames(isActive, navbarTheme);
|
return getActiveNavLinkClassNames(isActive, navbarTheme, fullWidth);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getActiveNavLinkClassNames = (
|
export const getActiveNavLinkClassNames = (
|
||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
navbarTheme: string
|
navbarTheme: string,
|
||||||
|
fullWidth = false
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
return classNames('mx-2 py-3 self-end relative', {
|
return classNames('mx-2 py-3 self-end relative', {
|
||||||
'cursor-default': isActive,
|
'cursor-default': isActive,
|
||||||
@ -20,6 +22,7 @@ export const getActiveNavLinkClassNames = (
|
|||||||
!isActive && navbarTheme !== 'yellow',
|
!isActive && navbarTheme !== 'yellow',
|
||||||
'text-black': isActive && navbarTheme === 'yellow',
|
'text-black': isActive && navbarTheme === 'yellow',
|
||||||
'text-black/60 hover:text-black': !isActive && navbarTheme === 'yellow',
|
'text-black/60 hover:text-black': !isActive && navbarTheme === 'yellow',
|
||||||
|
'flex-1': fullWidth,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user