From a5c89d4e06e3eef0a2dee0679d1bd60617b66113 Mon Sep 17 00:00:00 2001 From: Joseph Date: Thu, 28 May 2026 21:35:14 +0100 Subject: [PATCH] ux: Tab optimisation --- src/App.tsx | 39 ++++++++++++-- src/components/layout/Sidebar.jsx | 5 +- src/lib/store.ts | 5 ++ src/main.jsx | 5 +- tests/e2e/navigation.spec.js | 84 ++++++++++++++++++++++++++++--- 5 files changed, 124 insertions(+), 14 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 17bebb3..2a33bac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import React, { useEffect, useState, type ComponentType, type CSSProperties } from 'react' +import { Routes, Route, useNavigate, useLocation } from 'react-router-dom' import { I18nProvider } from './components/I18nProvider' import './i18n/index.js' import './styles/responsive.css' @@ -170,6 +171,7 @@ function NotificationBell({ onClick }: { onClick: () => void }) { } function DashboardLayout() { + const navigate = useNavigate() const { connectedAddress, activeTab, @@ -287,14 +289,14 @@ function DashboardLayout() { const handleSearchResult = (result: SearchResult | null | undefined): void => { if (!result) return if (result.type === 'transaction' || result.type === 'operation') { - setActiveTab('transactions') + navigate('/transactions') return } if (result.type === 'account') { - setActiveTab('account') + navigate('/account') return } - setActiveTab('overview') + navigate('/overview') } return ( @@ -374,10 +376,39 @@ function DashboardLayout() { ) } +function RouterSync() { + const navigate = useNavigate() + const location = useLocation() + const { connectedAddress, activeTab, setActiveTab } = useStore() + + const pathTab = location.pathname === '/' ? 'overview' : location.pathname.slice(1) + + useEffect(() => { + if (pathTab === 'connect') return + if (TABS[pathTab] && pathTab !== activeTab) { + setActiveTab(pathTab) + } + }, [location.pathname]) + + useEffect(() => { + if (!connectedAddress && pathTab !== 'connect') { + navigate('/connect', { replace: true }) + } else if (connectedAddress && pathTab === 'connect') { + navigate(`/${activeTab}`, { replace: true }) + } + }, [connectedAddress, location.pathname]) + + return null +} + export default function App() { return ( - + + + } /> + } /> + ) } diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx index 546d7e0..df62f14 100644 --- a/src/components/layout/Sidebar.jsx +++ b/src/components/layout/Sidebar.jsx @@ -1,4 +1,5 @@ import React, { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' import { useStore } from '../../lib/store' import CopyableValue from '../dashboard/CopyableValue' import { NETWORKS, updateCustomNetworkConfig } from '../../lib/stellar' @@ -30,9 +31,9 @@ const NAV_ITEMS = [ ] export default function Sidebar({ isMobile = false }) { + const navigate = useNavigate() const { activeTab, - setActiveTab, network, setNetwork, connectedAddress, @@ -43,7 +44,7 @@ export default function Sidebar({ isMobile = false }) { } = useStore() const handleNavClick = (tabId) => { - setActiveTab(tabId) + navigate(`/${tabId}`) setMobileMenuOpen(false) // Close mobile menu after navigation } diff --git a/src/lib/store.ts b/src/lib/store.ts index 38f6674..4bd71b2 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -260,6 +260,11 @@ export const useStore = create((set, get) => ({ })), })) +// ─── Expose store for e2e testing ──────────────────────────────────────────── +if (typeof window !== 'undefined') { + (window as any).__store = useStore +} + // ─── Persistence middleware ─────────────────────────────────────────────────── if (typeof window !== 'undefined') { diff --git a/src/main.jsx b/src/main.jsx index 53055c3..a331ca5 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,5 +1,6 @@ import React from "react"; import ReactDOM from "react-dom/client"; +import { BrowserRouter } from "react-router-dom"; import App from "./App"; import "./styles/globals.css"; import { initPerformanceMonitoring } from "./lib/performanceMonitoring"; @@ -9,6 +10,8 @@ initPerformanceMonitoring(); ReactDOM.createRoot(document.getElementById("root")).render( - + + + , ); diff --git a/tests/e2e/navigation.spec.js b/tests/e2e/navigation.spec.js index 54634d9..50d5ab1 100644 --- a/tests/e2e/navigation.spec.js +++ b/tests/e2e/navigation.spec.js @@ -13,16 +13,86 @@ test.describe('Sidebar navigation', () => { test('network toggle switches between testnet and mainnet', async ({ page }) => { const mainBtn = page.locator('button', { hasText: 'Main' }); await mainBtn.click(); - await expect(mainBtn).toHaveCSS('color', /0, 229, 255/); // cyan active color + await expect(mainBtn).toHaveCSS('color', /0, 229, 255/); }); - test('clicking Multisig nav item shows multisig page', async ({ page }) => { - await page.locator('button', { hasText: 'Multisig' }).click(); - await expect(page.locator('text=Multi-Signature')).toBeVisible(); + test('unauthenticated user visiting /transactions is redirected to /connect', async ({ page }) => { + await page.goto('/transactions'); + await expect(page).toHaveURL(/\/connect/); }); - test('clicking Overview nav item shows overview page', async ({ page }) => { - await page.locator('button', { hasText: 'Overview' }).click(); - await expect(page.locator('text=Overview')).toBeVisible(); + test('unauthenticated user visiting /overview is redirected to /connect', async ({ page }) => { + await page.goto('/overview'); + await expect(page).toHaveURL(/\/connect/); + }); + + test('unauthenticated user visiting /contracts is redirected to /connect', async ({ page }) => { + await page.goto('/contracts'); + await expect(page).toHaveURL(/\/connect/); + }); +}); + +test.describe('Authenticated navigation', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + await page.evaluate(() => { + const store = window.__store; + if (store) { + store.getState().setConnectedAddress('GA2C5W4Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5Q5X5'); + } + }); + }); + + test('sidebar click on Transactions updates URL to /transactions', async ({ page }) => { + await page.locator('aside button', { hasText: 'Transactions' }).click(); + await expect(page).toHaveURL(/\/transactions/); + }); + + test('sidebar click on Overview updates URL to /overview', async ({ page }) => { + await page.locator('aside button', { hasText: 'Overview' }).click(); + await expect(page).toHaveURL(/\/overview/); + }); + + test('sidebar click on Contracts updates URL to /contracts', async ({ page }) => { + await page.locator('aside button', { hasText: 'Contracts' }).click(); + await expect(page).toHaveURL(/\/contracts/); + }); + + test('sidebar click on Assets updates URL to /assets', async ({ page }) => { + await page.locator('aside button', { hasText: 'Assets' }).click(); + await expect(page).toHaveURL(/\/assets/); + }); + + test('sidebar click on Charts updates URL to /charts', async ({ page }) => { + await page.locator('aside button', { hasText: 'Charts' }).click(); + await expect(page).toHaveURL(/\/charts/); + }); + + test('browser back/forward updates active tab', async ({ page }) => { + await page.locator('aside button', { hasText: 'Contracts' }).click(); + await expect(page).toHaveURL(/\/contracts/); + + await page.locator('aside button', { hasText: 'Assets' }).click(); + await expect(page).toHaveURL(/\/assets/); + + await page.goBack(); + await expect(page).toHaveURL(/\/contracts/); + + await page.goBack(); + await expect(page).toHaveURL(/\/overview/); + + await page.goForward(); + await expect(page).toHaveURL(/\/contracts/); + }); + + test('direct navigation to /contracts renders contracts panel when connected', async ({ page }) => { + await page.goto('/contracts'); + await expect(page).toHaveURL(/\/contracts/); + await expect(page.locator('text=Contracts')).toBeVisible(); + }); + + test('direct navigation to /search renders search panel when connected', async ({ page }) => { + await page.goto('/search'); + await expect(page).toHaveURL(/\/search/); }); });