Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36061a8b1e | ||
|
|
3ac5a62be1 | ||
|
|
f8acb100ba | ||
|
|
6044b86ff4 | ||
|
|
341d78e761 |
@@ -1,147 +0,0 @@
|
||||
.pre-loader {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.pre-loader .loader-item {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #000;
|
||||
}
|
||||
.pre-loader .pre-loader-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.pre-loader .pre-loader-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(0) {
|
||||
animation-delay: 0ms;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:first-child {
|
||||
animation-delay: -0.1s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(2) {
|
||||
animation-delay: 0.3s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(3) {
|
||||
animation-delay: -0.45s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(4) {
|
||||
animation-delay: 1s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(5) {
|
||||
animation-delay: -0.75s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(6) {
|
||||
animation-delay: 0.9s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(7) {
|
||||
animation-delay: -1.4s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(8) {
|
||||
animation-delay: 1.6s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(9) {
|
||||
animation-delay: -0.45s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(10) {
|
||||
animation-delay: 1.5s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(11) {
|
||||
animation-delay: -2.75s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(12) {
|
||||
animation-delay: 1.2s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(13) {
|
||||
animation-delay: -1.95s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(14) {
|
||||
animation-delay: 2.8s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(15) {
|
||||
animation-delay: -0.75s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(16) {
|
||||
animation-delay: 4s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(17) {
|
||||
animation-delay: -0.85s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(18) {
|
||||
animation-delay: 1.8s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(19) {
|
||||
animation-delay: -1.9s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(20) {
|
||||
animation-delay: 5s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(21) {
|
||||
animation-delay: -5.25s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(22) {
|
||||
animation-delay: 4.4s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(23) {
|
||||
animation-delay: -5.75s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(24) {
|
||||
animation-delay: 4.8s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(25) {
|
||||
animation-delay: -5s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item {
|
||||
animation: flickering 0.4s linear infinite alternate;
|
||||
}
|
||||
@keyframes flickering {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
26% {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
html.dark .pre-loader .loader-item {
|
||||
background: #fff;
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
.pre-loader {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
.loader-item {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: black;
|
||||
}
|
||||
.pre-loader-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.pre-loader-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@for $i from 0 through 25 {
|
||||
.loader-item:nth-child(#{$i}) {
|
||||
@if $i % 2 == 0 {
|
||||
animation-delay: #{$i * 50 * random(5)}ms;
|
||||
animation-direction: reverse;
|
||||
} @else {
|
||||
animation-delay: #{$i * -50 * random(5)}ms;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
}
|
||||
}
|
||||
.loader-item {
|
||||
animation: flickering 0.4s linear alternate infinite;
|
||||
}
|
||||
@keyframes flickering {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
26% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
html.dark {
|
||||
.pre-loader {
|
||||
.loader-item {
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,7 +43,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
const asset = getAsset(market);
|
||||
|
||||
return (
|
||||
<div className="flex gap-8">
|
||||
<>
|
||||
<HeaderStat heading={t('Mark Price')} testId="market-price">
|
||||
<MarketMarkPrice
|
||||
marketId={market.id}
|
||||
@@ -144,7 +144,7 @@ export const MarketHeaderStats = ({ market }: MarketHeaderStatsProps) => {
|
||||
</HeaderStat>
|
||||
)}
|
||||
<MarketProposalNotification marketId={market.id} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+4
-8
@@ -1,6 +1,7 @@
|
||||
import type { InMemoryCacheConfig } from '@apollo/client';
|
||||
import {
|
||||
AppFailure,
|
||||
AppLoader,
|
||||
DocsLinks,
|
||||
NetworkLoader,
|
||||
NodeGuard,
|
||||
@@ -9,15 +10,10 @@ import {
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { MaintenancePage } from '@vegaprotocol/ui-toolkit';
|
||||
import { VegaWalletProvider } from '@vegaprotocol/wallet';
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Web3Provider } from './web3-provider';
|
||||
|
||||
export const DynamicLoader = dynamic(() => import('../preloader/preloader'), {
|
||||
loading: () => <>Loading...</>,
|
||||
});
|
||||
|
||||
export const AppLoader = ({ children }: { children: ReactNode }) => {
|
||||
export const Bootstrapper = ({ children }: { children: ReactNode }) => {
|
||||
const {
|
||||
error,
|
||||
VEGA_URL,
|
||||
@@ -47,13 +43,13 @@ export const AppLoader = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<NetworkLoader
|
||||
cache={cacheConfig}
|
||||
skeleton={<DynamicLoader />}
|
||||
skeleton={<AppLoader />}
|
||||
failure={
|
||||
<AppFailure title={t('Could not initialize app')} error={error} />
|
||||
}
|
||||
>
|
||||
<NodeGuard
|
||||
skeleton={<DynamicLoader />}
|
||||
skeleton={<AppLoader />}
|
||||
failure={<AppFailure title={t(`Node: ${VEGA_URL} is unsuitable`)} />}
|
||||
>
|
||||
<Web3Provider>
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
export * from './app-loader';
|
||||
export * from './bootstrapper';
|
||||
export * from './web3-provider';
|
||||
@@ -17,11 +17,11 @@ export const Header = ({ title, children }: TradeMarketHeaderProps) => {
|
||||
);
|
||||
return (
|
||||
<header className={headerClasses}>
|
||||
<div className="hidden lg:flex flex-col justify-center items-start pl-3 lg:pl-4 pt-2 xl:pb-2 pb-0">
|
||||
<div className="flex-col items-start justify-center hidden pt-2 pb-0 pl-3 lg:flex lg:pl-4 xl:pb-2">
|
||||
{title}
|
||||
</div>
|
||||
<div data-testid="header-summary" className="min-w-0">
|
||||
<div className="px-3 lg:px-4 py-2 flex flex-nowrap gap-4 items-center text-xs overflow-x-auto">
|
||||
<div className="flex items-center px-3 py-2 overflow-x-auto text-xs lg:px-4 flex-nowrap gap-6">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,7 +70,7 @@ export const HeaderTitle = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
<h1
|
||||
data-testid="header-title"
|
||||
className="flex gap-4 items-center text-lg whitespace-nowrap xl:pr-4 xl:border-r border-default"
|
||||
className="flex items-center text-lg gap-4 whitespace-nowrap xl:pr-4 xl:border-r border-default"
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
|
||||
@@ -40,9 +40,12 @@ export const LayoutWithSidebar = () => {
|
||||
</main>
|
||||
<aside
|
||||
// min-h-0 is needed as this element is part of a grid, we want the content to be scrollable, without it it will push the grid element taller
|
||||
className={classNames('col-start-1 lg:col-start-2 min-h-0', {
|
||||
hidden: !sidebarOpen,
|
||||
})}
|
||||
className={classNames(
|
||||
'col-start-1 lg:col-start-2 min-h-0 lg:border-l border-default lg:bg-vega-clight-800 dark:lg:bg-vega-cdark-800',
|
||||
{
|
||||
hidden: !sidebarOpen,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<SidebarContent />
|
||||
</aside>
|
||||
|
||||
@@ -28,12 +28,11 @@ export const MarketHeader = () => {
|
||||
<HeaderTitle>
|
||||
<span>
|
||||
{data.tradableInstrument.instrument.code}
|
||||
<span className="ml-2 text-xs uppercase text-muted">
|
||||
{' '}
|
||||
<span className="ml-1 text-xs uppercase text-muted">
|
||||
{data.tradableInstrument.instrument.product.__typename &&
|
||||
ProductTypeShortName[
|
||||
data.tradableInstrument.instrument.product.__typename
|
||||
]}{' '}
|
||||
]}
|
||||
</span>
|
||||
</span>
|
||||
<VegaIcon name={VegaIconNames.CHEVRON_DOWN} size={14} />
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Loader } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const Preloader = () => {
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
body{
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
html.dark body{
|
||||
background: #000;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div className="pre-loader">
|
||||
<Loader />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Preloader;
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
useSearchParams,
|
||||
} from 'react-router-dom';
|
||||
import { Connectors } from '../lib/vega-connectors';
|
||||
import { AppLoader, DynamicLoader } from '../components/app-loader';
|
||||
import { Bootstrapper } from '../components/bootstrapper';
|
||||
import { useDataProvider } from '@vegaprotocol/data-provider';
|
||||
import { activeOrdersProvider } from '@vegaprotocol/orders';
|
||||
import { useTelemetryApproval } from '../lib/hooks/use-telemetry-approval';
|
||||
@@ -52,6 +52,7 @@ import { ViewingBanner } from '../components/viewing-banner';
|
||||
import { NavHeader } from '../components/navbar/nav-header';
|
||||
import { Telemetry } from '../components/telemetry';
|
||||
import { Routes as AppRoutes } from '../lib/links';
|
||||
import { SSRLoader } from './ssr-loader';
|
||||
|
||||
const DEFAULT_TITLE = t('Welcome to Vega trading!');
|
||||
|
||||
@@ -144,14 +145,14 @@ function VegaTradingApp(props: AppProps) {
|
||||
// Prevent HashRouter from being server side rendered as it
|
||||
// relies on presence of document object
|
||||
if (status === 'default') {
|
||||
return <DynamicLoader />;
|
||||
return <SSRLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<HashRouter>
|
||||
<AppLoader>
|
||||
<Bootstrapper>
|
||||
<AppBody {...props} />
|
||||
</AppLoader>
|
||||
</Bootstrapper>
|
||||
<NodeSwitcherDialog open={nodeSwitcherOpen} setOpen={setNodeSwitcher} />
|
||||
</HashRouter>
|
||||
);
|
||||
|
||||
@@ -22,14 +22,17 @@ export default function Document() {
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" content="/favicon.ico" />
|
||||
|
||||
{/* eslint-disable-next-line @next/next/no-css-tags */}
|
||||
<link rel="stylesheet" href="/preloader.css" media="all" />
|
||||
|
||||
{/* scripts */}
|
||||
<script src="/theme-setter.js" type="text/javascript" async />
|
||||
</Head>
|
||||
<Html>
|
||||
<body className="bg-white dark:bg-vega-cdark-900 text-default font-alpha">
|
||||
<body
|
||||
// Nextjs will set body to display none until js runs. Because the entire app is client rendered
|
||||
// and delivered via ipfs we override this to show a server side render loading animation until the
|
||||
// js is downloaded and react takes over rendering
|
||||
style={{ display: 'block' }}
|
||||
className="bg-white dark:bg-vega-cdark-900 text-default font-alpha"
|
||||
>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
export const SSRLoader = () => {
|
||||
const randomDelay = () => {
|
||||
return parseFloat((Math.random() * (4 - 1) + 1).toFixed(2));
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<style jsx>{`
|
||||
@keyframes flickering {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
26% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<div style={{ width: 50, height: 50, display: 'flex', flexWrap: 'wrap' }}>
|
||||
{new Array(25).fill(null).map((_, i) => {
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
animation: 'flickering 0.4s linear alternate infinite',
|
||||
animationDelay: `-${randomDelay()}s`,
|
||||
animationDirection: i % 2 === 0 ? 'reverse' : 'alternate',
|
||||
background: 'black',
|
||||
opacity: Math.random() > 0.5 ? 1 : 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
.pre-loader {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.pre-loader .loader-item {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #000;
|
||||
}
|
||||
.pre-loader .pre-loader-center {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.pre-loader .pre-loader-wrapper {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
.pre-loader .loader-item {
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(0) {
|
||||
animation-delay: 0ms;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:first-child {
|
||||
animation-delay: -50ms;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(3) {
|
||||
animation-delay: -0.6s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(4) {
|
||||
animation-delay: 0.4s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(5) {
|
||||
animation-delay: -0.5s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(6) {
|
||||
animation-delay: 0.3s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(7) {
|
||||
animation-delay: -1.4s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(8) {
|
||||
animation-delay: 2s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(9) {
|
||||
animation-delay: -0.9s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(10) {
|
||||
animation-delay: 1.5s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(11) {
|
||||
animation-delay: -0.55s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(12) {
|
||||
animation-delay: 1.2s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(13) {
|
||||
animation-delay: -0.65s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(14) {
|
||||
animation-delay: 0.7s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(15) {
|
||||
animation-delay: -3.75s;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
.pre-loader .loader-item:nth-child(16) {
|
||||
animation-delay: 1.6s;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
.pre-loader .loader-item {
|
||||
animation: flickering 0.4s linear infinite alternate;
|
||||
}
|
||||
@keyframes flickering {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
25% {
|
||||
opacity: 1;
|
||||
}
|
||||
26% {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
html.dark .pre-loader .loader-item {
|
||||
background: #fff;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { t } from '@vegaprotocol/i18n';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { Button, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
import { useNodeSwitcherStore } from '../../hooks/use-node-switcher-store';
|
||||
|
||||
export const AppFailure = ({
|
||||
@@ -10,17 +10,15 @@ export const AppFailure = ({
|
||||
error?: string | null;
|
||||
}) => {
|
||||
const setNodeSwitcher = useNodeSwitcherStore((store) => store.setDialogOpen);
|
||||
const nonIdealWrapperClasses =
|
||||
'h-full min-h-screen flex items-center justify-center';
|
||||
return (
|
||||
<div className={nonIdealWrapperClasses}>
|
||||
<Splash>
|
||||
<div className="text-center">
|
||||
<p className="text-xl mb-4">{title}</p>
|
||||
{error && <p className="text-sm mb-8">{error}</p>}
|
||||
<p className="mb-4 text-xl">{title}</p>
|
||||
{error && <p className="mb-8 text-sm">{error}</p>}
|
||||
<Button onClick={() => setNodeSwitcher(true)}>
|
||||
{t('Change node')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Splash>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Loader, Splash } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
export const AppLoader = () => {
|
||||
return (
|
||||
<Splash>
|
||||
<Loader />
|
||||
</Splash>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { AppLoader } from './app-loader';
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './app-failure';
|
||||
export * from './app-loader';
|
||||
export * from './network-loader';
|
||||
export * from './network-switcher';
|
||||
export * from './node-guard';
|
||||
|
||||
@@ -30,11 +30,8 @@ export const Loader = ({ size = 'large', forceTheme }: LoaderProps) => {
|
||||
const generate = useMemo(() => pseudoRandom(1), []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-center pre-loader-center"
|
||||
data-testid="loader"
|
||||
>
|
||||
<div className={`${wrapperClasses} flex flex-wrap pre-loader-wrapper`}>
|
||||
<div className="flex flex-col items-center" data-testid="loader">
|
||||
<div className={`${wrapperClasses} flex flex-wrap`}>
|
||||
{new Array(items).fill(null).map((_, i) => {
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -1,77 +1,68 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { Sparkline } from './sparkline';
|
||||
import type { SparklineProps } from './sparkline';
|
||||
|
||||
const props = {
|
||||
data: [
|
||||
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,
|
||||
};
|
||||
describe('Sparkline', () => {
|
||||
let props: SparklineProps = {
|
||||
data: [],
|
||||
};
|
||||
|
||||
it('Renders an svg with a single path', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
expect(screen.getByTestId('sparkline-svg')).toBeInTheDocument();
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toBeInTheDocument();
|
||||
expect(path).toHaveAttribute('d', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke-width', '1');
|
||||
expect(path).toHaveAttribute('fill', 'transparent');
|
||||
});
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8,
|
||||
9, 10, 11, 12,
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it('Requires a data prop but width and height are optional', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
const svg = screen.getByTestId('sparkline-svg');
|
||||
expect(svg).toHaveAttribute('width', '60');
|
||||
expect(svg).toHaveAttribute('height', '15');
|
||||
});
|
||||
it('Renders an svg with a single path', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
expect(screen.getByTestId('sparkline-svg')).toBeInTheDocument();
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toBeInTheDocument();
|
||||
expect(path).toHaveAttribute('d', expect.any(String));
|
||||
expect(path).toHaveAttribute('stroke-width', '1');
|
||||
});
|
||||
|
||||
it('Renders a red line if the last value is less than the first', () => {
|
||||
props.data[0] = 10;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-market-red dark:stroke-market-red'
|
||||
);
|
||||
});
|
||||
it('Requires a data prop but width and height are optional', () => {
|
||||
render(<Sparkline {...props} />);
|
||||
const svg = screen.getByTestId('sparkline-svg');
|
||||
expect(svg).toHaveAttribute('width', '60');
|
||||
expect(svg).toHaveAttribute('height', '15');
|
||||
});
|
||||
|
||||
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(
|
||||
'[vector-effect:non-scaling-stroke] stroke-market-green-600 dark:stroke-market-green'
|
||||
);
|
||||
});
|
||||
it('Renders a red line if the last value is less than the first', () => {
|
||||
props.data[0] = 10;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-market-red dark:stroke-market-red'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders a white line if the first and last values are equal', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
|
||||
);
|
||||
});
|
||||
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;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-market-green-600 dark:stroke-market-green'
|
||||
);
|
||||
});
|
||||
|
||||
it('Renders a gray line if there are not 24 values', () => {
|
||||
props.data = [
|
||||
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
|
||||
22, 23,
|
||||
];
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.queryAllByTestId('sparkline-path');
|
||||
expect(paths).toHaveLength(2);
|
||||
expect(paths[0]).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
|
||||
);
|
||||
it('Renders a white line if the first and last values are equal', () => {
|
||||
props.data[0] = 5;
|
||||
props.data[props.data.length - 1] = 5;
|
||||
render(<Sparkline {...props} />);
|
||||
const paths = screen.getAllByTestId('sparkline-path');
|
||||
const path = paths[0];
|
||||
expect(path).toHaveClass(
|
||||
'[vector-effect:non-scaling-stroke] stroke-black/40 dark:stroke-white/40'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,39 +8,49 @@ export default {
|
||||
|
||||
const Template: Story = (args) => <Sparkline data={args['data']} {...args} />;
|
||||
|
||||
export const Grey = Template.bind({});
|
||||
Grey.args = {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8,
|
||||
],
|
||||
width: 60,
|
||||
height: 30,
|
||||
points: 25,
|
||||
className: 'w-[113px]',
|
||||
};
|
||||
|
||||
export const Equal = Template.bind({});
|
||||
Equal.args = {
|
||||
data: [
|
||||
12, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 12,
|
||||
],
|
||||
width: 60,
|
||||
width: 110,
|
||||
height: 30,
|
||||
points: 25,
|
||||
className: 'w-[113px]',
|
||||
};
|
||||
|
||||
export const Increase = Template.bind({});
|
||||
Increase.args = {
|
||||
data: [
|
||||
1, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 12,
|
||||
22,
|
||||
22,
|
||||
22, // extra values should be ignored, this should still render an increase
|
||||
0,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
],
|
||||
width: 60,
|
||||
width: 110,
|
||||
height: 30,
|
||||
points: 25,
|
||||
className: 'w-[113px]',
|
||||
};
|
||||
|
||||
export const Decrease = Template.bind({});
|
||||
@@ -49,8 +59,27 @@ Decrease.args = {
|
||||
12, 2, 3, 4, 5, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 6, 7, 8, 9,
|
||||
10, 11, 1,
|
||||
],
|
||||
width: 60,
|
||||
width: 110,
|
||||
height: 30,
|
||||
};
|
||||
|
||||
export const LessThan24HoursIncrease = Template.bind({});
|
||||
LessThan24HoursIncrease.args = {
|
||||
data: [20, 21, 22, 25, 24, 24, 22, 19, 20, 22, 23, 27],
|
||||
width: 110,
|
||||
height: 30,
|
||||
};
|
||||
|
||||
export const LessThan24HoursDecrease = Template.bind({});
|
||||
LessThan24HoursDecrease.args = {
|
||||
data: [20, 21, 22, 23, 24, 6, 7, 9, 11, 13, 11, 9],
|
||||
width: 110,
|
||||
height: 30,
|
||||
};
|
||||
|
||||
export const NoData = Template.bind({});
|
||||
NoData.args = {
|
||||
data: [],
|
||||
width: 110,
|
||||
height: 30,
|
||||
points: 25,
|
||||
className: 'w-[113px]',
|
||||
};
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { extent } from 'd3-array';
|
||||
import { scaleLinear } from 'd3-scale';
|
||||
import { line } from 'd3-shape';
|
||||
import { area, line } from 'd3-shape';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import React from 'react';
|
||||
|
||||
function colorByChange(a: number, b: number) {
|
||||
return a === b
|
||||
? 'stroke-black/40 dark:stroke-white/40'
|
||||
: a < b
|
||||
? 'stroke-market-green-600 dark:stroke-market-green'
|
||||
: 'stroke-market-red dark:stroke-market-red';
|
||||
if (a < b) {
|
||||
return 'stroke-market-green-600 dark:stroke-market-green';
|
||||
} else if (a > b) {
|
||||
return 'stroke-market-red dark:stroke-market-red';
|
||||
}
|
||||
return 'stroke-black/40 dark:stroke-white/40';
|
||||
}
|
||||
|
||||
function shadedColor(a: number, b: number) {
|
||||
if (a < b) {
|
||||
return 'fill-market-green-600 dark:fill-market-green';
|
||||
} else if (a > b) {
|
||||
return 'fill-market-red-600 dark:fill-market-red';
|
||||
}
|
||||
return 'fill-black dark:fill-white';
|
||||
}
|
||||
|
||||
export interface SparklineProps {
|
||||
@@ -18,75 +28,60 @@ export interface SparklineProps {
|
||||
height?: number;
|
||||
points?: number;
|
||||
className?: string;
|
||||
muted?: boolean;
|
||||
}
|
||||
|
||||
export const SparklineView = ({
|
||||
data,
|
||||
width = 60,
|
||||
height = 15,
|
||||
points = 25,
|
||||
muted = false,
|
||||
points = 24,
|
||||
className,
|
||||
}: SparklineProps) => {
|
||||
// How many points are missing. If market is 12 hours old the 25 - 12
|
||||
const preMarketLength = points - data.length;
|
||||
|
||||
// Create two dimensional array for sparkline points [x, y]
|
||||
const marketData: [number, number][] = data.map((d, i) => [
|
||||
preMarketLength + i,
|
||||
d,
|
||||
]);
|
||||
// Empty two dimensional array for gray, 'no data' line
|
||||
let preMarketData: [number, number][] = [];
|
||||
|
||||
// Get the extent for our y value
|
||||
const [min, max] = extent(marketData, (d) => d[1]);
|
||||
const [min, max] = extent(data, (d) => d);
|
||||
|
||||
if (typeof min !== 'number' || typeof max !== 'number') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create a second set of data to render a gray line for any
|
||||
// missing points if the market is less than 24 hours old
|
||||
if (marketData.length < points) {
|
||||
// Populate preMarketData with the average of our extents
|
||||
// so that the line renders centered vertically
|
||||
const fillValue = (min + max) / 2;
|
||||
preMarketData = new Array(points - marketData.length)
|
||||
.fill(fillValue)
|
||||
.map((d: number, i) => [i, d] as [number, number]);
|
||||
const midValue = (min + max) / 2;
|
||||
|
||||
// Add the first point of or market data so that the two
|
||||
// lines join up
|
||||
preMarketData.push(marketData[0] as [number, number]);
|
||||
}
|
||||
// Market may be less than 24hr old so padd the data array
|
||||
// with values that is the mid value (avg of min and max).
|
||||
// This will rendera horizontal line until the real data shifts the line
|
||||
const padCount = data.length < points ? points - data.length : 0;
|
||||
const padArr = new Array(padCount).fill(midValue);
|
||||
const trimmedData = data.slice(-points);
|
||||
|
||||
const xScale = scaleLinear().domain([0, points]).range([0, 100]);
|
||||
const yScale = scaleLinear().domain([min, max]).range([100, 0]);
|
||||
// Get the last 24 values if data has more than needed
|
||||
const lineData: [number, number][] = [...padArr, ...trimmedData].map(
|
||||
(d, i) => {
|
||||
return [i, d];
|
||||
}
|
||||
);
|
||||
|
||||
const xScale = scaleLinear().domain([0, points]).range([0, width]);
|
||||
const yScale = scaleLinear().domain([min, max]).range([height, 0]);
|
||||
|
||||
const lineSeries = line()
|
||||
.x((d) => xScale(d[0]))
|
||||
.y((d) => yScale(d[1]));
|
||||
|
||||
// Get the color of the marketData line
|
||||
const [firstVal, lastVal] = [data[0], data[data.length - 1]];
|
||||
const strokeClassName = muted
|
||||
? data.length >= 24
|
||||
? colorByChange(firstVal, lastVal)
|
||||
: 'stroke-black/40 dark:stroke-white/40'
|
||||
: colorByChange(firstVal, lastVal);
|
||||
const areaSeries = area()
|
||||
.x((d) => xScale(d[0]))
|
||||
.y0(height)
|
||||
.y1((d) => yScale(d[1]));
|
||||
|
||||
const firstVal = trimmedData[0];
|
||||
const lastVal = trimmedData[trimmedData.length - 1];
|
||||
|
||||
// Get the color of the marketData line depending on market movement
|
||||
const strokeClassName = colorByChange(firstVal, lastVal);
|
||||
const areaClassName = shadedColor(firstVal, lastVal);
|
||||
|
||||
// Create paths
|
||||
const preMarketCreationPath = lineSeries(preMarketData);
|
||||
const mainPath = lineSeries(marketData);
|
||||
const pathProps = {
|
||||
'data-testid': 'sparkline-path',
|
||||
className: `[vector-effect:non-scaling-stroke] ${strokeClassName}`,
|
||||
stroke: 'strokeCurrent',
|
||||
strokeWidth: 1,
|
||||
fill: 'transparent',
|
||||
};
|
||||
const linePath = lineSeries(lineData);
|
||||
const areaPath = areaSeries(lineData);
|
||||
|
||||
return (
|
||||
<svg
|
||||
@@ -94,13 +89,25 @@ export const SparklineView = ({
|
||||
className={className}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 100 100"
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
{preMarketCreationPath && (
|
||||
<path {...pathProps} d={preMarketCreationPath} />
|
||||
{linePath && (
|
||||
<path
|
||||
d={linePath}
|
||||
data-testid="sparkline-path"
|
||||
className={`[vector-effect:non-scaling-stroke] fill-transparent ${strokeClassName}`}
|
||||
strokeWidth={1}
|
||||
/>
|
||||
)}
|
||||
{areaPath && (
|
||||
<path
|
||||
className={areaClassName}
|
||||
fillOpacity={0.2}
|
||||
stroke="none"
|
||||
d={areaPath}
|
||||
/>
|
||||
)}
|
||||
{mainPath && <path {...pathProps} d={mainPath} />}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user