Feat/305 add console v2 first view screen (#424)
* [#305] add initial landing dialog on markets page and fix some typos * [#305] market-list utils and generate schema * [#305] initial styling of the landing dialog and add arrows * [#305] routing to markets and add hover and market list tests * [#305] fix z-index on dialog overlay * [#305] default market shoulde be oldest market that is currently trading in continuous mode * [#305] refactor market-list library * [#305] add arrow unit tests * Update libs/market-list/src/lib/components/landing/landing-dialog.tsx Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> * Update libs/market-list/src/lib/components/landing/select-market-list.tsx Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> * Update libs/market-list/src/lib/components/landing/select-market-list.tsx Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> * test: fix failing tests from homepage change * [#305] sort by id after sorting by date * test: increase timeout for failing tests in CI * [#305] destructuring all over the place and some code tweaks, arrows and percentage changes * [#305] update sparkline to show colour * [#305] fix order of market list * [#305] stretchedLink class plus a-tag href for navigation - accessibility updates * [#305] use href only and remove log * [#305] use bignumber.js for price calculations * [#305] change to bg-white/50 on dark mode overlay as asked from UX * [#305] change to bg-white/50 on dark mode overlay as asked from UX * [#305] toLocaleString fix * [#305] toLocaleString fix * [#305] add price-change-cell and use formatNumber * [#305] add extra test for select market list * Update apps/trading/specs/index.spec.tsx Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> * [#305] use memo, sort by date and id lodash Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> Co-authored-by: Joe <joe@vega.xyz>
This commit is contained in:
parent
a6a21bed46
commit
9ab6337e42
10
README.md
10
README.md
@ -67,6 +67,16 @@ Run `nx build my-app` to build the project. The build artifacts will be stored i
|
||||
|
||||
Run `nx serve my-app` for a dev server. Navigate to the port specified in `app/<project-name>/project.json`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
### Using Apollo GraphQL and Generate Types
|
||||
|
||||
In order to generate the schemas for your GraphQL queries, you can run `nx run types:generate`.
|
||||
If it is the first time you are running the command, make sure you are setting up the environment variable from `apollo.config.js`.
|
||||
|
||||
```bash
|
||||
export NX_VEGA_URL=https://lb.testnet.vega.xyz/query
|
||||
yarn nx run types:generate
|
||||
```
|
||||
|
||||
### Running tests
|
||||
|
||||
Run `yarn nx run <my-app>-e2e:e2e` to execute the e2e tests with [cypress](https://docs.cypress.io/), or `nx affected:e2e` will execute just the end-to-end tests affected by a change. You can use the `--watch` flag to open the cypress tests UI in watch mode, see [cypress executor](https://nx.dev/packages/cypress/executors/cypress) for all CLI flags.
|
||||
|
@ -1,7 +1,14 @@
|
||||
export default class BasePage {
|
||||
closeDialogBtn = 'dialog-close';
|
||||
porfolioUrl = '/portfolio';
|
||||
marketsUrl = '/markets';
|
||||
|
||||
closeDialog() {
|
||||
cy.getByTestId(this.closeDialogBtn, { timeout: 8000 }).click({
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
navigateToPortfolio() {
|
||||
cy.get(`a[href='${this.porfolioUrl}']`).should('be.visible').click();
|
||||
cy.url().should('include', '/portfolio');
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { Given } from 'cypress-cucumber-preprocessor/steps';
|
||||
import BasePage from '../pages/base-page';
|
||||
|
||||
const basePage = new BasePage();
|
||||
|
||||
Given('I am on the homepage', () => {
|
||||
cy.visit('/');
|
||||
basePage.closeDialog();
|
||||
});
|
||||
|
@ -7,7 +7,7 @@ interface AppLoaderProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Component to handle any app initialization, startup querys and other things
|
||||
* Component to handle any app initialization, startup queries and other things
|
||||
* that must happen for it can be used
|
||||
*/
|
||||
export function AppLoader({ children }: AppLoaderProps) {
|
||||
|
41
apps/trading/pages/__generated__/MarketsLanding.ts
generated
Normal file
41
apps/trading/pages/__generated__/MarketsLanding.ts
generated
Normal file
@ -0,0 +1,41 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { MarketTradingMode } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: MarketsLanding
|
||||
// ====================================================
|
||||
|
||||
export interface MarketsLanding_markets_marketTimestamps {
|
||||
__typename: "MarketTimestamps";
|
||||
/**
|
||||
* Time when the market is open and ready to accept trades
|
||||
*/
|
||||
open: string | null;
|
||||
}
|
||||
|
||||
export interface MarketsLanding_markets {
|
||||
__typename: "Market";
|
||||
/**
|
||||
* Market ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Current mode of execution of the market
|
||||
*/
|
||||
tradingMode: MarketTradingMode;
|
||||
/**
|
||||
* timestamps for state changes in the market
|
||||
*/
|
||||
marketTimestamps: MarketsLanding_markets_marketTimestamps;
|
||||
}
|
||||
|
||||
export interface MarketsLanding {
|
||||
/**
|
||||
* One or more instruments that are trading on the VEGA network
|
||||
*/
|
||||
markets: MarketsLanding_markets[] | null;
|
||||
}
|
@ -1,24 +1,44 @@
|
||||
import { Button, Callout, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { LandingDialog } from '@vegaprotocol/market-list';
|
||||
import { MarketTradingMode } from '@vegaprotocol/types';
|
||||
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import MarketPage from './markets/[marketId].page';
|
||||
import type { MarketsLanding } from './__generated__/MarketsLanding';
|
||||
|
||||
const MARKETS_QUERY = gql`
|
||||
query MarketsLanding {
|
||||
markets {
|
||||
id
|
||||
tradingMode
|
||||
marketTimestamps {
|
||||
open
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const marketList = ({ markets }: MarketsLanding) =>
|
||||
sortBy(
|
||||
markets?.filter(
|
||||
({ marketTimestamps, tradingMode }) =>
|
||||
marketTimestamps.open && tradingMode === MarketTradingMode.Continuous
|
||||
) || [],
|
||||
'marketTimestamps.open',
|
||||
'id'
|
||||
);
|
||||
|
||||
export function Index() {
|
||||
// The default market selected in the platform behind the overlay
|
||||
// should be the oldest market that is currently trading in continuous mode(i.e. not in auction).
|
||||
const { data, error, loading } = useQuery<MarketsLanding>(MARKETS_QUERY);
|
||||
return (
|
||||
<div className="m-24">
|
||||
<div className="mb-24">
|
||||
<Callout
|
||||
intent={Intent.Help}
|
||||
title="Welcome to Vega Trading App"
|
||||
iconName="endorsed"
|
||||
headingLevel={1}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<div>With a longer explaination</div>
|
||||
<Button className="block mt-8" variant="secondary">
|
||||
Action
|
||||
</Button>
|
||||
</div>
|
||||
</Callout>
|
||||
</div>
|
||||
</div>
|
||||
<>
|
||||
<LandingDialog />
|
||||
<AsyncRenderer data={data} error={error} loading={loading}>
|
||||
<MarketPage id={data && marketList(data)[0]?.id} />
|
||||
</AsyncRenderer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -18,14 +18,13 @@ const MARKET_QUERY = gql`
|
||||
}
|
||||
`;
|
||||
|
||||
const MarketPage = () => {
|
||||
const MarketPage = ({ id }: { id?: string }) => {
|
||||
const { query } = useRouter();
|
||||
const { w } = useWindowSize();
|
||||
|
||||
// Default to first marketId query item if found
|
||||
const marketId = Array.isArray(query.marketId)
|
||||
? query.marketId[0]
|
||||
: query.marketId;
|
||||
const marketId =
|
||||
id || (Array.isArray(query.marketId) ? query.marketId[0] : query.marketId);
|
||||
|
||||
if (!marketId) {
|
||||
return (
|
||||
|
@ -23,7 +23,7 @@ export const GridTabs = ({ children, group }: GridTabsProps) => {
|
||||
});
|
||||
|
||||
// Update the query string in the url when the active tab changes
|
||||
// uses group property as the query stirng key
|
||||
// uses group property as the query string key
|
||||
useEffect(() => {
|
||||
const [url, queryString] = asPath.split('?');
|
||||
const searchParams = new URLSearchParams(queryString);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { act, render } from '@testing-library/react';
|
||||
import Index from '../pages/index.page';
|
||||
import { MockedProvider } from '@apollo/react-testing';
|
||||
|
||||
jest.mock('@vegaprotocol/ui-toolkit', () => {
|
||||
const original = jest.requireActual('@vegaprotocol/ui-toolkit');
|
||||
@ -10,9 +11,26 @@ jest.mock('@vegaprotocol/ui-toolkit', () => {
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('next/router', () => ({
|
||||
useRouter() {
|
||||
return {
|
||||
route: '/',
|
||||
pathname: '',
|
||||
query: '',
|
||||
asPath: '',
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Index', () => {
|
||||
it('should render successfully', () => {
|
||||
render(<Index />);
|
||||
expect(true).toBeTruthy();
|
||||
it('should render successfully', async () => {
|
||||
act(() => {
|
||||
const { baseElement } = render(
|
||||
<MockedProvider>
|
||||
<Index />
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(baseElement).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -143,7 +143,7 @@ export const compactData = (
|
||||
),
|
||||
[]
|
||||
);
|
||||
// order by price, it's safe to cast to number price diff sould not exceed Number.MAX_SAFE_INTEGER
|
||||
// order by price, it's safe to cast to number price diff should not exceed Number.MAX_SAFE_INTEGER
|
||||
orderbookData.sort((a, b) => Number(BigInt(b.price) - BigInt(a.price)));
|
||||
// count cumulative volumes
|
||||
if (orderbookData.length > 1) {
|
||||
|
@ -1,5 +1,2 @@
|
||||
export * from './lib/market-list-table';
|
||||
export * from './lib/markets-container';
|
||||
export * from './lib/__generated__/Markets';
|
||||
export * from './lib/__generated__/MarketDataFields';
|
||||
export * from './lib/__generated__/MarketDataSub';
|
||||
export * from './lib/components';
|
||||
export * from './lib/utils';
|
||||
|
103
libs/market-list/src/lib/components/__generated__/MarketList.ts
generated
Normal file
103
libs/market-list/src/lib/components/__generated__/MarketList.ts
generated
Normal file
@ -0,0 +1,103 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
// @generated
|
||||
// This file was automatically generated and should not be edited.
|
||||
|
||||
import { Interval } from "@vegaprotocol/types";
|
||||
|
||||
// ====================================================
|
||||
// GraphQL query operation: MarketList
|
||||
// ====================================================
|
||||
|
||||
export interface MarketList_markets_tradableInstrument_instrument {
|
||||
__typename: "Instrument";
|
||||
/**
|
||||
* Full and fairly descriptive name for the instrument
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* A short non necessarily unique code used to easily describe the instrument (e.g: FX:BTCUSD/DEC18) (string)
|
||||
*/
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface MarketList_markets_tradableInstrument {
|
||||
__typename: "TradableInstrument";
|
||||
/**
|
||||
* An instance of or reference to a fully specified instrument.
|
||||
*/
|
||||
instrument: MarketList_markets_tradableInstrument_instrument;
|
||||
}
|
||||
|
||||
export interface MarketList_markets_marketTimestamps {
|
||||
__typename: "MarketTimestamps";
|
||||
/**
|
||||
* Time when the market is open and ready to accept trades
|
||||
*/
|
||||
open: string | null;
|
||||
/**
|
||||
* Time when the market is closed
|
||||
*/
|
||||
close: string | null;
|
||||
}
|
||||
|
||||
export interface MarketList_markets_candles {
|
||||
__typename: "Candle";
|
||||
/**
|
||||
* Open price (uint64)
|
||||
*/
|
||||
open: string;
|
||||
/**
|
||||
* Close price (uint64)
|
||||
*/
|
||||
close: string;
|
||||
}
|
||||
|
||||
export interface MarketList_markets {
|
||||
__typename: "Market";
|
||||
/**
|
||||
* Market ID
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* decimalPlaces indicates the number of decimal places that an integer must be shifted by in order to get a correct
|
||||
* number denominated in the currency of the Market. (uint64)
|
||||
*
|
||||
* Examples:
|
||||
* Currency Balance decimalPlaces Real Balance
|
||||
* GBP 100 0 GBP 100
|
||||
* GBP 100 2 GBP 1.00
|
||||
* GBP 100 4 GBP 0.01
|
||||
* GBP 1 4 GBP 0.0001 ( 0.01p )
|
||||
*
|
||||
* GBX (pence) 100 0 GBP 1.00 (100p )
|
||||
* GBX (pence) 100 2 GBP 0.01 ( 1p )
|
||||
* GBX (pence) 100 4 GBP 0.0001 ( 0.01p )
|
||||
* GBX (pence) 1 4 GBP 0.000001 ( 0.0001p)
|
||||
*/
|
||||
decimalPlaces: number;
|
||||
/**
|
||||
* An instance of or reference to a tradable instrument.
|
||||
*/
|
||||
tradableInstrument: MarketList_markets_tradableInstrument;
|
||||
/**
|
||||
* timestamps for state changes in the market
|
||||
*/
|
||||
marketTimestamps: MarketList_markets_marketTimestamps;
|
||||
/**
|
||||
* Candles on a market, for the 'last' n candles, at 'interval' seconds as specified by params
|
||||
*/
|
||||
candles: (MarketList_markets_candles | null)[] | null;
|
||||
}
|
||||
|
||||
export interface MarketList {
|
||||
/**
|
||||
* One or more instruments that are trading on the VEGA network
|
||||
*/
|
||||
markets: MarketList_markets[] | null;
|
||||
}
|
||||
|
||||
export interface MarketListVariables {
|
||||
interval: Interval;
|
||||
since: string;
|
||||
}
|
4
libs/market-list/src/lib/components/__generated__/index.ts
generated
Normal file
4
libs/market-list/src/lib/components/__generated__/index.ts
generated
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './MarketDataFields';
|
||||
export * from './MarketDataSub';
|
||||
export * from './MarketList';
|
||||
export * from './Markets';
|
3
libs/market-list/src/lib/components/index.ts
Normal file
3
libs/market-list/src/lib/components/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './__generated__';
|
||||
export * from './landing';
|
||||
export * from './markets-container';
|
2
libs/market-list/src/lib/components/landing/index.ts
Normal file
2
libs/market-list/src/lib/components/landing/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './landing-dialog';
|
||||
export * from './select-market-list';
|
@ -0,0 +1,58 @@
|
||||
import { gql, useQuery } from '@apollo/client';
|
||||
import { Interval } from '@vegaprotocol/types';
|
||||
import { AsyncRenderer, Dialog, Intent } from '@vegaprotocol/ui-toolkit';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { MarketList } from '../__generated__/MarketList';
|
||||
import { SelectMarketList } from './select-market-list';
|
||||
|
||||
const MARKET_LIST_QUERY = gql`
|
||||
query MarketList($interval: Interval!, $since: String!) {
|
||||
markets {
|
||||
id
|
||||
decimalPlaces
|
||||
tradableInstrument {
|
||||
instrument {
|
||||
name
|
||||
code
|
||||
}
|
||||
}
|
||||
marketTimestamps {
|
||||
open
|
||||
close
|
||||
}
|
||||
candles(interval: $interval, since: $since) {
|
||||
open
|
||||
close
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const LandingDialog = () => {
|
||||
const [open, setOpen] = useState(true);
|
||||
const setClose = () => setOpen(false);
|
||||
|
||||
const yesterday = Math.round(new Date().getTime() / 1000) - 24 * 3600;
|
||||
const yTimestamp = new Date(yesterday * 1000).toISOString();
|
||||
|
||||
const { data, loading, error } = useQuery<MarketList>(MARKET_LIST_QUERY, {
|
||||
variables: { interval: Interval.I1H, since: yTimestamp },
|
||||
});
|
||||
|
||||
return (
|
||||
<AsyncRenderer loading={loading} error={error} data={data}>
|
||||
{
|
||||
<Dialog
|
||||
title="Select a market to get started"
|
||||
intent={Intent.Prompt}
|
||||
open={open}
|
||||
onChange={setClose}
|
||||
titleClassNames="font-bold font-sans text-3xl tracking-tight mb-0 pl-8"
|
||||
>
|
||||
<SelectMarketList data={data} />
|
||||
</Dialog>
|
||||
}
|
||||
</AsyncRenderer>
|
||||
);
|
||||
};
|
@ -0,0 +1,120 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import type { MarketList } from '../__generated__/MarketList';
|
||||
import { SelectMarketList } from './select-market-list';
|
||||
|
||||
describe('SelectMarketList', () => {
|
||||
it('should render', () => {
|
||||
render(<SelectMarketList data={mockData.data as MarketList} />);
|
||||
expect(screen.getByText('AAPL.MF21')).toBeTruthy();
|
||||
expect(screen.getByText('-3.14%')).toBeTruthy();
|
||||
expect(screen.getByText('141.75')).toBeTruthy();
|
||||
expect(screen.getByText('Or view full market list')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
const mockData = {
|
||||
data: {
|
||||
markets: [
|
||||
{
|
||||
__typename: 'Market',
|
||||
id: '062ddcb97beae5b7cc4fa20621fe0c83b2a6f7e76cf5b129c6bd3dc14e8111ef',
|
||||
decimalPlaces: 2,
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'APEUSD (May 2022)',
|
||||
code: 'APEUSD',
|
||||
},
|
||||
},
|
||||
marketTimestamps: {
|
||||
__typename: 'MarketTimestamps',
|
||||
open: '2022-05-18T13:08:27.693537312Z',
|
||||
close: null,
|
||||
},
|
||||
candles: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '822',
|
||||
close: '798',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '793',
|
||||
close: '792',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '794',
|
||||
close: '776',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '785',
|
||||
close: '786',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '803',
|
||||
close: '770',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '785',
|
||||
close: '774',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
__typename: 'Market',
|
||||
id: '3e6671566ccf5c33702e955fe8b018683fcdb812bfe3ed283fc250bb4f798ff3',
|
||||
decimalPlaces: 5,
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'Apple Monthly (30 Jun 2022)',
|
||||
code: 'AAPL.MF21',
|
||||
},
|
||||
},
|
||||
marketTimestamps: {
|
||||
__typename: 'MarketTimestamps',
|
||||
open: '2022-05-18T13:00:39.328347732Z',
|
||||
close: null,
|
||||
},
|
||||
candles: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '14707175',
|
||||
close: '14633864',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '14658400',
|
||||
close: '14550193',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '14550193',
|
||||
close: '14373526',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '14307141',
|
||||
close: '14339846',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '14357485',
|
||||
close: '14179971',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '14179972',
|
||||
close: '14174855',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
@ -0,0 +1,95 @@
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
PriceCell,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { PriceCellChange, Sparkline } from '@vegaprotocol/ui-toolkit';
|
||||
import { mapDataToMarketList } from '../../utils';
|
||||
import type { MarketList } from '../__generated__/MarketList';
|
||||
|
||||
export interface SelectMarketListProps {
|
||||
data: MarketList | undefined;
|
||||
}
|
||||
|
||||
type CandleClose = Required<string>;
|
||||
|
||||
export const SelectMarketList = ({ data }: SelectMarketListProps) => {
|
||||
const thClassNames = (direction: 'left' | 'right') =>
|
||||
`px-8 text-${direction} font-sans font-normal text-ui-small leading-9 mb-0 text-dark/80 dark:text-white/80`;
|
||||
const tdClassNames =
|
||||
'px-8 font-sans leading-9 capitalize text-ui-small text-right';
|
||||
|
||||
const boldUnderlineClassNames =
|
||||
'px-8 underline font-sans text-base leading-9 font-bold tracking-tight decoration-solid text-ui light:hover:text-black/80 dark:hover:text-white/80';
|
||||
const stretchedLink = `after:content-[''] after:inset-0 after:z-[1] after:absolute after:box-border`;
|
||||
return (
|
||||
<div className="max-h-[40rem] overflow-x-auto">
|
||||
<table className="relative h-full min-w-full whitespace-nowrap">
|
||||
<thead className="sticky top-0 z-10 dark:bg-black bg-white">
|
||||
<tr>
|
||||
<th className={thClassNames('left')}>Market</th>
|
||||
<th className={thClassNames('right')}>Last price</th>
|
||||
<th className={thClassNames('right')}>Change (24h)</th>
|
||||
<th className={thClassNames('right')}></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data &&
|
||||
mapDataToMarketList(data)
|
||||
.slice(0, 12)
|
||||
?.map(({ id, marketName, lastPrice, candles, decimalPlaces }) => {
|
||||
const candlesClose: string[] = candles
|
||||
.map((candle) => candle?.close)
|
||||
.filter((c): c is CandleClose => c !== null);
|
||||
return (
|
||||
<tr
|
||||
key={id}
|
||||
className={`hover:bg-black/20 dark:hover:bg-white/20 cursor-pointer relative`}
|
||||
>
|
||||
<td className={`${boldUnderlineClassNames} relative`}>
|
||||
<a
|
||||
href={`/markets/${id}?portfolio=orders&trade=orderbook&chart=candles`}
|
||||
className={stretchedLink}
|
||||
>
|
||||
{marketName}
|
||||
</a>
|
||||
</td>
|
||||
<td className={tdClassNames}>
|
||||
{lastPrice && (
|
||||
<PriceCell
|
||||
value={BigInt(lastPrice)}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
lastPrice.toString(),
|
||||
decimalPlaces,
|
||||
2
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td className={`${tdClassNames} `}>
|
||||
<PriceCellChange
|
||||
candles={candlesClose}
|
||||
decimalPlaces={decimalPlaces}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-8">
|
||||
{candles && (
|
||||
<Sparkline
|
||||
width={100}
|
||||
height={20}
|
||||
muted={false}
|
||||
data={candlesClose.map((c) => Number(c))}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<a className={`${boldUnderlineClassNames} text-ui-small`} href="/markets">
|
||||
{'Or view full market list'}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export * from './market-list-table';
|
||||
export * from './markets-container';
|
||||
export * from './markets-data-provider';
|
||||
export * from './summary-cell';
|
@ -8,7 +8,10 @@ describe('MarketListTable', () => {
|
||||
await act(async () => {
|
||||
const { baseElement } = render(
|
||||
<MockedProvider>
|
||||
<MarketListTable />
|
||||
<MarketListTable
|
||||
data={[]}
|
||||
onRowClicked={jest.fn((marketId: string) => null)}
|
||||
/>
|
||||
</MockedProvider>
|
||||
);
|
||||
expect(baseElement).toBeTruthy();
|
@ -6,12 +6,12 @@ import {
|
||||
t,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import { AgGridDynamic as AgGrid } from '@vegaprotocol/ui-toolkit';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
import type {
|
||||
Markets_markets,
|
||||
Markets_markets_data_market,
|
||||
} from './__generated__/Markets';
|
||||
import { AgGridColumn } from 'ag-grid-react';
|
||||
import type { AgGridReact } from 'ag-grid-react';
|
||||
} from '../__generated__/Markets';
|
||||
|
||||
interface MarketListTableProps {
|
||||
data: Markets_markets[] | null;
|
@ -9,7 +9,7 @@ import type { AgGridReact } from 'ag-grid-react';
|
||||
import type {
|
||||
Markets_markets,
|
||||
Markets_markets_data,
|
||||
} from './__generated__/Markets';
|
||||
} from '../../components/__generated__/Markets';
|
||||
import { marketsDataProvider } from './markets-data-provider';
|
||||
|
||||
export const MarketsContainer = () => {
|
@ -1,11 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import type { Markets, Markets_markets } from './__generated__/Markets';
|
||||
import type {
|
||||
Markets,
|
||||
Markets_markets,
|
||||
} from '../../components/__generated__/Markets';
|
||||
import { makeDataProvider } from '@vegaprotocol/react-helpers';
|
||||
|
||||
import type {
|
||||
MarketDataSub,
|
||||
MarketDataSub_marketData,
|
||||
} from './__generated__/MarketDataSub';
|
||||
} from '../../components/__generated__/MarketDataSub';
|
||||
|
||||
const MARKET_DATA_FRAGMENT = gql`
|
||||
fragment MarketDataFields on MarketData {
|
1
libs/market-list/src/lib/utils/index.ts
Normal file
1
libs/market-list/src/lib/utils/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './market-list.utils';
|
151
libs/market-list/src/lib/utils/market-list.utils.spec.tsx
Normal file
151
libs/market-list/src/lib/utils/market-list.utils.spec.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
import type { MarketList } from '../components/__generated__/MarketList';
|
||||
import { mapDataToMarketList } from './market-list.utils';
|
||||
|
||||
describe('mapDataToMarketList', () => {
|
||||
it('should map queried data to market list format', () => {
|
||||
const result = mapDataToMarketList(mockData.data as unknown as MarketList);
|
||||
expect(result).toEqual(mockList);
|
||||
});
|
||||
});
|
||||
|
||||
const mockList = [
|
||||
{
|
||||
candles: [
|
||||
{ __typename: 'Candle', close: '14633864', open: '14707175' },
|
||||
{ __typename: 'Candle', close: '14550193', open: '14658400' },
|
||||
{ __typename: 'Candle', close: '14373526', open: '14550193' },
|
||||
{ __typename: 'Candle', close: '14339846', open: '14307141' },
|
||||
{ __typename: 'Candle', close: '14179971', open: '14357485' },
|
||||
{ __typename: 'Candle', close: '14174855', open: '14179972' },
|
||||
],
|
||||
close: null,
|
||||
decimalPlaces: 5,
|
||||
id: '3e6671566ccf5c33702e955fe8b018683fcdb812bfe3ed283fc250bb4f798ff3',
|
||||
lastPrice: '14174855',
|
||||
marketName: 'AAPL.MF21',
|
||||
open: 1652878839328,
|
||||
},
|
||||
{
|
||||
candles: [
|
||||
{ __typename: 'Candle', close: '798', open: '822' },
|
||||
{ __typename: 'Candle', close: '792', open: '793' },
|
||||
{ __typename: 'Candle', close: '776', open: '794' },
|
||||
{ __typename: 'Candle', close: '786', open: '785' },
|
||||
{ __typename: 'Candle', close: '770', open: '803' },
|
||||
{ __typename: 'Candle', close: '774', open: '785' },
|
||||
],
|
||||
close: null,
|
||||
decimalPlaces: 2,
|
||||
id: '062ddcb97beae5b7cc4fa20621fe0c83b2a6f7e76cf5b129c6bd3dc14e8111ef',
|
||||
lastPrice: '774',
|
||||
marketName: 'APEUSD',
|
||||
open: 1652879307693,
|
||||
},
|
||||
];
|
||||
|
||||
const mockData = {
|
||||
data: {
|
||||
markets: [
|
||||
{
|
||||
__typename: 'Market',
|
||||
id: '062ddcb97beae5b7cc4fa20621fe0c83b2a6f7e76cf5b129c6bd3dc14e8111ef',
|
||||
decimalPlaces: 2,
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'APEUSD (May 2022)',
|
||||
code: 'APEUSD',
|
||||
},
|
||||
},
|
||||
marketTimestamps: {
|
||||
__typename: 'MarketTimestamps',
|
||||
open: '2022-05-18T13:08:27.693537312Z',
|
||||
close: null,
|
||||
},
|
||||
candles: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '822',
|
||||
close: '798',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '793',
|
||||
close: '792',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '794',
|
||||
close: '776',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '785',
|
||||
close: '786',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '803',
|
||||
close: '770',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '785',
|
||||
close: '774',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
__typename: 'Market',
|
||||
id: '3e6671566ccf5c33702e955fe8b018683fcdb812bfe3ed283fc250bb4f798ff3',
|
||||
decimalPlaces: 5,
|
||||
tradableInstrument: {
|
||||
__typename: 'TradableInstrument',
|
||||
instrument: {
|
||||
__typename: 'Instrument',
|
||||
name: 'Apple Monthly (30 Jun 2022)',
|
||||
code: 'AAPL.MF21',
|
||||
},
|
||||
},
|
||||
marketTimestamps: {
|
||||
__typename: 'MarketTimestamps',
|
||||
open: '2022-05-18T13:00:39.328347732Z',
|
||||
close: null,
|
||||
},
|
||||
candles: [
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '14707175',
|
||||
close: '14633864',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '14658400',
|
||||
close: '14550193',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '14550193',
|
||||
close: '14373526',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '14307141',
|
||||
close: '14339846',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '14357485',
|
||||
close: '14179971',
|
||||
},
|
||||
{
|
||||
__typename: 'Candle',
|
||||
open: '14179972',
|
||||
close: '14174855',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
31
libs/market-list/src/lib/utils/market-list.utils.ts
Normal file
31
libs/market-list/src/lib/utils/market-list.utils.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import type {
|
||||
MarketList,
|
||||
MarketList_markets,
|
||||
} from '../components/__generated__/MarketList';
|
||||
|
||||
export const lastPrice = ({ candles }: MarketList_markets) =>
|
||||
candles && candles.length > 0
|
||||
? candles && candles[candles?.length - 1]?.close
|
||||
: undefined;
|
||||
|
||||
export const mapDataToMarketList = ({ markets }: MarketList) =>
|
||||
sortBy(
|
||||
markets?.map((m) => {
|
||||
return {
|
||||
id: m.id,
|
||||
decimalPlaces: m.decimalPlaces,
|
||||
marketName: m.tradableInstrument.instrument?.code,
|
||||
lastPrice: lastPrice(m),
|
||||
candles: (m.candles || []).filter((c) => c),
|
||||
open: m.marketTimestamps.open
|
||||
? new Date(m.marketTimestamps.open).getTime()
|
||||
: null,
|
||||
close: m.marketTimestamps.close
|
||||
? new Date(m.marketTimestamps.close).getTime()
|
||||
: null,
|
||||
};
|
||||
}) || [],
|
||||
'open',
|
||||
'id'
|
||||
);
|
@ -40,3 +40,9 @@ export const addDecimalsFormatNumber = (
|
||||
|
||||
return formatNumber(x, formatDecimals);
|
||||
};
|
||||
|
||||
export const formatNumberPercentage = (value: BigNumber, decimals?: number) => {
|
||||
const decimalPlaces =
|
||||
typeof decimals === 'undefined' ? Math.max(value.dp(), 2) : decimals;
|
||||
return `${value.dp(decimalPlaces).toFormat(decimalPlaces)}%`;
|
||||
};
|
||||
|
@ -12,8 +12,6 @@ module.exports = {
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
current: 'currentColor',
|
||||
bullish: '#26FF8A',
|
||||
bearish: '#ED1515',
|
||||
vega: {
|
||||
yellow: '#EDFF22',
|
||||
pink: '#FF2D5E',
|
||||
@ -23,11 +21,12 @@ module.exports = {
|
||||
DEFAULT: '#ED1515',
|
||||
transparent: 'rgba(255, 38, 65, 0.3)',
|
||||
vega: '#FF261A',
|
||||
dark: '#EB001B',
|
||||
},
|
||||
green: {
|
||||
DEFAULT: '#26FF8A',
|
||||
transparent: 'rgba(38, 255, 138, 0.3)',
|
||||
dark: '#246340',
|
||||
dark: '#008545',
|
||||
vega: '#00F780',
|
||||
},
|
||||
text: '#C7C7C7',
|
||||
|
23
libs/ui-toolkit/src/components/arrows/arrow.spec.tsx
Normal file
23
libs/ui-toolkit/src/components/arrows/arrow.spec.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { Arrow } from './arrow';
|
||||
|
||||
describe('Arrow', () => {
|
||||
it('should render successfully for positive values', () => {
|
||||
render(<Arrow value={0.5} />);
|
||||
expect(screen.queryByTestId('arrow-up')).toBeTruthy();
|
||||
expect(screen.queryByTestId('arrow-down')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should render successfully for negative values', () => {
|
||||
render(<Arrow value={-0.5} />);
|
||||
expect(screen.queryByTestId('arrow-down')).toBeTruthy();
|
||||
expect(screen.queryByTestId('arrow-up')).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not render successfully for zero values', () => {
|
||||
render(<Arrow value={0} />);
|
||||
expect(screen.queryByTestId('arrow-down')).toBeNull();
|
||||
expect(screen.queryByTestId('arrow-up')).toBeNull();
|
||||
});
|
||||
});
|
20
libs/ui-toolkit/src/components/arrows/arrow.tsx
Normal file
20
libs/ui-toolkit/src/components/arrows/arrow.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
export const ArrowUp = () => (
|
||||
<span
|
||||
data-testid="arrow-up"
|
||||
className="w-0 h-0 border-x border-x-[4px] border-solid border-x-transparent border-b-[4px] border-b-green-dark dark:border-b-green"
|
||||
></span>
|
||||
);
|
||||
export const ArrowDown = () => (
|
||||
<span
|
||||
data-testid="arrow-down"
|
||||
className="w-0 h-0 border-x border-x-[4px] border-solid border-x-transparent border-t-[4px] border-t-red-dark dark:border-t-red"
|
||||
></span>
|
||||
);
|
||||
|
||||
// Arrow
|
||||
export interface ArrowProps {
|
||||
value: number | bigint;
|
||||
}
|
||||
|
||||
export const Arrow = ({ value }: ArrowProps) =>
|
||||
value === 0 ? null : value > 0 ? <ArrowUp /> : <ArrowDown />;
|
1
libs/ui-toolkit/src/components/arrows/index.ts
Normal file
1
libs/ui-toolkit/src/components/arrows/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './arrow';
|
@ -24,7 +24,6 @@ export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
open: false,
|
||||
title: 'Title',
|
||||
setOpen: () => undefined,
|
||||
children: <p>Some content</p>,
|
||||
};
|
||||
|
||||
@ -32,7 +31,6 @@ export const Danger = Template.bind({});
|
||||
Danger.args = {
|
||||
open: false,
|
||||
title: 'Danger',
|
||||
setOpen: () => undefined,
|
||||
children: <p>Some content</p>,
|
||||
intent: Intent.Danger,
|
||||
};
|
||||
@ -41,7 +39,6 @@ export const Success = Template.bind({});
|
||||
Success.args = {
|
||||
open: false,
|
||||
title: 'Success',
|
||||
setOpen: () => undefined,
|
||||
children: <p>Some content</p>,
|
||||
intent: Intent.Success,
|
||||
};
|
||||
@ -50,7 +47,14 @@ export const Warning = Template.bind({});
|
||||
Warning.args = {
|
||||
open: false,
|
||||
title: 'Warning',
|
||||
setOpen: () => undefined,
|
||||
children: <p>Some content</p>,
|
||||
intent: Intent.Warning,
|
||||
};
|
||||
|
||||
export const Modal = Template.bind({});
|
||||
Modal.args = {
|
||||
open: false,
|
||||
title: 'Modal (Prompt)',
|
||||
children: <p>Some content</p>,
|
||||
intent: Intent.Prompt,
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ interface DialogProps {
|
||||
onChange: (isOpen: boolean) => void;
|
||||
title?: string;
|
||||
intent?: Intent;
|
||||
titleClassNames?: string;
|
||||
}
|
||||
|
||||
export function Dialog({
|
||||
@ -19,10 +20,11 @@ export function Dialog({
|
||||
onChange,
|
||||
title,
|
||||
intent,
|
||||
titleClassNames,
|
||||
}: DialogProps) {
|
||||
const contentClasses = classNames(
|
||||
// Positions the modal in the center of screen
|
||||
'fixed w-[520px] px-28 py-24 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
||||
'z-20 fixed w-full md:w-[520px] px-28 py-24 top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%]',
|
||||
// Need to apply background and text colors again as content is rendered in a portal
|
||||
'dark:bg-black dark:text-white-95 bg-white text-black-95',
|
||||
getIntentShadow(intent)
|
||||
@ -30,7 +32,7 @@ export function Dialog({
|
||||
return (
|
||||
<DialogPrimitives.Root open={open} onOpenChange={(x) => onChange(x)}>
|
||||
<DialogPrimitives.Portal>
|
||||
<DialogPrimitives.Overlay className="fixed inset-0 bg-black/50 dark:bg-white/15" />
|
||||
<DialogPrimitives.Overlay className="fixed inset-0 bg-black/50 z-10" />
|
||||
<DialogPrimitives.Content className={contentClasses}>
|
||||
<DialogPrimitives.Close
|
||||
className="p-12 absolute top-0 right-0"
|
||||
@ -39,7 +41,9 @@ export function Dialog({
|
||||
<Icon name="cross" />
|
||||
</DialogPrimitives.Close>
|
||||
{title && (
|
||||
<h1 className="text-h5 text-black-95 dark:text-white-95 mt-0 mb-20">
|
||||
<h1
|
||||
className={`text-h5 text-black-95 dark:text-white-95 mt-0 mb-20 ${titleClassNames}`}
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
)}
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from './ag-grid';
|
||||
export * from './arrows';
|
||||
export * from './async-renderer';
|
||||
export * from './button';
|
||||
export * from './callout';
|
||||
@ -15,13 +16,14 @@ export * from './input-error';
|
||||
export * from './key-value-table';
|
||||
export * from './loader';
|
||||
export * from './lozenge';
|
||||
export * from './price-change';
|
||||
export * from './radio-group';
|
||||
export * from './select';
|
||||
export * from './sparkline';
|
||||
export * from './splash';
|
||||
export * from './syntax-highlighter';
|
||||
export * from './text-area';
|
||||
export * from './theme-switcher';
|
||||
export * from './toggle';
|
||||
export * from './tooltip';
|
||||
export * from './vega-logo';
|
||||
export * from './syntax-highlighter';
|
||||
export * from './sparkline';
|
||||
|
1
libs/ui-toolkit/src/components/price-change/index.ts
Normal file
1
libs/ui-toolkit/src/components/price-change/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './price-change-cell';
|
@ -0,0 +1,21 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { PriceCellChange } from '..';
|
||||
|
||||
describe('PriceChangeCell', () => {
|
||||
it('renders correctly and calculates the price change', () => {
|
||||
render(
|
||||
<PriceCellChange
|
||||
candles={['45556', '678678', '23456']}
|
||||
decimalPlaces={3}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('-48.51%')).toBeInTheDocument();
|
||||
expect(screen.getByText('-22.100')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correctly and calculates the price change without decimals', () => {
|
||||
render(<PriceCellChange candles={['45556', '678678', '23456']} />);
|
||||
expect(screen.getByText('-48.51%')).toBeInTheDocument();
|
||||
expect(screen.getByText('-22,100.000')).toBeInTheDocument();
|
||||
});
|
||||
});
|
@ -0,0 +1,23 @@
|
||||
import type { Story, Meta } from '@storybook/react';
|
||||
import { PriceCellChange } from './price-change-cell';
|
||||
|
||||
export default {
|
||||
component: PriceCellChange,
|
||||
title: 'PriceCellChange',
|
||||
} as Meta;
|
||||
|
||||
const Template: Story = (args) => (
|
||||
<PriceCellChange candles={[args['candles']]} {...args} />
|
||||
);
|
||||
|
||||
export const Increased = Template.bind({});
|
||||
Increased.args = {
|
||||
candles: ['4564', '5674', '6784'],
|
||||
decimalPlaces: 3,
|
||||
};
|
||||
|
||||
export const Decreased = Template.bind({});
|
||||
Decreased.args = {
|
||||
candles: ['6784', '4564', '5674'],
|
||||
decimalPlaces: 3,
|
||||
};
|
@ -0,0 +1,91 @@
|
||||
import {
|
||||
addDecimalsFormatNumber,
|
||||
formatNumberPercentage,
|
||||
PriceCell,
|
||||
} from '@vegaprotocol/react-helpers';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import React from 'react';
|
||||
|
||||
import { Arrow } from '../arrows/arrow';
|
||||
|
||||
export interface PriceChangeCellProps {
|
||||
/** either candle `close`or `open` values to be filtered and used here in order to calculate the price change */
|
||||
candles: string[];
|
||||
decimalPlaces?: number;
|
||||
}
|
||||
|
||||
export const priceChangePercentage = (candles: string[]) => {
|
||||
const change = priceChange(candles);
|
||||
if (change && candles && candles.length > 0) {
|
||||
const yesterdayLastPrice = candles[0] && BigInt(candles[0]);
|
||||
if (yesterdayLastPrice) {
|
||||
return new BigNumber(change.toString())
|
||||
.dividedBy(new BigNumber(yesterdayLastPrice.toString()))
|
||||
.multipliedBy(100)
|
||||
.toNumber();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const priceChange = (candles: string[]) => {
|
||||
return candles &&
|
||||
candles[candles.length - 1] !== undefined &&
|
||||
candles[0] !== undefined
|
||||
? BigInt(candles[candles.length - 1] ?? 0) - BigInt(candles[0] ?? 0)
|
||||
: 0;
|
||||
};
|
||||
|
||||
const priceChangeClassNames = (value: number | bigint) =>
|
||||
value === 0
|
||||
? 'text-black dark:text-white'
|
||||
: value > 0
|
||||
? `text-green-dark dark:text-green-vega `
|
||||
: `text-red-dark dark:text-red-vega`;
|
||||
|
||||
export const PriceCellChange = React.memo(
|
||||
({ candles, decimalPlaces }: PriceChangeCellProps) => {
|
||||
const change = priceChange(candles);
|
||||
const changePercentage = priceChangePercentage(candles);
|
||||
return (
|
||||
<span
|
||||
className={`${priceChangeClassNames(
|
||||
change
|
||||
)} flex items-center gap-4 justify-end`}
|
||||
>
|
||||
<Arrow value={change} />
|
||||
<span className="flex items-center gap-6">
|
||||
<span>
|
||||
{
|
||||
<PriceCell
|
||||
value={changePercentage}
|
||||
valueFormatted={formatNumberPercentage(
|
||||
new BigNumber(changePercentage.toString()),
|
||||
2
|
||||
)}
|
||||
/>
|
||||
}
|
||||
|
||||
</span>
|
||||
<span>
|
||||
(
|
||||
{
|
||||
<PriceCell
|
||||
value={BigInt(change)}
|
||||
valueFormatted={addDecimalsFormatNumber(
|
||||
change.toString(),
|
||||
decimalPlaces ?? 0,
|
||||
3
|
||||
)}
|
||||
/>
|
||||
}
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
PriceCellChange.displayName = 'PriceCellChange';
|
@ -7,6 +7,7 @@ const props = {
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 12,
|
||||
],
|
||||
muted: true,
|
||||
};
|
||||
|
||||
it('Renders an svg with a single path', () => {
|
||||
@ -17,7 +18,7 @@ it('Renders an svg with a single path', () => {
|
||||
expect(path).toBeInTheDocument();
|
||||
expect(path).toHaveAttribute('d', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke-width', '2');
|
||||
expect(path).toHaveAttribute('stroke-width', '1');
|
||||
expect(path).toHaveAttribute('fill', 'transparent');
|
||||
});
|
||||
|
||||
@ -34,16 +35,21 @@ it('Renders a red line if the last value is less than the first', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass('stroke-bearish');
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-red-dark dark:stroke-red'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders a green line if the last value is greater than the first', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 10;
|
||||
props.muted = true;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass('stroke-bullish');
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-green-dark dark:stroke-green'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders a white line if the first and last values are equal', () => {
|
||||
|
@ -8,8 +8,8 @@ function colorByChange(a: number, b: number) {
|
||||
return a === b
|
||||
? 'stroke-black/40 dark:stroke-white/40'
|
||||
: a < b
|
||||
? 'stroke-bullish'
|
||||
: 'stroke-bearish';
|
||||
? 'stroke-green-dark dark:stroke-green'
|
||||
: 'stroke-red-dark dark:stroke-red';
|
||||
}
|
||||
|
||||
export interface SparklineProps {
|
||||
@ -18,6 +18,7 @@ export interface SparklineProps {
|
||||
height?: number;
|
||||
points?: number;
|
||||
className?: string;
|
||||
muted?: boolean;
|
||||
}
|
||||
|
||||
export const SparklineView = ({
|
||||
@ -25,6 +26,7 @@ export const SparklineView = ({
|
||||
width = 60,
|
||||
height = 15,
|
||||
points = 25,
|
||||
muted = false,
|
||||
className,
|
||||
}: SparklineProps) => {
|
||||
// How many points are missing. If market is 12 hours old the 25 - 12
|
||||
@ -69,10 +71,11 @@ export const SparklineView = ({
|
||||
|
||||
// Get the color of the marketData line
|
||||
const [firstVal, lastVal] = [data[0], data[data.length - 1]];
|
||||
const strokeClassName =
|
||||
data.length >= 24
|
||||
const strokeClassName = muted
|
||||
? data.length >= 24
|
||||
? colorByChange(firstVal, lastVal)
|
||||
: 'stroke-black/40 dark:stroke-white/40';
|
||||
: 'stroke-black/40 dark:stroke-white/40'
|
||||
: colorByChange(firstVal, lastVal);
|
||||
|
||||
// Create paths
|
||||
const preMarketCreationPath = lineSeries(preMarketData);
|
||||
@ -93,7 +96,7 @@ export const SparklineView = ({
|
||||
className={`[vector-effect:non-scaling-stroke] ${strokeClassName}`}
|
||||
d={preMarketCreationPath}
|
||||
stroke="strokeCurrent"
|
||||
strokeWidth={2}
|
||||
strokeWidth={1}
|
||||
fill="transparent"
|
||||
/>
|
||||
)}
|
||||
@ -103,7 +106,7 @@ export const SparklineView = ({
|
||||
d={mainPath}
|
||||
className={strokeClassName}
|
||||
stroke="strokeCurrent"
|
||||
strokeWidth={1}
|
||||
strokeWidth={2}
|
||||
fill="transparent"
|
||||
/>
|
||||
)}
|
||||
|
Loading…
Reference in New Issue
Block a user