feat: multi-tab instrument analysis
Each /instruments/:id navigation opens a persistent tab in the TabBar. Multiple instruments can be open simultaneously — state is preserved when switching. - TabsContext: adds instrumentIds[], openInstrument(), closeInstrument() - TabBar: renders instrument tabs (teal, monospaced, TrendingUp icon) after static tabs, separated by a divider - InstrumentDashboard: accepts instrumentIdProp so keep-alive instances use the right id regardless of URL - App: InstrumentRoute registers the tab on navigation; InstrumentKeepAlive mounts one dashboard per open instrument; NormalRoutes hides on instrument paths (hidden div, Routes still fires) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { BrowserRouter, Routes, Route, useLocation, Navigate } from 'react-router-dom'
|
||||
import { BrowserRouter, Routes, Route, useLocation, useNavigate, useParams, Navigate } from 'react-router-dom'
|
||||
import { useEffect, ComponentType } from 'react'
|
||||
import Sidebar from './components/layout/Sidebar'
|
||||
import TabBar from './components/layout/TabBar'
|
||||
@@ -96,18 +96,41 @@ function KeepAlivePage({ path, component: Component }: { path: string; component
|
||||
)
|
||||
}
|
||||
|
||||
// ── NormalRoutes — only InstrumentDashboard (uses :id param) + redirects ─────
|
||||
// ── Instrument keep-alive tabs ────────────────────────────────────────────────
|
||||
|
||||
// Registers the instrument tab on navigation (renders nothing itself)
|
||||
function InstrumentRoute() {
|
||||
const { id = 'EURUSD' } = useParams<{ id: string }>()
|
||||
const { openInstrument } = useTabs()
|
||||
useEffect(() => { openInstrument(id.toUpperCase()) }, [id, openInstrument])
|
||||
return null
|
||||
}
|
||||
|
||||
// Keeps one InstrumentDashboard mounted per open instrument tab
|
||||
function InstrumentKeepAlive({ id }: { id: string }) {
|
||||
const location = useLocation()
|
||||
const isActive = location.pathname.toLowerCase() === `/instruments/${id.toLowerCase()}`
|
||||
return (
|
||||
<div className={isActive ? 'flex-1 min-h-0 overflow-y-auto' : 'hidden'}>
|
||||
<InstrumentDashboard instrumentIdProp={id} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ── NormalRoutes — legacy redirects + instrument tab registration ─────────────
|
||||
|
||||
function NormalRoutes() {
|
||||
const location = useLocation()
|
||||
if (KEEP_ALIVE_PATHS.has(location.pathname)) return null
|
||||
|
||||
// On instrument paths the keep-alive layer renders the UI; this Routes just registers the tab
|
||||
const isInstrument = /^\/instruments\/\w/.test(location.pathname)
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<div className={isInstrument ? 'hidden' : 'flex-1 overflow-auto'}>
|
||||
<Routes>
|
||||
{/* InstrumentDashboard kept here: useParams() reads :id from URL */}
|
||||
<Route path="/instruments" element={<Navigate to="/instruments/SPY" replace />} />
|
||||
<Route path="/instruments/:id" element={<InstrumentDashboard />} />
|
||||
<Route path="/instruments" element={<Navigate to="/instruments/EURUSD" replace />} />
|
||||
<Route path="/instruments/:id" element={<InstrumentRoute />} />
|
||||
{/* Legacy redirects */}
|
||||
<Route path="/impact" element={<Navigate to="/market-events" replace />} />
|
||||
<Route path="/timeline" element={<Navigate to="/market-events" replace />} />
|
||||
@@ -125,23 +148,33 @@ function GlobalWatcher() {
|
||||
|
||||
// ── App ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
function AppInner() {
|
||||
const { instrumentIds } = useTabs()
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden">
|
||||
<GlobalWatcher />
|
||||
<Sidebar />
|
||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||
<TabBar />
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
{KEEP_ALIVE_DEFS.map(def => (
|
||||
<KeepAlivePage key={def.path} path={def.path} component={def.component} />
|
||||
))}
|
||||
{instrumentIds.map(id => (
|
||||
<InstrumentKeepAlive key={id} id={id} />
|
||||
))}
|
||||
<NormalRoutes />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<TabsProvider>
|
||||
<div className="flex h-screen overflow-hidden">
|
||||
<GlobalWatcher />
|
||||
<Sidebar />
|
||||
<div className="flex-1 flex flex-col min-h-0 overflow-hidden">
|
||||
<TabBar />
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
{KEEP_ALIVE_DEFS.map(def => (
|
||||
<KeepAlivePage key={def.path} path={def.path} component={def.component} />
|
||||
))}
|
||||
<NormalRoutes />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AppInner />
|
||||
</TabsProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user