Skip to content

Commit

Permalink
Merge pull request #10 from farabi-deriv/farabi/grwt-4891/landscape-l…
Browse files Browse the repository at this point in the history
…ayout

Farabi/grwt-4891/landscape-layout
  • Loading branch information
shafin-deriv authored Feb 4, 2025
2 parents acc9126 + 2730659 commit 67fb378
Show file tree
Hide file tree
Showing 13 changed files with 317 additions and 62 deletions.
8 changes: 6 additions & 2 deletions src/components/DurationOptions/DurationOptions.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { BarChart2, Clock, Expand } from 'lucide-react';

export const DurationOptions = () => {
interface DurationOptionsProps {
className?: string;
}

export const DurationOptions: React.FC<DurationOptionsProps> = ({ className = '' }) => {
return (
<div className="flex items-center justify-between p-4 border-t border-b">
<div className={`flex items-center justify-between p-4 border-t border-b ${className}`}>
<div className="flex items-center gap-4">
<button className="text-sm font-medium text-primary">1t</button>
<button className="text-sm font-medium text-gray-500">1m</button>
Expand Down
104 changes: 104 additions & 0 deletions src/components/SideNav/README.md
Original file line number Diff line number Diff line change
@@ -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 (
<div className="flex">
<SideNav />
{/* Other content */}
</div>
);
}
```

## 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
<button
onClick={() => navigate('/route')}
className={`flex flex-col items-center gap-1 ${
isActive ? 'text-primary' : 'text-gray-500'
}`}
>
<Icon className="w-5 h-5" />
<span className="text-xs">Label</span>
</button>
```

## Example

```tsx
import { SideNav } from "@/components/SideNav";

function App() {
return (
<div className="flex min-h-screen">
<SideNav />
<main className="flex-1">
{/* Main content */}
</main>
</div>
);
}
42 changes: 42 additions & 0 deletions src/components/SideNav/SideNav.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<nav className="hidden landscape:flex flex-col h-full w-16 border-r bg-white">
<div className="flex flex-col items-center gap-6 py-6">
<button
onClick={() => navigate('/trade')}
className={`flex flex-col items-center gap-1 ${
location.pathname === '/trade' ? 'text-primary' : 'text-gray-500'
}`}
>
<BarChart2 className="w-5 h-5" />
<span className="text-xs">Trade</span>
</button>
<button
onClick={() => navigate('/positions')}
className={`flex flex-col items-center gap-1 ${
location.pathname === '/positions' ? 'text-primary' : 'text-gray-500'
}`}
>
<Clock className="w-5 h-5" />
<span className="text-xs">Positions</span>
</button>
<button
onClick={() => navigate('/menu')}
className={`flex flex-col items-center gap-1 ${
location.pathname === '/menu' ? 'text-primary' : 'text-gray-500'
}`}
>
<Menu className="w-5 h-5" />
<span className="text-xs">Menu</span>
</button>
</div>
</nav>
);
};
74 changes: 74 additions & 0 deletions src/components/SideNav/__tests__/SideNav.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<MemoryRouter>
<SideNav />
</MemoryRouter>
);

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(
<MemoryRouter>
<SideNav />
</MemoryRouter>
);

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(
<MemoryRouter>
<SideNav />
</MemoryRouter>
);

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(
<MemoryRouter>
<SideNav />
</MemoryRouter>
);

const nav = screen.getByRole('navigation');
expect(nav).toHaveClass('hidden landscape:flex');
});
});
1 change: 1 addition & 0 deletions src/components/SideNav/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { SideNav } from './SideNav';
28 changes: 16 additions & 12 deletions src/components/TradeButton/TradeButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,22 @@ export const TradeButton: React.FC<TradeButtonProps> = ({
)}
variant="default"
>
<div className={cn(
"flex items-center justify-between w-full px-3",
title_position === "right" && "flex-row-reverse"
)}>
<span className="text-lg font-bold">{title}</span>
</div>
<div className={cn(
"flex items-center justify-between w-full px-3",
title_position === "right" && "flex-row-reverse"
)}>
<span className="text-sm">{value}</span>
<span className="text-sm opacity-80">{label}</span>
<div className="flex flex-col portrait:gap-1 landscape:gap-0 w-full px-3">
<div className={cn(
"flex items-center justify-between w-full",
"portrait:flex-row",
title_position === "right" ? "portrait:flex-row-reverse" : ""
)}>
<span className="text-lg font-bold">{title}</span>
</div>
<div className={cn(
"flex items-center justify-between w-full",
title_position === "right" && "flex-row-reverse",
"portrait:flex-row landscape:flex-row-reverse"
)}>
<span className="text-sm">{value}</span>
<span className="text-sm opacity-80">{label}</span>
</div>
</div>
</Button>
);
Expand Down
6 changes: 4 additions & 2 deletions src/components/TradeFields/TradeParam.tsx
Original file line number Diff line number Diff line change
@@ -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<TradeParamProps> = ({ label, value, onClick }) => {
const TradeParam: React.FC<TradeParamProps> = ({ label, value, className, onClick }) => {
return (
<Card
className={`flex-1 ${onClick ? 'cursor-pointer' : ''}`}
className={cn(`flex-1 ${onClick ? 'cursor-pointer' : ''}`, className)}
onClick={onClick}
>
<CardContent className="p-3">
Expand Down
2 changes: 1 addition & 1 deletion src/layouts/MainLayout/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { BottomNav } from "@/components/BottomNav";

export const Footer: React.FC = () => {
return (
<footer className="border-t">
<footer className="border-t landscape:hidden">
<BottomNav />
</footer>
);
Expand Down
4 changes: 2 additions & 2 deletions src/layouts/MainLayout/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ interface HeaderProps {

export const Header: React.FC<HeaderProps> = ({ balance }) => {
return (
<header className="flex items-center justify-between p-4">
<header className="flex items-center justify-between p-4 landscape:hidden">
<div className="flex flex-col">
<span className="text-sm text-gray-700">Real</span>
<span className="text-2xl font-bold text-teal-500">{balance}</span>
<span className="text-xl font-bold text-teal-500">{balance}</span>
</div>

<button className="px-4 py-2 font-bold border border-gray-700 rounded-3xl">
Expand Down
16 changes: 10 additions & 6 deletions src/layouts/MainLayout/MainLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import React from "react";
import { Footer } from "./Footer";
import { Header } from "./Header";
import { SideNav } from "@/components/SideNav";

interface MainLayoutProps {
children: React.ReactNode;
}

export const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
return (
<div className="flex flex-col min-h-screen">
<Header balance="10,000 USD" />
<main className="flex-1 flex flex-col">
{children}
</main>
<Footer />
<div className="flex min-h-screen">
<SideNav />
<div className="flex flex-col flex-1">
<Header balance="10,000 USD" />
<main className="flex-1 flex flex-col">
{children}
</main>
<Footer />
</div>
</div>
);
};
2 changes: 1 addition & 1 deletion src/layouts/MainLayout/__tests__/MainLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('MainLayout', () => {
it('applies correct layout classes', () => {
renderWithRouter(<div>Test Content</div>);
const container = screen.getByText('Test Content').parentElement?.parentElement;
expect(container).toHaveClass('flex flex-col min-h-screen');
expect(container).toHaveClass('flex flex-col flex-1');
});

it('renders with footer', () => {
Expand Down
Loading

0 comments on commit 67fb378

Please sign in to comment.