Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ The app runs at http://localhost:5173 by default.

```
src/
components/ reusable UI (Navbar, Footer, QuoteCard, TransferRow, ...)
components/ reusable UI (Navbar, Sidebar, Footer, QuoteCard, TransferRow, ...)
pages/ route screens (Home, SendMoney, Transfers, NotFound)
services/ mock api, wallet, fx and quote logic
hooks/ useWallet, useTransfers
Expand All @@ -63,6 +63,20 @@ cp .env.example .env
- `npm run test:watch` — watch mode for local test development
- `npm run lighthouse` — run Lighthouse CI against the local preview server

## Responsive Layout

The app adapts to three viewport ranges:

| Range | Target | Layout |
|---|---|---|
| >1024px | Desktop | Full sidebar (240px), 3-column features grid |
| 721–1024px | Tablet landscape | Full sidebar with toggle, 2-column features grid, compact content padding |
| ≤720px | Tablet portrait / mobile | Collapsed sidebar (60px), single-column layouts, wrapped navbar |
| ≤420px | Small mobile | Stacked hero actions, single-column currency grid |

The Sidebar is rendered alongside the main content area on all viewports wider than
720px and collapses automatically at narrower widths.

## Testing

The test suite includes comprehensive coverage of wallet connection handling:
Expand All @@ -86,9 +100,9 @@ The test suite includes comprehensive coverage of wallet connection handling:
- User interaction flows

Integration tests cover send-money validation, successful transfer submission,
pending button behavior, duplicate-submission prevention, Transfers page filter
sync (search, status, and date-range presets), and keyboard tab order across
the main pages.
pending button behavior, duplicate-submission prevention, Transfers page
filter sync (search, status, and date-range presets such as last 7/30/90 days),
and landscape tablet layout integration.

## Accessibility

Expand Down
4 changes: 4 additions & 0 deletions src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { AppProvider } from './context/AppContext.jsx'
import { LocaleProvider } from './context/I18nContext.jsx'
import ErrorBoundary from './components/ErrorBoundary.jsx'
import Navbar from './components/Navbar.jsx'
import Sidebar from './components/Sidebar.jsx'
import Footer from './components/Footer.jsx'
import Home from './pages/Home.jsx'
import SendMoney from './pages/SendMoney.jsx'
Expand All @@ -22,6 +23,8 @@ export default function App() {
<a href="#main-content" className="skip-link">
Skip to content
</a>
<Sidebar />
<div className="app-content">
<Navbar />
<main id="main-content" className="app-main">
<ErrorBoundary>
Expand All @@ -34,6 +37,7 @@ export default function App() {
</ErrorBoundary>
</main>
<Footer />
</div>
</div>
</BrowserRouter>
</AppProvider>
Expand Down
10 changes: 10 additions & 0 deletions src/responsive.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
/* Responsive tweaks for narrow viewports. */

@media (min-width: 721px) and (max-width: 1024px) {
.features {
grid-template-columns: repeat(2, 1fr);
}

.app-main {
padding: 1.5rem 1.25rem;
}
}

@media (max-width: 720px) {
.navbar {
flex-wrap: wrap;
Expand Down
88 changes: 88 additions & 0 deletions test/integration/landscape-layout.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import fs from 'node:fs'
import path from 'node:path'
import { render, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it } from 'vitest'
import App from '../../src/App.jsx'

function readCSS(filename) {
return fs.readFileSync(
path.resolve(process.cwd(), 'src', filename),
'utf8',
)
}

describe('landscape layout for tablets', () => {
describe('responsive.css', () => {
const css = readCSS('responsive.css')

it('contains a tablet landscape breakpoint (721px–1024px)', () => {
expect(css).toMatch(
/@media\s*\(\s*min-width\s*:\s*721px\s*\)\s*and\s*\(\s*max-width\s*:\s*1024px\s*\)/,
)
})

it('adjusts .features to 2 columns within the landscape breakpoint', () => {
const start = css.indexOf('@media (min-width: 721px)')
const block = css.slice(start, css.indexOf('@media (max-width: 720px)'))
expect(block).toContain('grid-template-columns')
expect(block).toContain('repeat(2, 1fr)')
})

it('adjusts .app-main padding within the landscape breakpoint', () => {
const start = css.indexOf('@media (min-width: 721px)')
const block = css.slice(start, css.indexOf('@media (max-width: 720px)'))
expect(block).toContain('.app-main')
expect(block).toContain('padding')
})
})

describe('sidebar integration in App', () => {
beforeEach(() => {
localStorage.clear()
})

it('renders the Sidebar as part of the app layout', () => {
render(<App />)
const sidebar = document.querySelector('.sidebar')
expect(sidebar).toBeInTheDocument()
})

it('renders sidebar navigation links within the app', () => {
render(<App />)
const homeLinks = screen.getAllByText('Home')
expect(homeLinks.length).toBeGreaterThanOrEqual(1)
const sidebarLinks = document.querySelectorAll('.sidebar-link')
expect(sidebarLinks.length).toBe(3)
})

it('renders the sidebar toggle button', () => {
render(<App />)
const toggle = screen.getByLabelText('Collapse sidebar')
expect(toggle).toBeInTheDocument()
})

it('renders the content area alongside the sidebar', () => {
render(<App />)
const app = document.querySelector('.app')
const sidebar = document.querySelector('.sidebar')
const content = document.querySelector('.app-content')

expect(app).toBeInTheDocument()
expect(sidebar).toBeInTheDocument()
expect(content).toBeInTheDocument()

expect(app.contains(sidebar)).toBe(true)
expect(app.contains(content)).toBe(true)
})

it('renders Navbar and Footer inside .app-content', () => {
render(<App />)
const content = document.querySelector('.app-content')
const navbar = document.querySelector('.navbar')
const footer = document.querySelector('.footer')

expect(content.contains(navbar)).toBe(true)
expect(content.contains(footer)).toBe(true)
})
})
})
Loading