diff --git a/src/components/DurationOptions/DurationOptions.tsx b/src/components/DurationOptions/DurationOptions.tsx index 59a5279..7e96e5e 100644 --- a/src/components/DurationOptions/DurationOptions.tsx +++ b/src/components/DurationOptions/DurationOptions.tsx @@ -1,8 +1,12 @@ import { BarChart2, Clock, Expand } from 'lucide-react'; -export const DurationOptions = () => { +interface DurationOptionsProps { + className?: string; +} + +export const DurationOptions: React.FC = ({ className = '' }) => { return ( -
+
diff --git a/src/components/SideNav/README.md b/src/components/SideNav/README.md new file mode 100644 index 0000000..abf75e3 --- /dev/null +++ b/src/components/SideNav/README.md @@ -0,0 +1,104 @@ +# SideNav Component + +## Overview +A responsive navigation sidebar component that appears in landscape mode, providing easy access to main application routes. + +## Features +- Landscape-mode specific display +- Active route highlighting +- Icon and label navigation +- React Router integration +- Responsive design with Tailwind CSS + +## Usage + +### Basic Usage +```tsx +import { SideNav } from "@/components/SideNav"; + +function MainLayout() { + return ( +
+ + {/* Other content */} +
+ ); +} +``` + +## Navigation Structure + +```typescript +interface NavigationItem { + path: string; // Route path + icon: LucideIcon; // Lucide icon component + label: string; // Navigation label +} + +// Available routes +const routes = [ + { path: '/trade', icon: BarChart2, label: 'Trade' }, + { path: '/positions', icon: Clock, label: 'Positions' }, + { path: '/menu', icon: Menu, label: 'Menu' } +]; +``` + +## Responsive Behavior +- Hidden by default on portrait mode: `hidden` +- Visible in landscape mode: `landscape:flex` +- Fixed width: `w-16` +- Full height: `h-full` + +## Styling +Uses Tailwind CSS for styling: +```tsx +// Container +className="hidden landscape:flex flex-col h-full w-16 border-r bg-white" + +// Navigation buttons +className="flex flex-col items-center gap-1" + +// Active route highlighting +className="text-primary" // Active route +className="text-gray-500" // Inactive route +``` + +## Implementation Details + +### Route Handling +```typescript +const navigate = useNavigate(); +const location = useLocation(); + +// Active route check +const isActive = location.pathname === '/route'; +``` + +### Button Structure +```tsx + +``` + +## Example + +```tsx +import { SideNav } from "@/components/SideNav"; + +function App() { + return ( +
+ +
+ {/* Main content */} +
+
+ ); +} diff --git a/src/components/SideNav/SideNav.tsx b/src/components/SideNav/SideNav.tsx new file mode 100644 index 0000000..c00f75c --- /dev/null +++ b/src/components/SideNav/SideNav.tsx @@ -0,0 +1,42 @@ +import React from "react"; +import { BarChart2, Clock, Menu } from "lucide-react"; +import { useNavigate, useLocation } from "react-router-dom"; + +export const SideNav: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + + return ( + + ); +}; diff --git a/src/components/SideNav/__tests__/SideNav.test.tsx b/src/components/SideNav/__tests__/SideNav.test.tsx new file mode 100644 index 0000000..e759234 --- /dev/null +++ b/src/components/SideNav/__tests__/SideNav.test.tsx @@ -0,0 +1,74 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom'; +import { SideNav } from '../SideNav'; + +// Mock react-router-dom hooks +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: jest.fn(), + useLocation: jest.fn(), +})); + +describe('SideNav', () => { + const mockNavigate = jest.fn(); + const mockLocation = { pathname: '/trade' }; + + beforeEach(() => { + (useNavigate as jest.Mock).mockReturnValue(mockNavigate); + (useLocation as jest.Mock).mockReturnValue(mockLocation); + }); + + it('renders navigation buttons', () => { + render( + + + + ); + + expect(screen.getByText('Trade')).toBeInTheDocument(); + expect(screen.getByText('Positions')).toBeInTheDocument(); + expect(screen.getByText('Menu')).toBeInTheDocument(); + }); + + it('navigates to correct routes when buttons are clicked', () => { + render( + + + + ); + + fireEvent.click(screen.getByText('Trade')); + expect(mockNavigate).toHaveBeenCalledWith('/trade'); + + fireEvent.click(screen.getByText('Positions')); + expect(mockNavigate).toHaveBeenCalledWith('/positions'); + + fireEvent.click(screen.getByText('Menu')); + expect(mockNavigate).toHaveBeenCalledWith('/menu'); + }); + + it('highlights active route', () => { + render( + + + + ); + + const tradeButton = screen.getByText('Trade').parentElement; + expect(tradeButton).toHaveClass('text-primary'); + + const positionsButton = screen.getByText('Positions').parentElement; + expect(positionsButton).toHaveClass('text-gray-500'); + }); + + it('is hidden in portrait and visible in landscape', () => { + render( + + + + ); + + const nav = screen.getByRole('navigation'); + expect(nav).toHaveClass('hidden landscape:flex'); + }); +}); diff --git a/src/components/SideNav/index.ts b/src/components/SideNav/index.ts new file mode 100644 index 0000000..fa5f6f0 --- /dev/null +++ b/src/components/SideNav/index.ts @@ -0,0 +1 @@ +export { SideNav } from './SideNav'; diff --git a/src/components/TradeButton/TradeButton.tsx b/src/components/TradeButton/TradeButton.tsx index d8d4136..1c8c8c1 100644 --- a/src/components/TradeButton/TradeButton.tsx +++ b/src/components/TradeButton/TradeButton.tsx @@ -25,18 +25,22 @@ export const TradeButton: React.FC = ({ )} variant="default" > -
- {title} -
-
- {value} - {label} +
+
+ {title} +
+
+ {value} + {label} +
); diff --git a/src/components/TradeFields/TradeParam.tsx b/src/components/TradeFields/TradeParam.tsx index 26b6804..09dcc10 100644 --- a/src/components/TradeFields/TradeParam.tsx +++ b/src/components/TradeFields/TradeParam.tsx @@ -1,15 +1,17 @@ import { Card, CardContent } from "@/components/ui/card"; +import { cn } from "@/lib/utils"; interface TradeParamProps { label: string; value: string; + className?: string; onClick?: () => void; } -const TradeParam: React.FC = ({ label, value, onClick }) => { +const TradeParam: React.FC = ({ label, value, className, onClick }) => { return ( diff --git a/src/layouts/MainLayout/Footer.tsx b/src/layouts/MainLayout/Footer.tsx index 0f54281..a359f43 100644 --- a/src/layouts/MainLayout/Footer.tsx +++ b/src/layouts/MainLayout/Footer.tsx @@ -3,7 +3,7 @@ import { BottomNav } from "@/components/BottomNav"; export const Footer: React.FC = () => { return ( -
+
); diff --git a/src/layouts/MainLayout/Header.tsx b/src/layouts/MainLayout/Header.tsx index a972c95..5ba78b3 100644 --- a/src/layouts/MainLayout/Header.tsx +++ b/src/layouts/MainLayout/Header.tsx @@ -6,10 +6,10 @@ interface HeaderProps { export const Header: React.FC = ({ balance }) => { return ( -
+
Real - {balance} + {balance}
+
+
- Loading...
}> - - +
+
+
+ Loading...
}> + + + +
+
- Loading...}> - - +
+ Loading...
}> + + + -
-
-
- - + Loading...
}> + + +
+ +
+
+
+ +
-
+
{
-
+
Loading...
}> -
- -
+ Loading...
}> { render(); }); - expect(screen.getByText('Vol. 100 (1s) Index')).toBeInTheDocument(); - expect(screen.getByText('Rise/Fall')).toBeInTheDocument(); + // Use getAllByText since the text appears in both landscape and portrait views + expect(screen.getAllByText('Vol. 100 (1s) Index')).toHaveLength(2); + expect(screen.getAllByText('Rise/Fall')).toHaveLength(2); }); it('renders trade parameters from store', async () => {