2022-03-02 01:01:10 +00:00
|
|
|
import { gql } from '@apollo/client';
|
2022-03-01 00:59:18 +00:00
|
|
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
|
|
|
import classNames from 'classnames';
|
|
|
|
import { useRouter } from 'next/router';
|
2022-03-02 00:05:31 +00:00
|
|
|
import React, {
|
|
|
|
Children,
|
|
|
|
isValidElement,
|
|
|
|
ReactNode,
|
2022-03-02 01:40:31 +00:00
|
|
|
useCallback,
|
2022-03-02 00:05:31 +00:00
|
|
|
useEffect,
|
|
|
|
useState,
|
|
|
|
} from 'react';
|
|
|
|
import debounce from 'lodash/debounce';
|
2022-03-01 23:51:46 +00:00
|
|
|
import { Market, MarketVariables, Market_market } from './__generated__/Market';
|
2022-03-02 01:01:10 +00:00
|
|
|
import { PageQueryContainer } from '../../components/page-query-container';
|
2022-03-01 00:59:18 +00:00
|
|
|
|
|
|
|
// Top level page query
|
|
|
|
const MARKET_QUERY = gql`
|
|
|
|
query Market($marketId: ID!) {
|
|
|
|
market(id: $marketId) {
|
|
|
|
id
|
|
|
|
name
|
|
|
|
trades {
|
|
|
|
id
|
|
|
|
price
|
|
|
|
size
|
|
|
|
createdAt
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`;
|
|
|
|
|
|
|
|
const MarketPage = () => {
|
|
|
|
const { query } = useRouter();
|
2022-03-02 00:05:31 +00:00
|
|
|
const { w } = useWindowSize();
|
2022-03-01 23:51:46 +00:00
|
|
|
|
2022-03-01 00:59:18 +00:00
|
|
|
return (
|
2022-03-02 01:01:10 +00:00
|
|
|
<PageQueryContainer<Market, MarketVariables>
|
|
|
|
query={MARKET_QUERY}
|
|
|
|
options={{
|
|
|
|
variables: { marketId: query.marketId as string },
|
|
|
|
skip: !query.marketId,
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
{(data) =>
|
|
|
|
w > 1050 ? (
|
|
|
|
<TradeGrid market={data.market} />
|
|
|
|
) : (
|
|
|
|
<TradePanels market={data.market} />
|
|
|
|
)
|
|
|
|
}
|
|
|
|
{}
|
|
|
|
</PageQueryContainer>
|
2022-03-01 00:59:18 +00:00
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default MarketPage;
|
|
|
|
|
2022-03-01 23:51:46 +00:00
|
|
|
interface TradeGridProps {
|
|
|
|
market: Market_market;
|
|
|
|
}
|
|
|
|
|
|
|
|
const TradeGrid = ({ market }: TradeGridProps) => {
|
2022-03-02 00:48:03 +00:00
|
|
|
const wrapperClasses = classNames(
|
|
|
|
'h-full max-h-full',
|
|
|
|
'grid gap-[1px] grid-cols-[1fr_325px_325px] grid-rows-[min-content_1fr_200px]',
|
|
|
|
'bg-neutral-200',
|
|
|
|
'text-ui'
|
|
|
|
);
|
2022-03-01 23:51:46 +00:00
|
|
|
return (
|
2022-03-02 00:48:03 +00:00
|
|
|
<div className={wrapperClasses}>
|
2022-03-01 23:51:46 +00:00
|
|
|
<header className="col-start-1 col-end-2 row-start-1 row-end-1 bg-white p-8">
|
|
|
|
<h1>Market: {market.name}</h1>
|
|
|
|
</header>
|
|
|
|
<TradeGridChild className="col-start-1 col-end-2">
|
|
|
|
<Chart />
|
|
|
|
</TradeGridChild>
|
|
|
|
<TradeGridChild className="row-start-1 row-end-3">
|
|
|
|
<Ticket />
|
|
|
|
</TradeGridChild>
|
|
|
|
<TradeGridChild className="row-start-1 row-end-3">
|
|
|
|
<GridTabs group="trade">
|
|
|
|
<GridTab name="trades">
|
|
|
|
<pre>{JSON.stringify(market.trades, null, 2)}</pre>
|
|
|
|
</GridTab>
|
|
|
|
<GridTab name="orderbook">
|
|
|
|
<Orderbook />
|
|
|
|
</GridTab>
|
|
|
|
</GridTabs>
|
|
|
|
</TradeGridChild>
|
|
|
|
<TradeGridChild className="col-span-3">
|
|
|
|
<GridTabs group="portfolio">
|
|
|
|
<GridTab name="orders">
|
|
|
|
<Orders />
|
|
|
|
</GridTab>
|
|
|
|
<GridTab name="positions">
|
|
|
|
<Positions />
|
|
|
|
</GridTab>
|
|
|
|
<GridTab name="collateral">
|
|
|
|
<Collateral />
|
|
|
|
</GridTab>
|
|
|
|
</GridTabs>
|
|
|
|
</TradeGridChild>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
interface TradeGridChildProps {
|
2022-03-01 00:59:18 +00:00
|
|
|
children: ReactNode;
|
|
|
|
className?: string;
|
|
|
|
}
|
|
|
|
|
2022-03-01 23:51:46 +00:00
|
|
|
const TradeGridChild = ({ children, className }: TradeGridChildProps) => {
|
2022-03-01 00:59:18 +00:00
|
|
|
const gridChildClasses = classNames('bg-white', className);
|
|
|
|
return (
|
|
|
|
<section className={gridChildClasses}>
|
2022-03-01 21:35:31 +00:00
|
|
|
<AutoSizer>
|
|
|
|
{({ width, height }) => (
|
|
|
|
<div style={{ width, height }} className="overflow-auto">
|
|
|
|
{children}
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</AutoSizer>
|
2022-03-01 00:59:18 +00:00
|
|
|
</section>
|
|
|
|
);
|
|
|
|
};
|
2022-03-01 21:35:31 +00:00
|
|
|
|
|
|
|
interface GridTabsProps {
|
|
|
|
children: ReactNode;
|
2022-03-01 22:14:33 +00:00
|
|
|
group: string;
|
2022-03-01 21:35:31 +00:00
|
|
|
}
|
|
|
|
|
2022-03-01 22:14:33 +00:00
|
|
|
const GridTabs = ({ children, group }: GridTabsProps) => {
|
|
|
|
const { query, asPath, replace } = useRouter();
|
2022-03-02 01:40:31 +00:00
|
|
|
const [activeTab, setActiveTab] = useState<string>(() => {
|
|
|
|
if (query[group]) {
|
|
|
|
return query[group];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Default to first tab
|
|
|
|
return children[0].props.name;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Using replace inside an effect causes a render loop. Seems like its not using useCallback
|
|
|
|
// eslint-disable-next-line
|
|
|
|
const safeReplace = useCallback((path: string) => replace(path), []);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const [url, queryString] = asPath.split('?');
|
|
|
|
const searchParams = new URLSearchParams(queryString);
|
|
|
|
searchParams.set(group, activeTab as string);
|
|
|
|
safeReplace(`${url}?${searchParams.toString()}`);
|
|
|
|
}, [activeTab, group, asPath, safeReplace]);
|
2022-03-01 21:35:31 +00:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="h-full grid grid-rows-[min-content_1fr]">
|
|
|
|
{/* the tabs */}
|
2022-03-02 00:26:51 +00:00
|
|
|
<div className="flex gap-[2px] bg-neutral-200" role="tablist">
|
2022-03-01 21:35:31 +00:00
|
|
|
{Children.map(children, (child) => {
|
|
|
|
if (!isValidElement(child)) return null;
|
2022-03-02 01:40:31 +00:00
|
|
|
const isActive = activeTab === child.props.name;
|
2022-03-02 00:26:51 +00:00
|
|
|
const buttonClass = classNames(
|
|
|
|
'py-4',
|
|
|
|
'px-12',
|
|
|
|
'border-t border-neutral-200',
|
|
|
|
'capitalize',
|
|
|
|
{
|
|
|
|
'text-vega-pink': isActive,
|
|
|
|
'bg-white': isActive,
|
|
|
|
}
|
|
|
|
);
|
2022-03-01 21:35:31 +00:00
|
|
|
return (
|
|
|
|
<button
|
|
|
|
className={buttonClass}
|
2022-03-02 01:40:31 +00:00
|
|
|
onClick={() => setActiveTab(child.props.name)}
|
2022-03-01 22:33:28 +00:00
|
|
|
role="tab"
|
|
|
|
aria-selected={isActive}
|
|
|
|
aria-controls={`tabpanel-${group}-${child.props.name}`}
|
|
|
|
id={`tab-${group}-${child.props.name}`}
|
2022-03-01 21:35:31 +00:00
|
|
|
>
|
|
|
|
{child.props.name}
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
{/* the content */}
|
2022-03-01 22:18:49 +00:00
|
|
|
<div className="h-full overflow-auto">
|
2022-03-01 21:35:31 +00:00
|
|
|
{Children.map(children, (child) => {
|
2022-03-02 01:40:31 +00:00
|
|
|
if (isValidElement(child) && activeTab === child.props.name) {
|
2022-03-01 22:33:28 +00:00
|
|
|
return (
|
|
|
|
<div
|
|
|
|
role="tabpanel"
|
|
|
|
id={`tabpanel-${group}-${child.props.name}`}
|
|
|
|
aria-labelledby={`tab-${group}-${child.props.name}`}
|
|
|
|
>
|
|
|
|
{child.props.children}
|
|
|
|
</div>
|
|
|
|
);
|
2022-03-01 21:35:31 +00:00
|
|
|
}
|
|
|
|
return null;
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
interface GridTabProps {
|
|
|
|
children: ReactNode;
|
|
|
|
name: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
const GridTab = ({ children }: GridTabProps) => {
|
|
|
|
return <div>{children}</div>;
|
|
|
|
};
|
2022-03-01 23:51:46 +00:00
|
|
|
|
|
|
|
///// SMALL SCREENS ///////
|
|
|
|
|
|
|
|
type View = keyof typeof Views;
|
|
|
|
|
|
|
|
interface TradePanelsProps {
|
|
|
|
market: Market_market;
|
|
|
|
}
|
|
|
|
|
|
|
|
const TradePanels = ({ market }: TradePanelsProps) => {
|
|
|
|
const [view, setView] = React.useState<View>('chart');
|
|
|
|
|
|
|
|
const renderView = () => {
|
|
|
|
const Component = Views[view];
|
|
|
|
|
|
|
|
if (!Component) {
|
|
|
|
throw new Error(`No component for view: ${view}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return <Component />;
|
|
|
|
};
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="h-full grid grid-rows-[min-content_1fr_min-content]">
|
|
|
|
<header className="bg-white p-8">
|
|
|
|
<h1>Market: {market.name}</h1>
|
|
|
|
</header>
|
|
|
|
<div className="h-full">
|
|
|
|
<AutoSizer>
|
|
|
|
{({ width, height }) => (
|
|
|
|
<div style={{ width, height }}>{renderView()}</div>
|
|
|
|
)}
|
|
|
|
</AutoSizer>
|
|
|
|
</div>
|
2022-03-02 00:26:51 +00:00
|
|
|
<div className="flex flex-nowrap gap-2 bg-neutral-200 border-neutral-200 border-t overflow-x-auto">
|
|
|
|
{Object.keys(Views).map((key: View) => {
|
|
|
|
const className = classNames(
|
|
|
|
'p-8',
|
|
|
|
'border-t',
|
|
|
|
'border-neutral-200',
|
|
|
|
'capitalize',
|
|
|
|
{
|
|
|
|
'text-vega-pink': view === key,
|
|
|
|
'bg-white': view === key,
|
|
|
|
}
|
|
|
|
);
|
|
|
|
return (
|
|
|
|
<button
|
|
|
|
onClick={() => setView(key)}
|
|
|
|
className={className}
|
|
|
|
key={key}
|
|
|
|
>
|
|
|
|
{key}
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
})}
|
2022-03-01 23:51:46 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const Chart = () => <div>TODO: Chart</div>;
|
|
|
|
const Ticket = () => <div>TODO: Ticket</div>;
|
|
|
|
const Orderbook = () => <div>TODO: Orderbook</div>;
|
|
|
|
const Orders = () => <div>TODO: Orders</div>;
|
|
|
|
const Positions = () => <div>TODO: Positions</div>;
|
|
|
|
const Collateral = () => <div>TODO: Collateral</div>;
|
|
|
|
|
|
|
|
const Views = {
|
|
|
|
chart: Chart,
|
|
|
|
ticket: Ticket,
|
|
|
|
orderbook: Orderbook,
|
|
|
|
orders: Orders,
|
|
|
|
positions: Positions,
|
|
|
|
collateral: Collateral,
|
|
|
|
};
|
2022-03-02 00:05:31 +00:00
|
|
|
|
|
|
|
const useWindowSize = () => {
|
|
|
|
const [windowSize, setWindowSize] = useState(() => {
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
return {
|
|
|
|
w: window.innerWidth,
|
|
|
|
h: window.innerHeight,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Something sensible for server rendered page
|
|
|
|
return {
|
|
|
|
w: 1200,
|
|
|
|
h: 900,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const handleResize = debounce((event) => {
|
|
|
|
setWindowSize({
|
|
|
|
w: event.target.innerWidth,
|
|
|
|
h: event.target.innerHeight,
|
|
|
|
});
|
|
|
|
}, 300);
|
|
|
|
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
window.removeEventListener('resize', handleResize);
|
|
|
|
};
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
return windowSize;
|
|
|
|
};
|