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
43 changes: 35 additions & 8 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -175,6 +176,7 @@ function NotificationBell({ onClick }: { onClick: () => void }) {
}

function DashboardLayout() {
const navigate = useNavigate()
const {
connectedAddress,
activeTab,
Expand Down Expand Up @@ -292,18 +294,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
}
if (result.type === 'contract') {
setActiveTab('contracts')
return
}
setActiveTab('overview')
navigate('/overview')
}

return (
Expand Down Expand Up @@ -383,10 +381,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 (
<I18nProvider>
<DashboardLayout />
<RouterSync />
<Routes>
<Route path="/connect" element={<DashboardLayout />} />
<Route path="/*" element={<DashboardLayout />} />
</Routes>
</I18nProvider>
)
}
31 changes: 5 additions & 26 deletions src/components/layout/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React from 'react'
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, switchToCustomProfile, loadCustomNetworkProfiles } from '../../lib/stellar'
Expand Down Expand Up @@ -43,13 +44,9 @@ const NAV_ITEMS = [
]

export default function Sidebar({ isMobile = false }) {
const initialCustomHeaders = getCustomNetworkAuthHeaders()
const initialHeaderName = Object.keys(initialCustomHeaders)[0] || 'Authorization'
const [customHeaderName, setCustomHeaderName] = useState(initialHeaderName)
const [customHeaderValue, setCustomHeaderValue] = useState(initialCustomHeaders[initialHeaderName] || '')
const navigate = useNavigate()
const {
activeTab,
setActiveTab,
network,
setNetwork,
connectedAddress,
Expand Down Expand Up @@ -84,26 +81,8 @@ export default function Sidebar({ isMobile = false }) {
}, [network])

const handleNavClick = (tabId) => {
setActiveTab(tabId)
setMobileMenuOpen(false)
}

const handleSwitchProfile = async (profileId) => {
try {
await switchToCustomProfile(profileId)
setActiveProfileId(profileId)
// Force store update to refresh clients
const profile = customProfiles.find(p => p.id === profileId)
if (profile) {
updateCustomNetworkConfig({
horizonUrl: profile.horizonUrl,
sorobanUrl: profile.sorobanUrl,
passphrase: profile.passphrase,
})
}
} catch (err) {
console.error('Failed to switch profile:', err)
}
navigate(`/${tabId}`)
setMobileMenuOpen(false) // Close mobile menu after navigation
}

// Restore custom API key from sessionStorage on mount
Expand Down
5 changes: 5 additions & 0 deletions src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,11 @@ export const useStore = create<StoreState>((set, get) => ({
setStreamError: (e) => set({ streamError: e }),
}))

// ─── Expose store for e2e testing ────────────────────────────────────────────
if (typeof window !== 'undefined') {
(window as any).__store = useStore
}

// ─── Persistence middleware ───────────────────────────────────────────────────

if (typeof window !== 'undefined') {
Expand Down
5 changes: 4 additions & 1 deletion src/main.jsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -9,6 +10,8 @@ initPerformanceMonitoring();

ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
);
84 changes: 77 additions & 7 deletions tests/e2e/navigation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
});
Loading