vega-frontend-monorepo/apps/trading/components/sidebar/sidebar.tsx
2023-09-20 13:28:34 -07:00

324 lines
8.4 KiB
TypeScript

import classNames from 'classnames';
import type { ReactNode } from 'react';
import { useEffect } from 'react';
import { Route, Routes, useParams } from 'react-router-dom';
import { create } from 'zustand';
import { TransferContainer } from '@vegaprotocol/accounts';
import { DealTicketContainer } from '@vegaprotocol/deal-ticket';
import { DepositContainer } from '@vegaprotocol/deposits';
import { t } from '@vegaprotocol/i18n';
import { MarketInfoAccordionContainer } from '@vegaprotocol/markets';
import { TinyScroll, VegaIcon, VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { NodeHealthContainer } from '../node-health';
import { Settings } from '../settings';
import { Tooltip } from '../../components/tooltip';
import { WithdrawContainer } from '../withdraw-container';
import { GetStarted } from '../welcome-dialog';
import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
export enum ViewType {
Order = 'Order',
Info = 'Info',
Deposit = 'Deposit',
Withdraw = 'Withdraw',
Transfer = 'Transfer',
Settings = 'Settings',
ViewAs = 'ViewAs',
}
type SidebarView =
| {
type: ViewType.Deposit;
assetId?: string;
}
| {
type: ViewType.Withdraw;
assetId?: string;
}
| {
type: ViewType.Transfer;
assetId?: string;
}
| {
type: ViewType.Order;
}
| {
type: ViewType.Info;
}
| {
type: ViewType.Settings;
};
const MarketSidebarButtons = () => {
const currentRouteId = useGetCurrentRouteId();
return (
<>
<SidebarDivider />
<SidebarButton
view={ViewType.Order}
icon={VegaIconNames.TICKET}
tooltip={t('Order')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
</>
);
};
export const Sidebar = () => {
const currentRouteId = useGetCurrentRouteId();
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet();
return (
<div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar">
<nav className={navClasses}>
{/* sidebar options that always show */}
<SidebarButton
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
{/* buttons for specific routes */}
<Routes>
<Route
path="markets/all"
// render nothing for markets/all, otherwise markets/:marketId will match with markets/all
element={null}
/>
<Route
// render nothing for portfolio
path="portfolio"
element={null}
/>
<Route path="markets/:marketId" element={<MarketSidebarButtons />} />
<Route
path="liquidity/:marketId"
element={<MarketSidebarButtons />}
/>
</Routes>
</nav>
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}>
<SidebarButton
view={ViewType.ViewAs}
onClick={() => {
setViewAsDialogOpen(true);
}}
icon={VegaIconNames.EYE}
tooltip={t('View as party')}
disabled={Boolean(pubKeys)}
routeId={currentRouteId}
/>
<SidebarButton
view={ViewType.Settings}
icon={VegaIconNames.COG}
tooltip={t('Settings')}
routeId={currentRouteId}
/>
<NodeHealthContainer />
</nav>
</div>
);
};
export const SidebarButton = ({
view,
icon,
tooltip,
disabled = false,
onClick,
routeId,
}: {
view?: ViewType;
icon: VegaIconNames;
tooltip: string;
disabled?: boolean;
onClick?: () => void;
routeId: string;
}) => {
const { setViews, getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(routeId);
const onSelect = (view: SidebarView['type']) => {
if (view === currView?.type) {
setViews(null, routeId);
} else {
setViews({ type: view }, routeId);
}
};
const buttonClasses = classNames(
'flex items-center p-1 rounded',
'disabled:cursor-not-allowed disabled:text-vega-clight-500 dark:disabled:text-vega-cdark-500',
{
'text-vega-clight-200 dark:text-vega-cdark-200 enabled:hover:bg-vega-clight-500 dark:enabled:hover:bg-vega-cdark-500':
!view || view !== currView?.type,
'bg-vega-yellow enabled:hover:bg-vega-yellow-550 text-black':
view && view === currView?.type,
}
);
return (
<Tooltip
description={tooltip}
align="center"
side="right"
sideOffset={10}
delayDuration={0}
>
<button
className={buttonClasses}
data-testid={view}
onClick={onClick || (() => onSelect(view as SidebarView['type']))}
disabled={disabled}
>
<VegaIcon name={icon} size={20} />
</button>
</Tooltip>
);
};
const SidebarDivider = () => {
return (
<div
className="w-px h-4 bg-vega-clight-600 dark:bg-vega-cdark-600 lg:w-4 lg:h-px"
role="separator"
/>
);
};
export const SidebarContent = () => {
const params = useParams();
const currentRouteId = useGetCurrentRouteId();
const { setViews, getView } = useSidebar();
const view = getView(currentRouteId);
if (!view) return null;
if (view.type === ViewType.Order) {
if (params.marketId) {
return (
<ContentWrapper>
<DealTicketContainer
marketId={params.marketId}
onDeposit={(assetId) =>
setViews({ type: ViewType.Deposit, assetId }, currentRouteId)
}
/>
<GetStarted />
</ContentWrapper>
);
} else {
return <CloseSidebar />;
}
}
if (view.type === ViewType.Info) {
if (params.marketId) {
return (
<ContentWrapper>
<MarketInfoAccordionContainer marketId={params.marketId} />
</ContentWrapper>
);
} else {
return <CloseSidebar />;
}
}
if (view.type === ViewType.Deposit) {
return (
<ContentWrapper title={t('Deposit')}>
<DepositContainer assetId={view.assetId} />
</ContentWrapper>
);
}
if (view.type === ViewType.Withdraw) {
return (
<ContentWrapper title={t('Withdraw')}>
<WithdrawContainer assetId={view.assetId} />
</ContentWrapper>
);
}
if (view.type === ViewType.Transfer) {
return (
<ContentWrapper title={t('Transfer')}>
<TransferContainer assetId={view.assetId} />
</ContentWrapper>
);
}
if (view.type === ViewType.Settings) {
return (
<ContentWrapper title={t('Settings')}>
<Settings />
</ContentWrapper>
);
}
throw new Error('invalid sidebar');
};
const ContentWrapper = ({
children,
title,
}: {
children: ReactNode;
title?: string;
}) => {
return (
<TinyScroll
className="h-full py-4 pl-3 pr-4 overflow-auto"
// panes have p-1, since sidebar is on the right make pl less to account for additional pane space
data-testid="sidebar-content"
>
{title && <h2 className="mb-4">{title}</h2>}
{children}
</TinyScroll>
);
};
/** If rendered will close sidebar */
const CloseSidebar = () => {
const currentRouteId = useGetCurrentRouteId();
const setViews = useSidebar((store) => store.setViews);
useEffect(() => {
setViews(null, currentRouteId);
}, [setViews, currentRouteId]);
return null;
};
export const useSidebar = create<{
views: { [key: string]: SidebarView | null };
setViews: (view: SidebarView | null, routeId: string) => void;
getView: (routeId: string) => SidebarView | null | undefined;
}>()((set, get) => ({
views: {},
setViews: (x, routeId) =>
set(({ views }) => ({ views: { ...views, [routeId]: x } })),
getView: (routeId) => get().views[routeId],
}));