feat(trading): mobile layout and buttons (#5751)

Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
m.ray 2024-02-09 12:30:24 +02:00 committed by GitHub
parent c5a27dc6a2
commit 41fd14dd00
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 366 additions and 36 deletions

View File

@ -0,0 +1,235 @@
import { Route, Routes } from 'react-router-dom';
import {
Intent,
MobileActionsDropdown,
Tooltip,
TradingButton,
TradingDropdownItem,
VegaIcon,
VegaIconNames,
} from '@vegaprotocol/ui-toolkit';
import { type BarView, ViewType, useSidebar } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
import { useEffect } from 'react';
import classNames from 'classnames';
import { useVegaWallet, useVegaWalletDialogStore } from '@vegaprotocol/wallet';
const ViewInitializer = () => {
const currentRouteId = useGetCurrentRouteId();
const { setViews, getView } = useSidebar();
const view = getView(currentRouteId);
const { screenSize } = useScreenDimensions();
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
useEffect(() => {
if (largeScreen && view === undefined) {
setViews({ type: ViewType.Order }, currentRouteId);
}
}, [setViews, view, currentRouteId, largeScreen]);
return null;
};
export const MarketsMobileSidebar = () => {
const t = useT();
const currentRouteId = useGetCurrentRouteId();
const { pubKeys, isReadOnly } = useVegaWallet();
const openVegaWalletDialog = useVegaWalletDialogStore(
(store) => store.openVegaWalletDialog
);
return (
<Routes>
<Route
path=":marketId"
element={
<>
<ViewInitializer />
<div className="grid grid-cols-3 grow md:grow-0 md:flex lg:flex-col items-center gap-2 lg:gap-4 p-1">
{!pubKeys || isReadOnly ? (
<>
<TradingButton
intent={Intent.Primary}
size="medium"
onClick={() => {
openVegaWalletDialog();
}}
>
{t('Connect')}
</TradingButton>
<MobileButton
view={ViewType.Order}
tooltip={t('Trade')}
routeId={currentRouteId}
/>
<MobileBarActionsDropdown currentRouteId={currentRouteId} />
</>
) : (
<>
<MobileButton
view={ViewType.Order}
tooltip={t('Trade')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Deposit}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileBarActionsDropdown currentRouteId={currentRouteId} />
</>
)}
</div>
</>
}
/>
</Routes>
);
};
export const MobileButton = ({
view,
tooltip: label,
disabled = false,
onClick,
routeId,
}: {
view?: ViewType;
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: BarView['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={label} align="center" side="right" sideOffset={10}>
<TradingButton
className={buttonClasses}
data-testid={view}
onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled}
>
{label}
</TradingButton>
</Tooltip>
);
};
export const MobileDropdownItem = ({
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: BarView['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}>
<TradingDropdownItem
className={buttonClasses}
data-testid={view}
onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled}
>
<VegaIcon name={icon} size={20} />
{tooltip}
</TradingDropdownItem>
</Tooltip>
);
};
export const MobileBarActionsDropdown = ({
currentRouteId,
}: {
currentRouteId: string;
}) => {
const t = useT();
return (
<MobileActionsDropdown>
<MobileDropdownItem
view={ViewType.Deposit}
icon={VegaIconNames.DEPOSIT}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Withdraw}
icon={VegaIconNames.WITHDRAW}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Transfer}
icon={VegaIconNames.TRANSFER}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Info}
icon={VegaIconNames.BREAKDOWN}
tooltip={t('Market specification')}
routeId={currentRouteId}
/>
<MobileDropdownItem
view={ViewType.Settings}
icon={VegaIconNames.COG}
tooltip={t('Settings')}
routeId={currentRouteId}
/>
</MobileActionsDropdown>
);
};

View File

@ -2,6 +2,7 @@ import { VegaIconNames } from '@vegaprotocol/ui-toolkit';
import { SidebarButton, ViewType } from '../../components/sidebar'; import { SidebarButton, ViewType } from '../../components/sidebar';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { MobileButton } from '../markets/mobile-buttons';
export const PortfolioSidebar = () => { export const PortfolioSidebar = () => {
const t = useT(); const t = useT();
@ -30,3 +31,28 @@ export const PortfolioSidebar = () => {
</> </>
); );
}; };
export const PortfolioMobileSidebar = () => {
const t = useT();
const currentRouteId = useGetCurrentRouteId();
return (
<div className="grid grid-cols-3 grow md:grow-0 md:flex lg:flex-col items-center gap-2 lg:gap-4 p-1">
<MobileButton
view={ViewType.Deposit}
tooltip={t('Deposit')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Withdraw}
tooltip={t('Withdraw')}
routeId={currentRouteId}
/>
<MobileButton
view={ViewType.Transfer}
tooltip={t('Transfer')}
routeId={currentRouteId}
/>
</div>
);
};

View File

@ -16,7 +16,7 @@ export const LayoutWithSidebar = ({
const sidebarOpen = sidebarView !== null; const sidebarOpen = sidebarView !== null;
const gridClasses = classNames( const gridClasses = classNames(
'h-full relative z-0 grid', 'h-full relative z-0 grid',
'grid-rows-[min-content_1fr_40px]', 'grid-rows-[min-content_1fr_50px]',
'lg:grid-rows-[min-content_1fr]', 'lg:grid-rows-[min-content_1fr]',
'lg:grid-cols-[1fr_280px_40px]', 'lg:grid-cols-[1fr_280px_40px]',
'xxxl:grid-cols-[1fr_320px_40px]' 'xxxl:grid-cols-[1fr_320px_40px]'

View File

@ -17,6 +17,7 @@ import { useVegaWallet, useViewAsDialog } from '@vegaprotocol/wallet';
import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id'; import { useGetCurrentRouteId } from '../../lib/hooks/use-get-current-route-id';
import { useT } from '../../lib/use-t'; import { useT } from '../../lib/use-t';
import { ErrorBoundary } from '../error-boundary'; import { ErrorBoundary } from '../error-boundary';
import { useScreenDimensions } from '@vegaprotocol/react-helpers';
export enum ViewType { export enum ViewType {
Order = 'Order', Order = 'Order',
@ -26,9 +27,10 @@ export enum ViewType {
Transfer = 'Transfer', Transfer = 'Transfer',
Settings = 'Settings', Settings = 'Settings',
ViewAs = 'ViewAs', ViewAs = 'ViewAs',
Close = 'Close',
} }
type SidebarView = export type BarView =
| { | {
type: ViewType.Deposit; type: ViewType.Deposit;
assetId?: string; assetId?: string;
@ -49,6 +51,9 @@ type SidebarView =
} }
| { | {
type: ViewType.Settings; type: ViewType.Settings;
}
| {
type: ViewType.Close;
}; };
export const Sidebar = ({ options }: { options?: ReactNode }) => { export const Sidebar = ({ options }: { options?: ReactNode }) => {
@ -57,26 +62,52 @@ export const Sidebar = ({ options }: { options?: ReactNode }) => {
const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1'; const navClasses = 'flex lg:flex-col items-center gap-2 lg:gap-4 p-1';
const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen); const setViewAsDialogOpen = useViewAsDialog((state) => state.setOpen);
const { pubKeys } = useVegaWallet(); const { pubKeys } = useVegaWallet();
const { isMobile } = useScreenDimensions();
const { getView } = useSidebar((store) => ({
setViews: store.setViews,
getView: store.getView,
}));
const currView = getView(currentRouteId);
return ( return (
<div className="flex h-full p-1 lg:flex-col gap-2" data-testid="sidebar"> <div className="flex h-full lg:flex-col gap-1" data-testid="sidebar">
{options && <nav className={navClasses}>{options}</nav>} {options && (
<nav className={classNames(navClasses, 'ml-auto lg:mt-auto lg:ml-0')}> <nav className={classNames(navClasses, 'flex grow')}>{options}</nav>
<SidebarButton )}
view={ViewType.ViewAs} <nav
onClick={() => { className={classNames(
setViewAsDialogOpen(true); navClasses,
}} 'ml-auto lg:mt-auto lg:ml-0 shrink-0'
icon={VegaIconNames.EYE} )}
tooltip={t('View as party')} >
disabled={Boolean(pubKeys)} {!isMobile ? (
routeId={currentRouteId} <>
/> <SidebarButton
<SidebarButton view={ViewType.ViewAs}
view={ViewType.Settings} onClick={() => {
icon={VegaIconNames.COG} setViewAsDialogOpen(true);
tooltip={t('Settings')} }}
routeId={currentRouteId} 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}
/>
</>
) : (
currView && (
<SidebarButton
view={ViewType.Close}
icon={VegaIconNames.ARROW_LEFT}
tooltip={t('Back')}
routeId={currentRouteId}
/>
)
)}
<NodeHealthContainer /> <NodeHealthContainer />
</nav> </nav>
</div> </div>
@ -103,7 +134,7 @@ export const SidebarButton = ({
getView: store.getView, getView: store.getView,
})); }));
const currView = getView(routeId); const currView = getView(routeId);
const onSelect = (view: SidebarView['type']) => { const onSelect = (view: BarView['type']) => {
if (view === currView?.type) { if (view === currView?.type) {
setViews(null, routeId); setViews(null, routeId);
} else { } else {
@ -133,7 +164,7 @@ export const SidebarButton = ({
<button <button
className={buttonClasses} className={buttonClasses}
data-testid={view} data-testid={view}
onClick={onClick || (() => onSelect(view as SidebarView['type']))} onClick={onClick || (() => onSelect(view as BarView['type']))}
disabled={disabled} disabled={disabled}
> >
<VegaIcon name={icon} size={20} /> <VegaIcon name={icon} size={20} />
@ -180,6 +211,10 @@ export const SidebarContent = () => {
} }
} }
if (view.type === ViewType.Close) {
return <CloseSidebar />;
}
if (view.type === ViewType.Info) { if (view.type === ViewType.Info) {
if (params.marketId) { if (params.marketId) {
return ( return (
@ -267,9 +302,9 @@ const CloseSidebar = () => {
}; };
export const useSidebar = create<{ export const useSidebar = create<{
views: { [key: string]: SidebarView | null }; views: { [key: string]: BarView | null };
setViews: (view: SidebarView | null, routeId: string) => void; setViews: (view: BarView | null, routeId: string) => void;
getView: (routeId: string) => SidebarView | null | undefined; getView: (routeId: string) => BarView | null | undefined;
}>()((set, get) => ({ }>()((set, get) => ({
views: {}, views: {},
setViews: (x, routeId) => setViews: (x, routeId) =>

View File

@ -24,7 +24,10 @@ import { compact } from 'lodash';
import { useFeatureFlags } from '@vegaprotocol/environment'; import { useFeatureFlags } from '@vegaprotocol/environment';
import { LiquidityHeader } from '../components/liquidity-header'; import { LiquidityHeader } from '../components/liquidity-header';
import { MarketHeader, MobileMarketHeader } from '../components/market-header'; import { MarketHeader, MobileMarketHeader } from '../components/market-header';
import { PortfolioSidebar } from '../client-pages/portfolio/portfolio-sidebar'; import {
PortfolioMobileSidebar,
PortfolioSidebar,
} from '../client-pages/portfolio/portfolio-sidebar';
import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar'; import { LiquiditySidebar } from '../client-pages/liquidity/liquidity-sidebar';
import { MarketsSidebar } from '../client-pages/markets/markets-sidebar'; import { MarketsSidebar } from '../client-pages/markets/markets-sidebar';
import { useT } from '../lib/use-t'; import { useT } from '../lib/use-t';
@ -33,9 +36,10 @@ import { CompetitionsTeams } from '../client-pages/competitions/competitions-tea
import { CompetitionsTeam } from '../client-pages/competitions/competitions-team'; import { CompetitionsTeam } from '../client-pages/competitions/competitions-team';
import { CompetitionsCreateTeam } from '../client-pages/competitions/competitions-create-team'; import { CompetitionsCreateTeam } from '../client-pages/competitions/competitions-create-team';
import { CompetitionsUpdateTeam } from '../client-pages/competitions/competitions-update-team'; import { CompetitionsUpdateTeam } from '../client-pages/competitions/competitions-update-team';
import { MarketsMobileSidebar } from '../client-pages/markets/mobile-buttons';
import { useScreenDimensions } from '@vegaprotocol/react-helpers'; import { useScreenDimensions } from '@vegaprotocol/react-helpers';
// These must remain dynamically imported as pennant cannot be compiled by nextjs due to ESM // These must remain dynamically imported as pennant cannot be compiled by Next.js due to ESM
// Using dynamic imports is a workaround for this until pennant is published as ESM // Using dynamic imports is a workaround for this until pennant is published as ESM
const MarketPage = lazy(() => import('../client-pages/market')); const MarketPage = lazy(() => import('../client-pages/market'));
const Portfolio = lazy(() => import('../client-pages/portfolio')); const Portfolio = lazy(() => import('../client-pages/portfolio'));
@ -54,6 +58,17 @@ export const useRouterConfig = (): RouteObject[] => {
const { screenSize } = useScreenDimensions(); const { screenSize } = useScreenDimensions();
const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize); const largeScreen = ['lg', 'xl', 'xxl', 'xxxl'].includes(screenSize);
const marketHeader = largeScreen ? <MarketHeader /> : <MobileMarketHeader />; const marketHeader = largeScreen ? <MarketHeader /> : <MobileMarketHeader />;
const marketsSidebar = largeScreen ? (
<MarketsSidebar />
) : (
<MarketsMobileSidebar />
);
const portfolioSidebar = largeScreen ? (
<PortfolioSidebar />
) : (
<PortfolioMobileSidebar />
);
const routeConfig = compact([ const routeConfig = compact([
{ {
index: true, index: true,
@ -70,7 +85,7 @@ export const useRouterConfig = (): RouteObject[] => {
featureFlags.REFERRALS featureFlags.REFERRALS
? { ? {
path: AppRoutes.REFERRALS, path: AppRoutes.REFERRALS,
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />, element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [ children: [
{ {
element: ( element: (
@ -103,7 +118,7 @@ export const useRouterConfig = (): RouteObject[] => {
featureFlags.TEAM_COMPETITION featureFlags.TEAM_COMPETITION
? { ? {
path: AppRoutes.COMPETITIONS, path: AppRoutes.COMPETITIONS,
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />, element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [ children: [
// pages with planets and stars // pages with planets and stars
{ {
@ -134,7 +149,7 @@ export const useRouterConfig = (): RouteObject[] => {
: undefined, : undefined,
{ {
path: 'fees/*', path: 'fees/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />, element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [ children: [
{ {
index: true, index: true,
@ -144,7 +159,7 @@ export const useRouterConfig = (): RouteObject[] => {
}, },
{ {
path: 'rewards/*', path: 'rewards/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />, element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [ children: [
{ {
index: true, index: true,
@ -155,7 +170,7 @@ export const useRouterConfig = (): RouteObject[] => {
{ {
path: 'markets/*', path: 'markets/*',
element: ( element: (
<LayoutWithSidebar header={marketHeader} sidebar={<MarketsSidebar />} /> <LayoutWithSidebar header={marketHeader} sidebar={marketsSidebar} />
), ),
children: [ children: [
{ {
@ -176,7 +191,7 @@ export const useRouterConfig = (): RouteObject[] => {
}, },
{ {
path: 'portfolio/*', path: 'portfolio/*',
element: <LayoutWithSidebar sidebar={<PortfolioSidebar />} />, element: <LayoutWithSidebar sidebar={portfolioSidebar} />,
children: [ children: [
{ {
index: true, index: true,

View File

@ -38,7 +38,7 @@ export function Dialog({
); );
const wrapperClasses = classNames( const wrapperClasses = classNames(
// Dimensions // Dimensions
'w-screen sm:max-w-[90vw] p-4 md:p-8', 'max-w-[95vw] sm:max-w-[90vw] p-4 md:p-8',
// Need to apply background and text colors again as content is rendered in a portal // Need to apply background and text colors again as content is rendered in a portal
'dark:bg-black bg-white dark:text-white', 'dark:bg-black bg-white dark:text-white',
getIntentBorder(intent), getIntentBorder(intent),

View File

@ -1,4 +1,5 @@
import { VegaIcon, VegaIconNames } from '../icon'; import { VegaIcon, VegaIconNames } from '../icon';
import { TradingButton } from '../trading-button';
import { import {
TradingDropdown, TradingDropdown,
TradingDropdownContent, TradingDropdownContent,
@ -15,6 +16,16 @@ export const ActionsDropdownTrigger = () => {
); );
}; };
export const MobileActionsDropdownTrigger = () => {
return (
<TradingDropdownTrigger data-testid="dropdown-menu">
<TradingButton size="medium">
<VegaIcon name={VegaIconNames.KEBAB} />
</TradingButton>
</TradingDropdownTrigger>
);
};
type ActionMenuContentProps = React.ComponentProps< type ActionMenuContentProps = React.ComponentProps<
typeof TradingDropdownContent typeof TradingDropdownContent
>; >;
@ -26,3 +37,11 @@ export const ActionsDropdown = (props: ActionMenuContentProps) => {
</TradingDropdown> </TradingDropdown>
); );
}; };
export const MobileActionsDropdown = (props: ActionMenuContentProps) => {
return (
<TradingDropdown trigger={<MobileActionsDropdownTrigger />}>
<TradingDropdownContent {...props} side="bottom" align="end" />
</TradingDropdown>
);
};

View File

@ -1,2 +1,2 @@
export * from './trading-dropdown';
export * from './actions-dropdown'; export * from './actions-dropdown';
export * from './trading-dropdown';