finished initial symbols pages
This commit is contained in:
parent
62706cdb42
commit
4e4a048988
13 changed files with 688 additions and 107 deletions
|
|
@ -4,7 +4,7 @@ import { ExchangesPage } from '@/features/exchanges';
|
||||||
import { MonitoringPage } from '@/features/monitoring';
|
import { MonitoringPage } from '@/features/monitoring';
|
||||||
import { PipelinePage } from '@/features/pipeline';
|
import { PipelinePage } from '@/features/pipeline';
|
||||||
import { BacktestPage } from '@/features/backtest';
|
import { BacktestPage } from '@/features/backtest';
|
||||||
import { ChartPage } from '@/features/charts';
|
import { SymbolsPage, SymbolDetailPage } from '@/features/symbols';
|
||||||
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom';
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
|
|
@ -14,7 +14,10 @@ export function App() {
|
||||||
<Route path="/" element={<Layout />}>
|
<Route path="/" element={<Layout />}>
|
||||||
<Route index element={<Navigate to="/dashboard" replace />} />
|
<Route index element={<Navigate to="/dashboard" replace />} />
|
||||||
<Route path="dashboard" element={<DashboardPage />} />
|
<Route path="dashboard" element={<DashboardPage />} />
|
||||||
<Route path="charts" element={<ChartPage />} />
|
<Route path="symbols">
|
||||||
|
<Route index element={<SymbolsPage />} />
|
||||||
|
<Route path=":symbol" element={<SymbolDetailPage />} />
|
||||||
|
</Route>
|
||||||
<Route path="exchanges" element={<ExchangesPage />} />
|
<Route path="exchanges" element={<ExchangesPage />} />
|
||||||
<Route
|
<Route
|
||||||
path="portfolio"
|
path="portfolio"
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@ import { navigation } from '@/lib/constants';
|
||||||
import type { NavigationItem } from '@/lib/constants';
|
import type { NavigationItem } from '@/lib/constants';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Dialog, Transition } from '@headlessui/react';
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
import { XMarkIcon, ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/24/outline';
|
import { XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
import { Fragment, useState } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { NavLink, useLocation } from 'react-router-dom';
|
import { NavLink, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
|
|
@ -79,29 +79,6 @@ export function Sidebar({ sidebarOpen, setSidebarOpen }: SidebarProps) {
|
||||||
function SidebarContent() {
|
function SidebarContent() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
// Auto-expand items that have active children
|
|
||||||
const getInitialExpanded = () => {
|
|
||||||
const expanded = new Set<string>();
|
|
||||||
navigation.forEach(item => {
|
|
||||||
if (item.children && item.children.some(child => location.pathname === child.href)) {
|
|
||||||
expanded.add(item.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return expanded;
|
|
||||||
};
|
|
||||||
|
|
||||||
const [expandedItems, setExpandedItems] = useState<Set<string>>(getInitialExpanded());
|
|
||||||
|
|
||||||
const toggleExpanded = (name: string) => {
|
|
||||||
const newExpanded = new Set(expandedItems);
|
|
||||||
if (newExpanded.has(name)) {
|
|
||||||
newExpanded.delete(name);
|
|
||||||
} else {
|
|
||||||
newExpanded.add(name);
|
|
||||||
}
|
|
||||||
setExpandedItems(newExpanded);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isChildActive = (children: NavigationItem[]) => {
|
const isChildActive = (children: NavigationItem[]) => {
|
||||||
return children.some(child => location.pathname === child.href);
|
return children.some(child => location.pathname === child.href);
|
||||||
};
|
};
|
||||||
|
|
@ -119,12 +96,11 @@ function SidebarContent() {
|
||||||
<li key={item.name}>
|
<li key={item.name}>
|
||||||
{item.children ? (
|
{item.children ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<div
|
||||||
onClick={() => toggleExpanded(item.name)}
|
|
||||||
className={cn(
|
className={cn(
|
||||||
isChildActive(item.children)
|
isChildActive(item.children)
|
||||||
? 'bg-surface-secondary text-primary-400'
|
? 'bg-surface-secondary text-primary-400'
|
||||||
: 'text-text-secondary hover:text-primary-400 hover:bg-surface-secondary',
|
: 'text-text-secondary',
|
||||||
'group flex w-full gap-x-2 rounded-r-md px-2 py-1.5 text-sm leading-tight font-medium transition-colors'
|
'group flex w-full gap-x-2 rounded-r-md px-2 py-1.5 text-sm leading-tight font-medium transition-colors'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -132,52 +108,45 @@ function SidebarContent() {
|
||||||
className={cn(
|
className={cn(
|
||||||
isChildActive(item.children)
|
isChildActive(item.children)
|
||||||
? 'text-primary-400'
|
? 'text-primary-400'
|
||||||
: 'text-text-muted group-hover:text-primary-400',
|
: 'text-text-muted',
|
||||||
'h-4 w-4 shrink-0 transition-colors'
|
'h-4 w-4 shrink-0 transition-colors'
|
||||||
)}
|
)}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<span className="flex-1 text-left">{item.name}</span>
|
<span className="flex-1 text-left">{item.name}</span>
|
||||||
{expandedItems.has(item.name) ? (
|
</div>
|
||||||
<ChevronDownIcon className="h-3 w-3 text-text-muted" />
|
<ul className="mt-1 space-y-0.5 pl-5">
|
||||||
) : (
|
{item.children.map(child => (
|
||||||
<ChevronRightIcon className="h-3 w-3 text-text-muted" />
|
<li key={child.name}>
|
||||||
)}
|
<NavLink
|
||||||
</button>
|
to={child.href || ''}
|
||||||
{expandedItems.has(item.name) && (
|
className={({ isActive }) =>
|
||||||
<ul className="mt-1 space-y-0.5 pl-8">
|
cn(
|
||||||
{item.children.map(child => (
|
isActive
|
||||||
<li key={child.name}>
|
? 'bg-surface-secondary text-primary-400 border-l-2 border-primary-500'
|
||||||
<NavLink
|
: 'text-text-secondary hover:text-primary-400 hover:bg-surface-secondary',
|
||||||
to={child.href || ''}
|
'group flex gap-x-2 rounded-r-md px-2 py-1 text-sm leading-tight font-medium transition-colors'
|
||||||
className={({ isActive }) =>
|
)
|
||||||
cn(
|
}
|
||||||
isActive
|
>
|
||||||
? 'bg-surface-secondary text-primary-400 border-l-2 border-primary-500'
|
{({ isActive }) => (
|
||||||
: 'text-text-secondary hover:text-primary-400 hover:bg-surface-secondary',
|
<>
|
||||||
'group flex gap-x-2 rounded-r-md px-2 py-1 text-sm leading-tight font-medium transition-colors'
|
<child.icon
|
||||||
)
|
className={cn(
|
||||||
}
|
isActive
|
||||||
>
|
? 'text-primary-400'
|
||||||
{({ isActive }) => (
|
: 'text-text-muted group-hover:text-primary-400',
|
||||||
<>
|
'h-3 w-3 shrink-0 transition-colors'
|
||||||
<child.icon
|
)}
|
||||||
className={cn(
|
aria-hidden="true"
|
||||||
isActive
|
/>
|
||||||
? 'text-primary-400'
|
{child.name}
|
||||||
: 'text-text-muted group-hover:text-primary-400',
|
</>
|
||||||
'h-3 w-3 shrink-0 transition-colors'
|
)}
|
||||||
)}
|
</NavLink>
|
||||||
aria-hidden="true"
|
</li>
|
||||||
/>
|
))}
|
||||||
{child.name}
|
</ul>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</NavLink>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<NavLink
|
<NavLink
|
||||||
|
|
|
||||||
|
|
@ -42,9 +42,14 @@ function generateMockData(days: number = 365): CandlestickData[] {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChartPage() {
|
interface ChartPageProps {
|
||||||
|
symbol?: string;
|
||||||
|
showHeader?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChartPage({ symbol = 'AAPL', showHeader = true }: ChartPageProps = {}) {
|
||||||
const [config, setConfig] = useState<ChartConfig>({
|
const [config, setConfig] = useState<ChartConfig>({
|
||||||
symbol: 'AAPL',
|
symbol,
|
||||||
interval: '1d',
|
interval: '1d',
|
||||||
chartType: 'candlestick',
|
chartType: 'candlestick',
|
||||||
showVolume: true,
|
showVolume: true,
|
||||||
|
|
@ -201,14 +206,16 @@ export function ChartPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-shrink-0">
|
{showHeader && (
|
||||||
<h1 className="text-lg font-bold text-text-primary mb-2 px-8 pt-4">Market Charts</h1>
|
<div className="flex-shrink-0">
|
||||||
<p className="text-text-secondary mb-4 text-sm px-8">
|
<h1 className="text-lg font-bold text-text-primary mb-2 px-8 pt-4">Market Charts</h1>
|
||||||
Real-time market data and advanced charting powered by TradingView.
|
<p className="text-text-secondary mb-4 text-sm px-8">
|
||||||
</p>
|
Real-time market data and advanced charting powered by TradingView.
|
||||||
</div>
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex-1 flex bg-surface-secondary rounded-lg border border-border mx-8 mb-8 overflow-hidden">
|
<div className={`flex-1 flex bg-surface-secondary ${showHeader ? 'rounded-lg border border-border mx-8 mb-8' : ''} overflow-hidden`}>
|
||||||
{/* Left Sidebar - Indicator List */}
|
{/* Left Sidebar - Indicator List */}
|
||||||
{showSidebar && (
|
{showSidebar && (
|
||||||
<IndicatorList
|
<IndicatorList
|
||||||
|
|
|
||||||
|
|
@ -27,19 +27,8 @@ const chartTypes: { value: ChartType; label: string; icon?: typeof ChartBarIcon
|
||||||
|
|
||||||
export function ChartToolbar({ config, onConfigChange }: ChartToolbarProps) {
|
export function ChartToolbar({ config, onConfigChange }: ChartToolbarProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-4 p-4 bg-surface-secondary border-b border-border">
|
<div className="flex items-center justify-between gap-2 bg-surface-secondary border-b border-border">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-2">
|
||||||
{/* Symbol Input */}
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={config.symbol}
|
|
||||||
onChange={(e) => onConfigChange({ symbol: e.target.value.toUpperCase() })}
|
|
||||||
placeholder="Symbol"
|
|
||||||
className="px-3 py-1.5 bg-background border border-border rounded-md text-sm font-medium text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500 w-24"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Interval Selector */}
|
{/* Interval Selector */}
|
||||||
<div className="flex items-center gap-1 bg-background rounded-md border border-border p-0.5">
|
<div className="flex items-center gap-1 bg-background rounded-md border border-border p-0.5">
|
||||||
{intervals.map(({ value, label }) => (
|
{intervals.map(({ value, label }) => (
|
||||||
|
|
@ -76,19 +65,6 @@ export function ChartToolbar({ config, onConfigChange }: ChartToolbarProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Volume Toggle */}
|
|
||||||
<button
|
|
||||||
onClick={() => onConfigChange({ showVolume: !config.showVolume })}
|
|
||||||
className={`px-3 py-1.5 text-xs font-medium rounded-md border transition-colors ${
|
|
||||||
config.showVolume
|
|
||||||
? 'bg-primary-500 text-white border-primary-500'
|
|
||||||
: 'bg-background text-text-secondary border-border hover:text-text-primary hover:border-primary-500/50'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
Volume
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
{/* Theme Toggle */}
|
{/* Theme Toggle */}
|
||||||
<button
|
<button
|
||||||
onClick={() => onConfigChange({ theme: config.theme === 'dark' ? 'light' : 'dark' })}
|
onClick={() => onConfigChange({ theme: config.theme === 'dark' ? 'light' : 'dark' })}
|
||||||
|
|
|
||||||
221
apps/stock/web-app/src/features/symbols/SymbolDetailPage.tsx
Normal file
221
apps/stock/web-app/src/features/symbols/SymbolDetailPage.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
import { ArrowLeftIcon } from '@heroicons/react/24/outline';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
|
import { ChartPage } from '../charts/ChartPage';
|
||||||
|
import type { Symbol } from './types';
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'chart', name: 'Chart' },
|
||||||
|
{ id: 'financials', name: 'Financials' },
|
||||||
|
{ id: 'filings', name: 'Filings' },
|
||||||
|
{ id: 'insiders', name: 'Insiders' },
|
||||||
|
{ id: 'shorts', name: 'Shorts' },
|
||||||
|
{ id: 'sentiment', name: 'Sentiment' },
|
||||||
|
{ id: 'news', name: 'News' },
|
||||||
|
{ id: 'info', name: 'Info' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock symbol data - in real app would fetch from API
|
||||||
|
const mockSymbolData: Record<string, Symbol> = {
|
||||||
|
'AAPL': { symbol: 'AAPL', name: 'Apple Inc.', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 3.48e12, sector: 'Technology', industry: 'Consumer Electronics' },
|
||||||
|
'SHOP': { symbol: 'SHOP', name: 'Shopify Inc.', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 135e9, sector: 'Technology', industry: 'E-Commerce Software' },
|
||||||
|
'TD': { symbol: 'TD', name: 'Toronto-Dominion Bank', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 150e9, sector: 'Financial', industry: 'Banks' },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SymbolDetailPage() {
|
||||||
|
const { symbol } = useParams<{ symbol: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [activeTab, setActiveTab] = useState('chart');
|
||||||
|
const [symbolData, setSymbolData] = useState<Symbol | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Simulate API call to fetch symbol data
|
||||||
|
setIsLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
const data = mockSymbolData[symbol || ''] || {
|
||||||
|
symbol: symbol || 'UNKNOWN',
|
||||||
|
name: 'Unknown Company',
|
||||||
|
exchange: 'UNKNOWN',
|
||||||
|
country: 'US',
|
||||||
|
type: 'Common Stock',
|
||||||
|
currency: 'USD',
|
||||||
|
};
|
||||||
|
setSymbolData(data);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 500);
|
||||||
|
}, [symbol]);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="h-full flex items-center justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-primary-500"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!symbolData) {
|
||||||
|
return (
|
||||||
|
<div className="h-full flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-text-secondary mb-4">Symbol not found</p>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/symbols')}
|
||||||
|
className="text-primary-500 hover:text-primary-600"
|
||||||
|
>
|
||||||
|
Back to Symbols
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderTabContent = () => {
|
||||||
|
switch (activeTab) {
|
||||||
|
case 'chart':
|
||||||
|
return <ChartPage symbol={symbolData.symbol} showHeader={false} />;
|
||||||
|
case 'financials':
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<h2 className="text-lg font-semibold text-text-primary mb-4">Financial Statements</h2>
|
||||||
|
<p className="text-text-secondary">Financial data coming soon...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'filings':
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<h2 className="text-lg font-semibold text-text-primary mb-4">SEC Filings</h2>
|
||||||
|
<p className="text-text-secondary">Filings data coming soon...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'insiders':
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<h2 className="text-lg font-semibold text-text-primary mb-4">Insider Trading</h2>
|
||||||
|
<p className="text-text-secondary">Insider trading data coming soon...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'shorts':
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<h2 className="text-lg font-semibold text-text-primary mb-4">Short Interest</h2>
|
||||||
|
<p className="text-text-secondary">Short interest data coming soon...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'sentiment':
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<h2 className="text-lg font-semibold text-text-primary mb-4">Market Sentiment</h2>
|
||||||
|
<p className="text-text-secondary">Sentiment analysis coming soon...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'news':
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<h2 className="text-lg font-semibold text-text-primary mb-4">Latest News</h2>
|
||||||
|
<p className="text-text-secondary">News feed coming soon...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case 'info':
|
||||||
|
return (
|
||||||
|
<div className="p-8">
|
||||||
|
<h2 className="text-lg font-semibold text-text-primary mb-4">Company Information</h2>
|
||||||
|
<div className="bg-surface-secondary rounded-lg border border-border p-6">
|
||||||
|
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-text-secondary">Symbol</dt>
|
||||||
|
<dd className="mt-1 text-sm text-text-primary">{symbolData.symbol}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-text-secondary">Name</dt>
|
||||||
|
<dd className="mt-1 text-sm text-text-primary">{symbolData.name}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-text-secondary">Exchange</dt>
|
||||||
|
<dd className="mt-1 text-sm text-text-primary">{symbolData.exchange}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-text-secondary">Country</dt>
|
||||||
|
<dd className="mt-1 text-sm text-text-primary">{symbolData.country}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-text-secondary">Type</dt>
|
||||||
|
<dd className="mt-1 text-sm text-text-primary">{symbolData.type}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-text-secondary">Currency</dt>
|
||||||
|
<dd className="mt-1 text-sm text-text-primary">{symbolData.currency}</dd>
|
||||||
|
</div>
|
||||||
|
{symbolData.sector && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-text-secondary">Sector</dt>
|
||||||
|
<dd className="mt-1 text-sm text-text-primary">{symbolData.sector}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{symbolData.industry && (
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-text-secondary">Industry</dt>
|
||||||
|
<dd className="mt-1 text-sm text-text-primary">{symbolData.industry}</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
{/* Header with Tabs */}
|
||||||
|
<div className="flex-shrink-0 border-b border-border">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/symbols')}
|
||||||
|
className="p-2 hover:bg-surface-tertiary transition-colors"
|
||||||
|
aria-label="Back to symbols"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon className="h-5 w-5 text-text-secondary" />
|
||||||
|
</button>
|
||||||
|
<div className="px-2">
|
||||||
|
<h1 className="text-sm font-bold text-text-primary flex items-center gap-1">
|
||||||
|
{symbolData.symbol}
|
||||||
|
<span className="text-xs font-normal text-text-secondary">
|
||||||
|
{symbolData.exchange}
|
||||||
|
</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-text-secondary">{symbolData.name}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<nav className="flex ml-4" aria-label="Tabs">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<button
|
||||||
|
key={tab.id}
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
className={`
|
||||||
|
whitespace-nowrap py-2 px-3 border-b-2 font-medium text-sm
|
||||||
|
${activeTab === tab.id
|
||||||
|
? 'border-primary-500 text-primary-500'
|
||||||
|
: 'border-transparent text-text-secondary hover:text-text-primary hover:border-border'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Content */}
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
{renderTabContent()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
101
apps/stock/web-app/src/features/symbols/SymbolsPage.tsx
Normal file
101
apps/stock/web-app/src/features/symbols/SymbolsPage.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { SymbolsFilter } from './components/SymbolsFilter';
|
||||||
|
import { SymbolsTable } from './components/SymbolsTable';
|
||||||
|
import type { Exchange, Symbol, SymbolFilter } from './types';
|
||||||
|
|
||||||
|
// Mock data for demonstration
|
||||||
|
const mockExchanges: Exchange[] = [
|
||||||
|
{ code: 'TSX', name: 'Toronto Stock Exchange', country: 'CA' },
|
||||||
|
{ code: 'TSXV', name: 'TSX Venture Exchange', country: 'CA' },
|
||||||
|
{ code: 'NYSE', name: 'New York Stock Exchange', country: 'US' },
|
||||||
|
{ code: 'NASDAQ', name: 'NASDAQ', country: 'US' },
|
||||||
|
{ code: 'AMEX', name: 'NYSE American', country: 'US' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const mockSymbols: Symbol[] = [
|
||||||
|
{ symbol: 'AAPL', name: 'Apple Inc.', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 3.48e12, sector: 'Technology', industry: 'Consumer Electronics' },
|
||||||
|
{ symbol: 'MSFT', name: 'Microsoft Corporation', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 3.18e12, sector: 'Technology', industry: 'Software' },
|
||||||
|
{ symbol: 'GOOGL', name: 'Alphabet Inc. Class A', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 2.23e12, sector: 'Technology', industry: 'Internet Services' },
|
||||||
|
{ symbol: 'AMZN', name: 'Amazon.com Inc.', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 2.16e12, sector: 'Consumer Cyclical', industry: 'E-Commerce' },
|
||||||
|
{ symbol: 'TSLA', name: 'Tesla Inc.', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 1.25e12, sector: 'Consumer Cyclical', industry: 'Auto Manufacturers' },
|
||||||
|
{ symbol: 'META', name: 'Meta Platforms Inc.', exchange: 'NASDAQ', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 1.52e12, sector: 'Technology', industry: 'Social Media' },
|
||||||
|
{ symbol: 'JPM', name: 'JPMorgan Chase & Co.', exchange: 'NYSE', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 650e9, sector: 'Financial', industry: 'Banks' },
|
||||||
|
{ symbol: 'BAC', name: 'Bank of America Corp.', exchange: 'NYSE', country: 'US', type: 'Common Stock', currency: 'USD', marketCap: 370e9, sector: 'Financial', industry: 'Banks' },
|
||||||
|
{ symbol: 'TD', name: 'Toronto-Dominion Bank', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 150e9, sector: 'Financial', industry: 'Banks' },
|
||||||
|
{ symbol: 'RY', name: 'Royal Bank of Canada', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 200e9, sector: 'Financial', industry: 'Banks' },
|
||||||
|
{ symbol: 'SHOP', name: 'Shopify Inc.', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 135e9, sector: 'Technology', industry: 'E-Commerce Software' },
|
||||||
|
{ symbol: 'CP', name: 'Canadian Pacific Kansas City', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 110e9, sector: 'Industrials', industry: 'Railroads' },
|
||||||
|
{ symbol: 'CNR', name: 'Canadian National Railway', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 105e9, sector: 'Industrials', industry: 'Railroads' },
|
||||||
|
{ symbol: 'BMO', name: 'Bank of Montreal', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 125e9, sector: 'Financial', industry: 'Banks' },
|
||||||
|
{ symbol: 'BNS', name: 'Bank of Nova Scotia', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 95e9, sector: 'Financial', industry: 'Banks' },
|
||||||
|
{ symbol: 'ENB', name: 'Enbridge Inc.', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 110e9, sector: 'Energy', industry: 'Oil & Gas Pipelines' },
|
||||||
|
{ symbol: 'SU', name: 'Suncor Energy Inc.', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 65e9, sector: 'Energy', industry: 'Oil & Gas' },
|
||||||
|
{ symbol: 'TRP', name: 'TC Energy Corporation', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 60e9, sector: 'Energy', industry: 'Oil & Gas Pipelines' },
|
||||||
|
{ symbol: 'MFC', name: 'Manulife Financial Corp.', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 55e9, sector: 'Financial', industry: 'Insurance' },
|
||||||
|
{ symbol: 'BAM', name: 'Brookfield Asset Management', exchange: 'TSX', country: 'CA', type: 'Common Stock', currency: 'CAD', marketCap: 85e9, sector: 'Financial', industry: 'Asset Management' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SymbolsPage() {
|
||||||
|
const [filters, setFilters] = useState<SymbolFilter>({
|
||||||
|
country: ['CA', 'US'],
|
||||||
|
});
|
||||||
|
const [filteredSymbols, setFilteredSymbols] = useState<Symbol[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Simulate API call
|
||||||
|
setIsLoading(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
let symbols = [...mockSymbols];
|
||||||
|
|
||||||
|
// Apply filters
|
||||||
|
if (filters.exchange) {
|
||||||
|
symbols = symbols.filter(s => s.exchange === filters.exchange);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.searchText) {
|
||||||
|
const search = filters.searchText.toLowerCase();
|
||||||
|
symbols = symbols.filter(s =>
|
||||||
|
s.symbol.toLowerCase().includes(search) ||
|
||||||
|
s.name.toLowerCase().includes(search)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.country && filters.country.length > 0) {
|
||||||
|
symbols = symbols.filter(s => filters.country!.includes(s.country));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort by market cap (descending)
|
||||||
|
symbols.sort((a, b) => (b.marketCap || 0) - (a.marketCap || 0));
|
||||||
|
|
||||||
|
setFilteredSymbols(symbols);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 500);
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-col">
|
||||||
|
<div className="flex-shrink-0 border-b border-border">
|
||||||
|
<div className="flex items-center gap-4 px-3 py-2">
|
||||||
|
<h1 className="text-lg font-bold text-text-primary">Symbols</h1>
|
||||||
|
<SymbolsFilter
|
||||||
|
filters={filters}
|
||||||
|
exchanges={mockExchanges}
|
||||||
|
onFiltersChange={setFilters}
|
||||||
|
/>
|
||||||
|
<div className="text-sm text-text-secondary ml-auto">
|
||||||
|
{isLoading ? (
|
||||||
|
'Loading...'
|
||||||
|
) : (
|
||||||
|
`${filteredSymbols.length} found`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 w-full">
|
||||||
|
<SymbolsTable symbols={filteredSymbols} isLoading={isLoading} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { Fragment, useState } from 'react';
|
||||||
|
import { Listbox, Transition } from '@headlessui/react';
|
||||||
|
import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/24/outline';
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MultiSelectProps {
|
||||||
|
options: Option[];
|
||||||
|
selected: string[];
|
||||||
|
onChange: (selected: string[]) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MultiSelect({ options, selected, onChange, placeholder = 'Select...', className = '' }: MultiSelectProps) {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleToggle = (value: string) => {
|
||||||
|
if (selected.includes(value)) {
|
||||||
|
onChange(selected.filter(v => v !== value));
|
||||||
|
} else {
|
||||||
|
onChange([...selected, value]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedLabels = selected.map(value =>
|
||||||
|
options.find(opt => opt.value === value)?.label || value
|
||||||
|
).join(', ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`relative ${className}`}>
|
||||||
|
<Listbox value={selected} onChange={() => {}} multiple>
|
||||||
|
<div className="relative">
|
||||||
|
<Listbox.Button
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
className="relative w-full cursor-default rounded-md bg-background border border-border py-1 pl-2 pr-8 text-left text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
>
|
||||||
|
<span className={`block truncate ${selected.length === 0 ? 'text-text-secondary' : ''}`}>
|
||||||
|
{selected.length > 0 ? selectedLabels : placeholder}
|
||||||
|
</span>
|
||||||
|
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||||
|
<ChevronUpDownIcon className="h-4 w-4 text-text-secondary" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</Listbox.Button>
|
||||||
|
<Transition
|
||||||
|
show={isOpen}
|
||||||
|
as={Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Listbox.Options
|
||||||
|
static
|
||||||
|
className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-background border border-border py-1 text-sm shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<Listbox.Option
|
||||||
|
key={option.value}
|
||||||
|
value={option.value}
|
||||||
|
disabled={false}
|
||||||
|
className={({ active }) =>
|
||||||
|
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
|
||||||
|
active ? 'bg-surface-secondary text-text-primary' : 'text-text-primary'
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleToggle(option.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<span className={`block truncate ${selected.includes(option.value) ? 'font-medium' : 'font-normal'}`}>
|
||||||
|
{option.label}
|
||||||
|
</span>
|
||||||
|
{selected.includes(option.value) ? (
|
||||||
|
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-500">
|
||||||
|
<CheckIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
</Listbox.Option>
|
||||||
|
))}
|
||||||
|
</Listbox.Options>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</Listbox>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||||
|
import type { Exchange, SymbolFilter } from '../types';
|
||||||
|
import { MultiSelect } from './MultiSelect';
|
||||||
|
|
||||||
|
interface SymbolsFilterProps {
|
||||||
|
filters: SymbolFilter;
|
||||||
|
exchanges: Exchange[];
|
||||||
|
onFiltersChange: (filters: SymbolFilter) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SymbolsFilter({ filters, exchanges, onFiltersChange }: SymbolsFilterProps) {
|
||||||
|
const handleExchangeChange = (exchange: string) => {
|
||||||
|
onFiltersChange({
|
||||||
|
...filters,
|
||||||
|
exchange: exchange === 'all' ? undefined : exchange,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = (searchText: string) => {
|
||||||
|
onFiltersChange({
|
||||||
|
...filters,
|
||||||
|
searchText: searchText.trim() || undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCountryChange = (countries: string[]) => {
|
||||||
|
onFiltersChange({
|
||||||
|
...filters,
|
||||||
|
country: countries.length > 0 ? countries : undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const countryOptions = [
|
||||||
|
{ value: 'CA', label: 'Canada' },
|
||||||
|
{ value: 'US', label: 'United States' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 flex-1">
|
||||||
|
{/* Exchange Dropdown */}
|
||||||
|
<select
|
||||||
|
value={filters.exchange || 'all'}
|
||||||
|
onChange={(e) => handleExchangeChange(e.target.value)}
|
||||||
|
className="rounded-md bg-background border border-border px-2 py-1 text-sm text-text-primary focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
>
|
||||||
|
<option value="all">All Exchanges</option>
|
||||||
|
{exchanges.map((exchange) => (
|
||||||
|
<option key={exchange.code} value={exchange.code}>
|
||||||
|
{exchange.code}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{/* Symbol Search */}
|
||||||
|
<div className="relative flex-1 max-w-xs">
|
||||||
|
<div className="absolute inset-y-0 left-0 pl-2 flex items-center pointer-events-none">
|
||||||
|
<MagnifyingGlassIcon className="h-3.5 w-3.5 text-text-secondary" />
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
value={filters.searchText || ''}
|
||||||
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
|
className="w-full pl-8 pr-2 py-1 rounded-md bg-background border border-border text-sm text-text-primary placeholder-text-secondary focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Country Filter */}
|
||||||
|
<div className="w-40">
|
||||||
|
<MultiSelect
|
||||||
|
options={countryOptions}
|
||||||
|
selected={filters.country || ['CA', 'US']}
|
||||||
|
onChange={handleCountryChange}
|
||||||
|
placeholder="Countries"
|
||||||
|
className="text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import type { Symbol } from '../types';
|
||||||
|
import { DataTable } from '@/components/ui/DataTable';
|
||||||
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
interface SymbolsTableProps {
|
||||||
|
symbols: Symbol[];
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SymbolsTable({ symbols, isLoading }: SymbolsTableProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const formatMarketCap = (value?: number) => {
|
||||||
|
if (!value) return '-';
|
||||||
|
if (value >= 1e12) return `$${(value / 1e12).toFixed(2)}T`;
|
||||||
|
if (value >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;
|
||||||
|
if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
|
||||||
|
return `$${value.toLocaleString()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnDef<Symbol>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'symbol',
|
||||||
|
header: 'Symbol',
|
||||||
|
size: 120,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium text-text-primary">{row.original.symbol}</div>
|
||||||
|
<div className="text-xs text-text-secondary">{row.original.currency}</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
size: 250,
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<div className="text-sm text-text-primary">{getValue() as string}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'exchange',
|
||||||
|
header: 'Exchange',
|
||||||
|
size: 150,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-text-primary">{row.original.exchange}</div>
|
||||||
|
<div className="text-xs text-text-secondary">{row.original.country}</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'type',
|
||||||
|
header: 'Type',
|
||||||
|
size: 120,
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<span className="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-primary-100 text-primary-800">
|
||||||
|
{getValue() as string}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'marketCap',
|
||||||
|
header: 'Market Cap',
|
||||||
|
size: 120,
|
||||||
|
cell: ({ getValue }) => (
|
||||||
|
<span className="text-sm text-text-primary">{formatMarketCap(getValue() as number)}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'sector',
|
||||||
|
header: 'Sector',
|
||||||
|
size: 200,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div>
|
||||||
|
<div className="text-sm text-text-primary">{row.original.sector || '-'}</div>
|
||||||
|
<div className="text-xs text-text-secondary">{row.original.industry || '-'}</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
if (symbols.length === 0 && !isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="bg-surface-secondary border-t border-border p-8">
|
||||||
|
<div className="text-center text-text-secondary">
|
||||||
|
No symbols found matching your criteria
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable
|
||||||
|
data={symbols}
|
||||||
|
columns={columns}
|
||||||
|
loading={isLoading}
|
||||||
|
onRowClick={(symbol) => navigate(`/symbols/${symbol.symbol}`)}
|
||||||
|
className="border-0 rounded-none border-t h-full"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { SymbolsFilter } from './SymbolsFilter';
|
||||||
|
export { SymbolsTable } from './SymbolsTable';
|
||||||
|
export { MultiSelect } from './MultiSelect';
|
||||||
3
apps/stock/web-app/src/features/symbols/index.ts
Normal file
3
apps/stock/web-app/src/features/symbols/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export { SymbolsPage } from './SymbolsPage';
|
||||||
|
export { SymbolDetailPage } from './SymbolDetailPage';
|
||||||
|
export * from './types';
|
||||||
23
apps/stock/web-app/src/features/symbols/types.ts
Normal file
23
apps/stock/web-app/src/features/symbols/types.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
export interface Symbol {
|
||||||
|
symbol: string;
|
||||||
|
name: string;
|
||||||
|
exchange: string;
|
||||||
|
country: string;
|
||||||
|
type: string;
|
||||||
|
currency: string;
|
||||||
|
marketCap?: number;
|
||||||
|
sector?: string;
|
||||||
|
industry?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SymbolFilter {
|
||||||
|
exchange?: string;
|
||||||
|
searchText?: string;
|
||||||
|
country?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Exchange {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
country: string;
|
||||||
|
}
|
||||||
|
|
@ -25,8 +25,8 @@ export const navigation: NavigationItem[] = [
|
||||||
name: 'Market Data',
|
name: 'Market Data',
|
||||||
icon: CurrencyDollarIcon,
|
icon: CurrencyDollarIcon,
|
||||||
children: [
|
children: [
|
||||||
{ name: 'Charts', href: '/charts', icon: ChartBarIcon },
|
|
||||||
{ name: 'Exchanges', href: '/exchanges', icon: BuildingLibraryIcon },
|
{ name: 'Exchanges', href: '/exchanges', icon: BuildingLibraryIcon },
|
||||||
|
{ name: 'Symbols', href: '/symbols', icon: ChartBarIcon },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ name: 'Portfolio', href: '/portfolio', icon: ChartBarIcon },
|
{ name: 'Portfolio', href: '/portfolio', icon: ChartBarIcon },
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue