removed angular app and switched to react

This commit is contained in:
Boki 2025-06-15 20:17:02 -04:00
parent 56e3938561
commit 3a9d45c543
101 changed files with 2697 additions and 4075 deletions

232
apps/web/src/App.tsx Normal file
View file

@ -0,0 +1,232 @@
import { useState } from 'react';
import { Dialog } from '@headlessui/react';
import {
Bars3Icon,
XMarkIcon,
HomeIcon,
ChartBarIcon,
CogIcon,
DocumentTextIcon,
CurrencyDollarIcon,
} from '@heroicons/react/24/outline';
const navigation = [
{ name: 'Dashboard', href: '#', icon: HomeIcon, current: true },
{ name: 'Portfolio', href: '#', icon: CurrencyDollarIcon, current: false },
{ name: 'Analytics', href: '#', icon: ChartBarIcon, current: false },
{ name: 'Reports', href: '#', icon: DocumentTextIcon, current: false },
{ name: 'Settings', href: '#', icon: CogIcon, current: false },
];
function classNames(...classes: string[]) {
return classes.filter(Boolean).join(' ');
}
export default function App() {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className="min-h-screen bg-background">
<div>
<Dialog
as="div"
className="relative z-50 lg:hidden"
open={sidebarOpen}
onClose={setSidebarOpen}
>
<div className="fixed inset-0 bg-black/80" />
<div className="fixed inset-0 flex">
<Dialog.Panel className="relative mr-16 flex w-full max-w-xs flex-1">
<div className="absolute left-full top-0 flex w-16 justify-center pt-5">
<button
type="button"
className="-m-2.5 p-2.5"
onClick={() => setSidebarOpen(false)}
>
<span className="sr-only">Close sidebar</span>
<XMarkIcon className="h-6 w-6 text-text-primary" aria-hidden="true" />
</button>
</div>
<div className="flex grow flex-col gap-y-3 overflow-y-auto bg-background px-3 pb-2 scrollbar-thin">
<div className="flex h-12 shrink-0 items-center border-b border-border">
<h1 className="text-lg font-bold text-text-primary">Stock Bot</h1>
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-4">
<li>
<ul role="list" className="-mx-1 space-y-0.5">
{navigation.map(item => (
<li key={item.name}>
<a
href={item.href}
className={classNames(
item.current
? 'bg-surface-secondary text-primary-400 border-l-2 border-primary-500'
: 'text-text-secondary hover:text-primary-400 hover:bg-surface-secondary',
'group flex gap-x-2 rounded-r-md px-2 py-1.5 text-sm leading-tight font-medium transition-colors'
)}
>
<item.icon
className={classNames(
item.current
? 'text-primary-400'
: 'text-text-muted group-hover:text-primary-400',
'h-4 w-4 shrink-0 transition-colors'
)}
aria-hidden="true"
/>
{item.name}
</a>
</li>
))}
</ul>
</li>
</ul>
</nav>
</div>
</Dialog.Panel>
</div>
</Dialog>
{/* Static sidebar for desktop */}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-60 lg:flex-col">
<div className="flex grow flex-col gap-y-3 overflow-y-auto border-r border-border bg-background px-3 scrollbar-thin">
<div className="flex h-12 shrink-0 items-center border-b border-border">
<h1 className="text-lg font-bold text-text-primary">Stock Bot</h1>
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-4">
<li>
<ul role="list" className="-mx-1 space-y-0.5">
{navigation.map(item => (
<li key={item.name}>
<a
href={item.href}
className={classNames(
item.current
? 'bg-surface-secondary text-primary-400 border-l-2 border-primary-500'
: 'text-text-secondary hover:text-primary-400 hover:bg-surface-secondary',
'group flex gap-x-2 rounded-r-md px-2 py-1.5 text-sm leading-tight font-medium transition-colors'
)}
>
<item.icon
className={classNames(
item.current
? 'text-primary-400'
: 'text-text-muted group-hover:text-primary-400',
'h-4 w-4 shrink-0 transition-colors'
)}
aria-hidden="true"
/>
{item.name}
</a>
</li>
))}
</ul>
</li>
</ul>
</nav>
</div>
</div>
<div className="sticky top-0 z-40 flex items-center gap-x-4 bg-background px-3 py-2 shadow-sm sm:px-4 lg:hidden border-b border-border">
<button
type="button"
className="-m-1.5 p-1.5 text-text-secondary lg:hidden hover:text-text-primary transition-colors"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
<Bars3Icon className="h-5 w-5" aria-hidden="true" />
</button>
<div className="flex-1 text-sm font-medium leading-tight text-text-primary">
Dashboard
</div>
</div>
<main className="py-4 lg:pl-60 w-full">
<div className="px-4">
<h1 className="text-lg font-bold text-text-primary mb-2">
Welcome to Stock Bot Dashboard
</h1>
<p className="text-text-secondary mb-6 text-sm">
Monitor your trading performance, manage portfolios, and analyze market data.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 mb-6">
<div className="bg-surface-secondary p-4 rounded-lg border border-border hover:border-primary-500/50 transition-colors">
<div className="flex items-center">
<div className="p-1.5 bg-primary-500/10 rounded">
<CurrencyDollarIcon className="h-5 w-5 text-primary-400" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-text-primary">Portfolio Value</h3>
<p className="text-lg font-bold text-primary-400">$0.00</p>
<p className="text-xs text-text-muted">Total assets</p>
</div>
</div>
</div>
<div className="bg-surface-secondary p-4 rounded-lg border border-border hover:border-success/50 transition-colors">
<div className="flex items-center">
<div className="p-1.5 bg-success/10 rounded">
<ChartBarIcon className="h-5 w-5 text-success" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-text-primary">Total Return</h3>
<p className="text-lg font-bold text-success">+0.00%</p>
<p className="text-xs text-text-muted">Since inception</p>
</div>
</div>
</div>
<div className="bg-surface-secondary p-4 rounded-lg border border-border hover:border-warning/50 transition-colors">
<div className="flex items-center">
<div className="p-1.5 bg-warning/10 rounded">
<DocumentTextIcon className="h-5 w-5 text-warning" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-text-primary">
Active Strategies
</h3>
<p className="text-lg font-bold text-warning">0</p>
<p className="text-xs text-text-muted">Running algorithms</p>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
<h3 className="text-base font-medium text-text-primary mb-3">
Recent Activity
</h3>
<div className="space-y-2">
<div className="flex items-center justify-between p-2 bg-surface-tertiary rounded border border-border">
<span className="text-text-secondary text-sm">No recent activity</span>
<span className="text-text-muted text-xs">--</span>
</div>
</div>
</div>
<div className="bg-surface-secondary p-4 rounded-lg border border-border">
<h3 className="text-base font-medium text-text-primary mb-3">
Market Overview
</h3>
<div className="space-y-2">
<div className="flex items-center justify-between p-2 bg-surface-tertiary rounded border border-border">
<span className="text-text-secondary text-sm">
Market data loading...
</span>
<span className="text-text-muted text-xs">--</span>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</div>
);
}

10
apps/web/src/app/App.tsx Normal file
View file

@ -0,0 +1,10 @@
import { Layout } from '@/components/layout';
import { DashboardPage } from '@/features/dashboard';
export function App() {
return (
<Layout title="Dashboard">
<DashboardPage />
</Layout>
);
}

View file

@ -0,0 +1 @@
export { App } from './App';

View file

@ -0,0 +1,2 @@
export * from './ui';
export * from './layout';

View file

@ -0,0 +1,22 @@
import { Bars3Icon } from '@heroicons/react/24/outline';
interface HeaderProps {
setSidebarOpen: (open: boolean) => void;
title?: string;
}
export function Header({ setSidebarOpen, title = 'Dashboard' }: HeaderProps) {
return (
<div className="sticky top-0 z-40 flex items-center gap-x-4 bg-background px-3 py-2 shadow-sm sm:px-4 lg:hidden border-b border-border">
<button
type="button"
className="-m-1.5 p-1.5 text-text-secondary lg:hidden hover:text-text-primary transition-colors"
onClick={() => setSidebarOpen(true)}
>
<span className="sr-only">Open sidebar</span>
<Bars3Icon className="h-5 w-5" aria-hidden="true" />
</button>
<div className="flex-1 text-sm font-medium leading-tight text-text-primary">{title}</div>
</div>
);
}

View file

@ -0,0 +1,23 @@
import { useState, ReactNode } from 'react';
import { Sidebar } from './Sidebar';
import { Header } from './Header';
interface LayoutProps {
children: ReactNode;
title?: string;
}
export function Layout({ children, title }: LayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className="h-full">
<Sidebar sidebarOpen={sidebarOpen} setSidebarOpen={setSidebarOpen} />
<Header setSidebarOpen={setSidebarOpen} title={title} />
<main className="py-4 lg:pl-60 w-full">
<div className="px-4">{children}</div>
</main>
</div>
);
}

View file

@ -0,0 +1,117 @@
import { Fragment } from 'react';
import { Dialog, Transition } from '@headlessui/react';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { navigation } from '@/lib/constants';
import { cn } from '@/lib/utils';
interface SidebarProps {
sidebarOpen: boolean;
setSidebarOpen: (open: boolean) => void;
}
export function Sidebar({ sidebarOpen, setSidebarOpen }: SidebarProps) {
return (
<>
{/* Mobile sidebar */}
<Transition.Root show={sidebarOpen} as={Fragment}>
<Dialog as="div" className="relative z-50 lg:hidden" onClose={setSidebarOpen}>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black/80" />
</Transition.Child>
<div className="fixed inset-0 flex">
<Transition.Child
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="-translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="-translate-x-full"
>
<Dialog.Panel className="relative mr-16 flex w-full max-w-xs flex-1">
<Transition.Child
as={Fragment}
enter="ease-in-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in-out duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="absolute left-full top-0 flex w-16 justify-center pt-5">
<button
type="button"
className="-m-2.5 p-2.5"
onClick={() => setSidebarOpen(false)}
>
<span className="sr-only">Close sidebar</span>
<XMarkIcon className="h-6 w-6 text-text-primary" aria-hidden="true" />
</button>
</div>
</Transition.Child>
<SidebarContent />
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
{/* Static sidebar for desktop */}
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-60 lg:flex-col">
<SidebarContent />
</div>
</>
);
}
function SidebarContent() {
return (
<div className="flex grow flex-col gap-y-3 overflow-y-auto border-r border-border bg-background px-3 scrollbar-thin">
<div className="flex h-12 shrink-0 items-center border-b border-border">
<h1 className="text-lg font-bold text-text-primary">Stock Bot</h1>
</div>
<nav className="flex flex-1 flex-col">
<ul role="list" className="flex flex-1 flex-col gap-y-4">
<li>
<ul role="list" className="-mx-1 space-y-0.5">
{navigation.map(item => (
<li key={item.name}>
<a
href={item.href}
className={cn(
item.current
? 'bg-surface-secondary text-primary-400 border-l-2 border-primary-500'
: 'text-text-secondary hover:text-primary-400 hover:bg-surface-secondary',
'group flex gap-x-2 rounded-r-md px-2 py-1.5 text-sm leading-tight font-medium transition-colors'
)}
>
<item.icon
className={cn(
item.current
? 'text-primary-400'
: 'text-text-muted group-hover:text-primary-400',
'h-4 w-4 shrink-0 transition-colors'
)}
aria-hidden="true"
/>
{item.name}
</a>
</li>
))}
</ul>
</li>
</ul>
</nav>
</div>
);
}

View file

@ -0,0 +1,3 @@
export { Layout } from './Layout';
export { Sidebar } from './Sidebar';
export { Header } from './Header';

View file

@ -0,0 +1,40 @@
import { ReactNode } from 'react';
import { cn } from '@/lib/utils';
interface CardProps {
children: ReactNode;
className?: string;
hover?: boolean;
}
export function Card({ children, className, hover = false }: CardProps) {
return (
<div
className={cn(
'bg-surface-secondary rounded-lg border border-border p-4',
hover && 'hover:border-primary-500/50 transition-colors',
className
)}
>
{children}
</div>
);
}
interface CardHeaderProps {
children: ReactNode;
className?: string;
}
export function CardHeader({ children, className }: CardHeaderProps) {
return <div className={cn('flex items-center mb-3', className)}>{children}</div>;
}
interface CardContentProps {
children: ReactNode;
className?: string;
}
export function CardContent({ children, className }: CardContentProps) {
return <div className={cn('space-y-2', className)}>{children}</div>;
}

View file

@ -0,0 +1,191 @@
import { cn } from '@/lib/utils';
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline';
import {
ColumnDef,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from '@tanstack/react-table';
import React, { useCallback, useMemo, useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { VariableSizeGrid as Grid } from 'react-window';
interface DataTableProps<T> {
data: T[];
columns: ColumnDef<T>[];
rowHeight?: number;
headerHeight?: number;
loading?: boolean;
onRowClick?: (row: T) => void;
className?: string;
}
export function DataTable<T>({
data,
columns,
rowHeight = 35,
headerHeight = 40,
loading = false,
onRowClick,
className = '',
}: DataTableProps<T>) {
const [sorting, setSorting] = useState<SortingState>([]);
const [globalFilter, setGlobalFilter] = useState('');
const table = useReactTable({
data,
columns,
state: {
sorting,
globalFilter,
},
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
enableSorting: true,
enableGlobalFilter: true,
});
const { rows } = table.getRowModel();
const visibleColumns = table.getVisibleFlatColumns();
// Calculate column widths as numbers for react-window
const columnWidths = useMemo(() => {
return visibleColumns.map(column => column.getSize());
}, [visibleColumns]);
// Unified cell renderer that handles both header and data rows
const Cell = useCallback(
({
columnIndex,
rowIndex,
style,
}: {
columnIndex: number;
rowIndex: number;
style: React.CSSProperties;
}) => {
const column = visibleColumns[columnIndex];
// Header row (rowIndex 0) - make it sticky
if (rowIndex === 0) {
return (
<div
style={{
...style,
width: columnWidths[columnIndex],
position: 'sticky',
top: 0,
zIndex: 10,
}}
className={cn(
'flex items-center justify-between px-3 py-2 text-xs font-medium text-text-secondary uppercase tracking-wider',
'border-r border-b border-border bg-surface hover:bg-surface-secondary',
column.getCanSort() && 'cursor-pointer select-none'
)}
onClick={column.getToggleSortingHandler()}
>
<span className="truncate">
{typeof column.columnDef.header === 'string' ? column.columnDef.header : column.id}
</span>
{column.getCanSort() && (
<div className="ml-2 flex-shrink-0">
{column.getIsSorted() === 'asc' ? (
<ChevronUpIcon className="h-4 w-4 text-primary-400" />
) : column.getIsSorted() === 'desc' ? (
<ChevronDownIcon className="h-4 w-4 text-primary-400" />
) : (
<div className="h-4 w-4" />
)}
</div>
)}
</div>
);
}
// Data rows (rowIndex > 0)
const row = rows[rowIndex - 1]; // Subtract 1 because row 0 is header
const cell = row?.getVisibleCells()[columnIndex];
if (!cell || !column) {
return <div style={style} className="border-r border-border bg-background" />;
}
return (
<div
style={{
...style,
width: columnWidths[columnIndex],
}}
className={cn(
'flex items-center px-3 py-2 text-sm text-text-primary border-r border-border bg-background',
'hover:bg-surface cursor-pointer'
)}
onClick={() => onRowClick?.(row.original)}
>
<div className="truncate w-full">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
</div>
);
},
[rows, visibleColumns, onRowClick, columnWidths]
);
if (loading) {
return (
<div className="flex items-center justify-center h-64 bg-background border border-border rounded-lg">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-500"></div>
</div>
);
}
return (
<div className={cn('bg-background text-text-primary w-full h-full flex flex-col', className)}>
{/* Search */}
<div className="p-4 border-b border-border flex-shrink-0">
<input
type="text"
value={globalFilter}
onChange={e => setGlobalFilter(e.target.value)}
placeholder="Search..."
className="w-full max-w-md bg-surface border border-border rounded px-3 py-2 text-sm text-text-primary placeholder-text-muted focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
/>
</div>
{/* Virtual Grid with AutoSizer - includes sticky header */}
<div className="flex-1 border border-border rounded-lg overflow-hidden">
<AutoSizer>
{({ height: autoHeight, width: autoWidth }) => (
<Grid
height={autoHeight}
width={autoWidth}
rowCount={rows.length + 1} // +1 for header row
columnCount={visibleColumns.length}
rowHeight={index => (index === 0 ? headerHeight : rowHeight)} // Header height for row 0
columnWidth={index => columnWidths[index] || 150}
overscanRowCount={5}
overscanColumnCount={2}
>
{Cell}
</Grid>
)}
</AutoSizer>
</div>
{/* Footer */}
<div className="flex items-center justify-between mt-2 px-2 text-sm text-text-secondary flex-shrink-0">
<span>
Showing {rows.length} of {data.length} rows
</span>
{globalFilter && <span>Filtered by: "{globalFilter}"</span>}
</div>
</div>
);
}

View file

@ -0,0 +1,89 @@
import React from 'react';
import { ColumnDef } from '@tanstack/react-table';
import { DataTable } from '@/components/ui';
interface SampleData {
id: number;
name: string;
email: string;
status: 'active' | 'inactive';
value: number;
}
export function ExampleTable() {
// Sample data
const data: SampleData[] = [
{ id: 1, name: 'John Doe', email: 'john@example.com', status: 'active', value: 100 },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com', status: 'inactive', value: 250 },
{ id: 3, name: 'Bob Johnson', email: 'bob@example.com', status: 'active', value: 150 },
// Add more sample data...
...Array.from({ length: 100 }, (_, i) => ({
id: i + 4,
name: `User ${i + 4}`,
email: `user${i + 4}@example.com`,
status: Math.random() > 0.5 ? 'active' : ('inactive' as const),
value: Math.floor(Math.random() * 1000),
})),
];
// Define columns
const columns: ColumnDef<SampleData>[] = [
{
id: 'id',
header: 'ID',
accessorKey: 'id',
size: 80,
},
{
id: 'name',
header: 'Name',
accessorKey: 'name',
size: 200,
},
{
id: 'email',
header: 'Email',
accessorKey: 'email',
size: 250,
},
{
id: 'status',
header: 'Status',
accessorKey: 'status',
size: 120,
cell: ({ getValue }) => {
const status = getValue() as string;
return (
<span
className={`px-2 py-1 rounded text-xs font-medium ${
status === 'active' ? 'bg-success/20 text-success' : 'bg-danger/20 text-danger'
}`}
>
{status}
</span>
);
},
},
{
id: 'value',
header: 'Value',
accessorKey: 'value',
size: 120,
cell: ({ getValue }) => (
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
),
},
];
return (
<div className="p-4">
<h2 className="text-xl font-bold text-text-primary mb-4">Example Data Table</h2>
<DataTable
data={data}
columns={columns}
height={500}
onRowClick={row => console.log('Clicked row:', row)}
/>
</div>
);
}

View file

@ -0,0 +1,145 @@
import React, { useState } from 'react';
import { Table } from '@tanstack/react-table';
import { MagnifyingGlassIcon, FunnelIcon, Squares2X2Icon } from '@heroicons/react/24/outline';
import { TableColumn } from './types';
import { cn } from '@/lib/utils';
interface TableControlsProps<T> {
table: Table<T>;
globalFilter: string;
setGlobalFilter: (value: string) => void;
columns: TableColumn<T>[];
onGroupingChange: (grouping: string[]) => void;
}
export function TableControls<T>({
table,
globalFilter,
setGlobalFilter,
columns,
onGroupingChange,
}: TableControlsProps<T>) {
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false);
const [selectedGroupColumn, setSelectedGroupColumn] = useState('');
const groupableColumns = columns.filter(col => col.groupable);
const currentGrouping = table.getState().grouping;
const handleGroupingChange = (columnId: string) => {
if (columnId === '') {
onGroupingChange([]);
} else {
onGroupingChange([columnId]);
}
setSelectedGroupColumn(columnId);
};
return (
<div className="bg-background border-b border-border p-4 space-y-4">
{/* Main Controls Row */}
<div className="flex items-center justify-between gap-4">
{/* Global Search */}
<div className="flex items-center space-x-2 flex-1 max-w-md">
<MagnifyingGlassIcon className="h-4 w-4 text-text-muted" />
<input
type="text"
value={globalFilter}
onChange={e => setGlobalFilter(e.target.value)}
placeholder="Search all columns..."
className="w-full bg-surface border border-border rounded px-3 py-2 text-sm text-text-primary placeholder-text-muted focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
/>
</div>
{/* Action Buttons */}
<div className="flex items-center space-x-2">
{/* Grouping */}
{groupableColumns.length > 0 && (
<div className="flex items-center space-x-2">
<Squares2X2Icon className="h-4 w-4 text-text-muted" />
<select
value={selectedGroupColumn}
onChange={e => handleGroupingChange(e.target.value)}
className="bg-surface border border-border rounded px-2 py-1 text-sm text-text-primary"
>
<option value="">No grouping</option>
{groupableColumns.map(column => (
<option key={column.id} value={column.id}>
Group by {column.header}
</option>
))}
</select>
</div>
)}
{/* Advanced Filters Toggle */}
<button
onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}
className={cn(
'flex items-center space-x-1 px-3 py-2 rounded text-sm font-medium transition-colors',
showAdvancedFilters
? 'bg-primary-500 text-white'
: 'bg-surface border border-border text-text-secondary hover:bg-surface-secondary'
)}
>
<FunnelIcon className="h-4 w-4" />
<span>Filters</span>
</button>
{/* Reset Button */}
<button
onClick={() => {
setGlobalFilter('');
table.resetSorting();
table.resetColumnFilters();
table.resetGrouping();
setSelectedGroupColumn('');
}}
className="px-3 py-2 bg-surface border border-border rounded text-sm text-text-secondary hover:bg-surface-secondary transition-colors"
>
Reset
</button>
</div>
</div>
{/* Advanced Filters Panel */}
{showAdvancedFilters && (
<div className="bg-surface rounded-lg border border-border p-4">
<h4 className="text-sm font-medium text-text-primary mb-3">Column Filters</h4>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{table
.getAllColumns()
.filter(column => column.getCanFilter())
.map(column => (
<div key={column.id} className="space-y-1">
<label className="text-xs text-text-secondary">
{typeof column.columnDef.header === 'string'
? column.columnDef.header
: column.id}
</label>
<input
type="text"
value={(column.getFilterValue() as string) || ''}
onChange={e => column.setFilterValue(e.target.value)}
placeholder={`Filter ${column.id}...`}
className="w-full bg-background border border-border rounded px-2 py-1 text-xs text-text-primary placeholder-text-muted focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
/>
</div>
))}
</div>
</div>
)}
{/* Status Info */}
<div className="flex items-center justify-between text-xs text-text-muted">
<div className="flex items-center space-x-4">
{globalFilter && <span>Global filter: "{globalFilter}"</span>}
{currentGrouping.length > 0 && <span>Grouped by: {currentGrouping.join(', ')}</span>}
{table.getState().columnFilters.length > 0 && (
<span>{table.getState().columnFilters.length} column filters active</span>
)}
</div>
<span>{table.getPreFilteredRowModel().rows.length} total rows</span>
</div>
</div>
);
}

View file

@ -0,0 +1,83 @@
import { cn } from '@/lib/utils';
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline';
import { Table, flexRender } from '@tanstack/react-table';
interface TableHeaderProps<T> {
table: Table<T>;
height: number;
totalWidth: number;
}
export function TableHeader<T>({ table, height, totalWidth }: TableHeaderProps<T>) {
return (
<div
className="bg-surface border-b border-border overflow-hidden"
style={{ height, minWidth: totalWidth }}
>
<div className="flex h-full">
{table.getVisibleFlatColumns().map(column => (
<div
key={column.id}
className={cn(
'flex items-center px-2 text-xs font-medium text-text-secondary uppercase tracking-wider',
'border-r border-border bg-surface hover:bg-surface-secondary',
column.getCanSort() && 'cursor-pointer select-none'
)}
style={{ width: column.getSize() }}
onClick={column.getToggleSortingHandler()}
>
<div className="flex items-center justify-between w-full">
<span className="truncate">
{flexRender(column.columnDef.header, column.getContext())}
</span>
{column.getCanSort() && (
<div className="flex flex-col ml-1">
{column.getIsSorted() === 'asc' ? (
<ChevronUpIcon className="h-3 w-3 text-primary-400" />
) : column.getIsSorted() === 'desc' ? (
<ChevronDownIcon className="h-3 w-3 text-primary-400" />
) : (
<div className="flex flex-col">
<ChevronUpIcon className="h-2 w-2 text-text-muted" />
<ChevronDownIcon className="h-2 w-2 text-text-muted" />
</div>
)}
</div>
)}
{column.getCanGroup() && (
<button
onClick={e => {
e.stopPropagation();
column.toggleGrouping();
}}
className={cn(
'ml-1 px-1 py-0.5 text-xs rounded',
column.getIsGrouped()
? 'bg-primary-500 text-white'
: 'bg-surface-tertiary text-text-muted hover:bg-surface-secondary'
)}
>
G
</button>
)}
{column.getCanResize() && (
<div
onMouseDown={column.getResizeHandler()}
onTouchStart={column.getResizeHandler()}
className={cn(
'absolute right-0 top-0 h-full w-1 cursor-col-resize',
'hover:bg-primary-500 opacity-0 hover:opacity-100',
column.getIsResizing() && 'bg-primary-500 opacity-100'
)}
/>
)}
</div>
</div>
))}
</div>
</div>
);
}

View file

@ -0,0 +1 @@
export { DataTable } from './DataTable';

View file

@ -0,0 +1,66 @@
import { ReactNode } from 'react';
export interface TableColumn<T = Record<string, unknown>> {
id: string;
header: string;
accessorKey?: keyof T;
accessorFn?: (row: T) => unknown;
cell?: (props: { getValue: () => unknown; row: { original: T } }) => ReactNode;
sortable?: boolean;
filterable?: boolean;
groupable?: boolean;
width?: number;
minWidth?: number;
maxWidth?: number;
enableResizing?: boolean;
}
export interface TableConfig {
enableSorting?: boolean;
enableFiltering?: boolean;
enableGrouping?: boolean;
enableColumnResizing?: boolean;
enableRowSelection?: boolean;
manualPagination?: boolean;
pageSize?: number;
}
export interface VirtualTableProps<T> {
data: T[];
columns: TableColumn<T>[];
height?: number;
width?: number;
rowHeight?: number;
headerHeight?: number;
config?: TableConfig;
loading?: boolean;
onRowClick?: (row: T) => void;
className?: string;
}
export interface FilterCondition {
column: string;
operator:
| 'equals'
| 'contains'
| 'startsWith'
| 'endsWith'
| 'gt'
| 'lt'
| 'gte'
| 'lte'
| 'between'
| 'in';
value: string | number | boolean | null;
value2?: string | number | boolean | null; // For 'between' operator
}
export interface SortingState {
id: string;
desc: boolean;
}
export interface GroupingState {
groupBy: string[];
aggregations?: Record<string, 'sum' | 'avg' | 'count' | 'min' | 'max'>;
}

View file

@ -0,0 +1,35 @@
import { ReactNode } from 'react';
import { Card } from './Card';
interface StatCardProps {
title: string;
value: string | number;
subtitle?: string;
icon: ReactNode;
iconBgColor: string;
valueColor: string;
borderColor: string;
}
export function StatCard({
title,
value,
subtitle,
icon,
iconBgColor,
valueColor,
borderColor,
}: StatCardProps) {
return (
<Card hover className={`hover:${borderColor}`}>
<div className="flex items-center">
<div className={`p-1.5 ${iconBgColor} rounded`}>{icon}</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-text-primary">{title}</h3>
<p className={`text-lg font-bold ${valueColor}`}>{value}</p>
{subtitle && <p className="text-xs text-text-muted">{subtitle}</p>}
</div>
</div>
</Card>
);
}

View file

@ -0,0 +1,3 @@
export { Card, CardHeader, CardContent } from './Card';
export { StatCard } from './StatCard';
export { DataTable } from './DataTable';

View file

@ -0,0 +1,16 @@
import { DashboardStats, DashboardActivity, PortfolioTable } from './components';
export function DashboardPage() {
return (
<>
<h1 className="text-lg font-bold text-text-primary mb-2">Welcome to Stock Bot Dashboard</h1>
<p className="text-text-secondary mb-6 text-sm">
Monitor your trading performance, manage portfolios, and analyze market data.
</p>
<DashboardStats />
<DashboardActivity />
<PortfolioTable />
</>
);
}

View file

@ -0,0 +1,31 @@
import { Card, CardHeader, CardContent } from '@/components/ui';
export function DashboardActivity() {
return (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card>
<CardHeader>
<h3 className="text-base font-medium text-text-primary">Recent Activity</h3>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between p-2 bg-surface-tertiary rounded border border-border">
<span className="text-text-secondary text-sm">No recent activity</span>
<span className="text-text-muted text-xs">--</span>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<h3 className="text-base font-medium text-text-primary">Market Overview</h3>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between p-2 bg-surface-tertiary rounded border border-border">
<span className="text-text-secondary text-sm">Market data loading...</span>
<span className="text-text-muted text-xs">--</span>
</div>
</CardContent>
</Card>
</div>
);
}

View file

@ -0,0 +1,38 @@
import { CurrencyDollarIcon, ChartBarIcon, DocumentTextIcon } from '@heroicons/react/24/outline';
import { StatCard } from '@/components/ui';
export function DashboardStats() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 mb-6">
<StatCard
title="Portfolio Value"
value="$0.00"
subtitle="Total assets"
icon={<CurrencyDollarIcon className="h-5 w-5 text-primary-400" />}
iconBgColor="bg-primary-500/10"
valueColor="text-primary-400"
borderColor="border-primary-500/50"
/>
<StatCard
title="Total Return"
value="+0.00%"
subtitle="Since inception"
icon={<ChartBarIcon className="h-5 w-5 text-success" />}
iconBgColor="bg-success/10"
valueColor="text-success"
borderColor="border-success/50"
/>
<StatCard
title="Active Strategies"
value="0"
subtitle="Running algorithms"
icon={<DocumentTextIcon className="h-5 w-5 text-warning" />}
iconBgColor="bg-warning/10"
valueColor="text-warning"
borderColor="border-warning/50"
/>
</div>
);
}

View file

@ -0,0 +1,652 @@
import { DataTable } from '@/components/ui';
import { ColumnDef } from '@tanstack/react-table';
import React from 'react';
interface PortfolioItem {
symbol: string;
quantity: number;
avgPrice: number;
currentPrice: number;
value: number;
change: number;
changePercent: number;
// Additional columns for stress testing
volume: number;
marketCap: number;
pe: number;
pb: number;
roe: number;
debt: number;
revenue: number;
earnings: number;
dividend: number;
beta: number;
rsi: number;
macd: number;
sma20: number;
sma50: number;
sma200: number;
support: number;
resistance: number;
volatility: number;
sharpe: number;
alpha: number;
correlation: number;
sector: string;
industry: string;
country: string;
exchange: string;
currency: string;
lastUpdate: string;
analyst1: string;
analyst2: string;
analyst3: string;
rating1: number;
rating2: number;
rating3: number;
target1: number;
target2: number;
target3: number;
risk: string;
esg: number;
}
export function PortfolioTable() {
// Generate 100,000 rows of sample data
const data: PortfolioItem[] = React.useMemo(() => {
const symbols = [
'AAPL',
'GOOGL',
'MSFT',
'TSLA',
'AMZN',
'META',
'NFLX',
'NVDA',
'AMD',
'INTC',
'CRM',
'ORCL',
'IBM',
'CSCO',
'UBER',
'LYFT',
'SNAP',
'TWTR',
'SPOT',
'SQ',
];
const sectors = [
'Technology',
'Healthcare',
'Finance',
'Energy',
'Consumer',
'Industrial',
'Materials',
'Utilities',
'Real Estate',
'Telecom',
];
const industries = [
'Software',
'Hardware',
'Biotech',
'Banking',
'Oil & Gas',
'Retail',
'Manufacturing',
'Mining',
'Utilities',
'REITs',
];
const countries = [
'USA',
'Canada',
'UK',
'Germany',
'Japan',
'China',
'India',
'Brazil',
'Australia',
'France',
];
const exchanges = ['NYSE', 'NASDAQ', 'LSE', 'TSX', 'Nikkei', 'SSE', 'BSE', 'ASX', 'Euronext'];
const currencies = ['USD', 'CAD', 'GBP', 'EUR', 'JPY', 'CNY', 'INR', 'BRL', 'AUD'];
const analysts = [
'Goldman Sachs',
'Morgan Stanley',
'JPMorgan',
'Bank of America',
'Wells Fargo',
'Credit Suisse',
'Deutsche Bank',
'Barclays',
'UBS',
'Citigroup',
];
const risks = ['Low', 'Medium', 'High', 'Very High'];
return Array.from({ length: 100000 }, (_, i) => {
const basePrice = 50 + Math.random() * 500;
const change = (Math.random() - 0.5) * 20;
const quantity = Math.floor(Math.random() * 1000) + 1;
return {
symbol: `${symbols[i % symbols.length]}${Math.floor(i / symbols.length)}`,
quantity,
avgPrice: basePrice,
currentPrice: basePrice + change,
value: (basePrice + change) * quantity,
change: change * quantity,
changePercent: (change / basePrice) * 100,
volume: Math.floor(Math.random() * 10000000),
marketCap: Math.floor(Math.random() * 1000000000000),
pe: Math.random() * 50 + 5,
pb: Math.random() * 10 + 0.5,
roe: Math.random() * 30 + 5,
debt: Math.random() * 50,
revenue: Math.floor(Math.random() * 100000000000),
earnings: Math.floor(Math.random() * 10000000000),
dividend: Math.random() * 5,
beta: Math.random() * 3 + 0.5,
rsi: Math.random() * 100,
macd: (Math.random() - 0.5) * 10,
sma20: basePrice + (Math.random() - 0.5) * 10,
sma50: basePrice + (Math.random() - 0.5) * 20,
sma200: basePrice + (Math.random() - 0.5) * 50,
support: basePrice - Math.random() * 20,
resistance: basePrice + Math.random() * 20,
volatility: Math.random() * 100,
sharpe: Math.random() * 3,
alpha: (Math.random() - 0.5) * 20,
correlation: (Math.random() - 0.5) * 2,
sector: sectors[Math.floor(Math.random() * sectors.length)],
industry: industries[Math.floor(Math.random() * industries.length)],
country: countries[Math.floor(Math.random() * countries.length)],
exchange: exchanges[Math.floor(Math.random() * exchanges.length)],
currency: currencies[Math.floor(Math.random() * currencies.length)],
lastUpdate: new Date(Date.now() - Math.random() * 86400000).toISOString(),
analyst1: analysts[Math.floor(Math.random() * analysts.length)],
analyst2: analysts[Math.floor(Math.random() * analysts.length)],
analyst3: analysts[Math.floor(Math.random() * analysts.length)],
rating1: Math.random() * 5 + 1,
rating2: Math.random() * 5 + 1,
rating3: Math.random() * 5 + 1,
target1: basePrice + (Math.random() - 0.3) * 50,
target2: basePrice + (Math.random() - 0.3) * 50,
target3: basePrice + (Math.random() - 0.3) * 50,
risk: risks[Math.floor(Math.random() * risks.length)],
esg: Math.random() * 100,
};
});
}, []);
const columns: ColumnDef<PortfolioItem>[] = [
{
id: 'symbol',
header: 'Symbol',
accessorKey: 'symbol',
size: 120,
cell: ({ getValue }) => (
<span className="font-mono font-bold text-primary-400">{getValue() as string}</span>
),
},
{
id: 'quantity',
header: 'Quantity',
accessorKey: 'quantity',
size: 100,
cell: ({ getValue }) => (
<span className="font-mono">{(getValue() as number).toLocaleString()}</span>
),
},
{
id: 'avgPrice',
header: 'Avg Price',
accessorKey: 'avgPrice',
size: 120,
cell: ({ getValue }) => (
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'currentPrice',
header: 'Current Price',
accessorKey: 'currentPrice',
size: 120,
cell: ({ getValue }) => (
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'value',
header: 'Value',
accessorKey: 'value',
size: 120,
cell: ({ getValue }) => (
<span className="font-mono font-bold">${(getValue() as number).toLocaleString()}</span>
),
},
{
id: 'change',
header: 'P&L',
accessorKey: 'change',
size: 120,
cell: ({ getValue }) => {
const value = getValue() as number;
const isPositive = value >= 0;
return (
<span className={`font-mono font-medium ${isPositive ? 'text-success' : 'text-danger'}`}>
{isPositive ? '+' : ''}${value.toLocaleString()}
</span>
);
},
},
{
id: 'changePercent',
header: 'P&L %',
accessorKey: 'changePercent',
size: 100,
cell: ({ getValue }) => {
const value = getValue() as number;
const isPositive = value >= 0;
return (
<span className={`font-mono font-medium ${isPositive ? 'text-success' : 'text-danger'}`}>
{isPositive ? '+' : ''}
{value.toFixed(2)}%
</span>
);
},
},
{
id: 'volume',
header: 'Volume',
accessorKey: 'volume',
size: 120,
cell: ({ getValue }) => (
<span className="font-mono text-text-secondary">
{(getValue() as number).toLocaleString()}
</span>
),
},
{
id: 'marketCap',
header: 'Market Cap',
accessorKey: 'marketCap',
size: 120,
cell: ({ getValue }) => {
const value = getValue() as number;
if (value >= 1e12) return <span className="font-mono">${(value / 1e12).toFixed(2)}T</span>;
if (value >= 1e9) return <span className="font-mono">${(value / 1e9).toFixed(2)}B</span>;
if (value >= 1e6) return <span className="font-mono">${(value / 1e6).toFixed(2)}M</span>;
return <span className="font-mono">${value.toLocaleString()}</span>;
},
},
{
id: 'pe',
header: 'P/E',
accessorKey: 'pe',
size: 80,
cell: ({ getValue }) => (
<span className="font-mono">{(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'pb',
header: 'P/B',
accessorKey: 'pb',
size: 80,
cell: ({ getValue }) => (
<span className="font-mono">{(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'roe',
header: 'ROE %',
accessorKey: 'roe',
size: 80,
cell: ({ getValue }) => (
<span className="font-mono">{(getValue() as number).toFixed(1)}%</span>
),
},
{
id: 'debt',
header: 'Debt %',
accessorKey: 'debt',
size: 80,
cell: ({ getValue }) => (
<span className="font-mono">{(getValue() as number).toFixed(1)}%</span>
),
},
{
id: 'revenue',
header: 'Revenue',
accessorKey: 'revenue',
size: 120,
cell: ({ getValue }) => {
const value = getValue() as number;
if (value >= 1e9) return <span className="font-mono">${(value / 1e9).toFixed(2)}B</span>;
if (value >= 1e6) return <span className="font-mono">${(value / 1e6).toFixed(2)}M</span>;
return <span className="font-mono">${value.toLocaleString()}</span>;
},
},
{
id: 'earnings',
header: 'Earnings',
accessorKey: 'earnings',
size: 120,
cell: ({ getValue }) => {
const value = getValue() as number;
if (value >= 1e9) return <span className="font-mono">${(value / 1e9).toFixed(2)}B</span>;
if (value >= 1e6) return <span className="font-mono">${(value / 1e6).toFixed(2)}M</span>;
return <span className="font-mono">${value.toLocaleString()}</span>;
},
},
{
id: 'dividend',
header: 'Dividend %',
accessorKey: 'dividend',
size: 100,
cell: ({ getValue }) => {
const value = getValue() as number;
return <span className="font-mono text-success">{value.toFixed(2)}%</span>;
},
},
{
id: 'beta',
header: 'Beta',
accessorKey: 'beta',
size: 80,
cell: ({ getValue }) => {
const value = getValue() as number;
const color = value > 1 ? 'text-warning' : value < 1 ? 'text-success' : 'text-text-primary';
return <span className={`font-mono ${color}`}>{value.toFixed(2)}</span>;
},
},
{
id: 'rsi',
header: 'RSI',
accessorKey: 'rsi',
size: 80,
cell: ({ getValue }) => {
const value = getValue() as number;
const color =
value > 70 ? 'text-danger' : value < 30 ? 'text-success' : 'text-text-primary';
return <span className={`font-mono ${color}`}>{value.toFixed(1)}</span>;
},
},
{
id: 'macd',
header: 'MACD',
accessorKey: 'macd',
size: 80,
cell: ({ getValue }) => {
const value = getValue() as number;
const color = value > 0 ? 'text-success' : 'text-danger';
return <span className={`font-mono ${color}`}>{value.toFixed(2)}</span>;
},
},
{
id: 'sma20',
header: 'SMA 20',
accessorKey: 'sma20',
size: 100,
cell: ({ getValue }) => (
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'sma50',
header: 'SMA 50',
accessorKey: 'sma50',
size: 100,
cell: ({ getValue }) => (
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'sma200',
header: 'SMA 200',
accessorKey: 'sma200',
size: 100,
cell: ({ getValue }) => (
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'support',
header: 'Support',
accessorKey: 'support',
size: 100,
cell: ({ getValue }) => (
<span className="font-mono text-success">${(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'resistance',
header: 'Resistance',
accessorKey: 'resistance',
size: 100,
cell: ({ getValue }) => (
<span className="font-mono text-danger">${(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'volatility',
header: 'Volatility',
accessorKey: 'volatility',
size: 100,
cell: ({ getValue }) => (
<span className="font-mono">{(getValue() as number).toFixed(1)}%</span>
),
},
{
id: 'sharpe',
header: 'Sharpe',
accessorKey: 'sharpe',
size: 80,
cell: ({ getValue }) => (
<span className="font-mono">{(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'alpha',
header: 'Alpha',
accessorKey: 'alpha',
size: 80,
cell: ({ getValue }) => {
const value = getValue() as number;
const color = value > 0 ? 'text-success' : 'text-danger';
return <span className={`font-mono ${color}`}>{value.toFixed(2)}</span>;
},
},
{
id: 'correlation',
header: 'Correlation',
accessorKey: 'correlation',
size: 100,
cell: ({ getValue }) => (
<span className="font-mono">{(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'sector',
header: 'Sector',
accessorKey: 'sector',
size: 150,
cell: ({ getValue }) => <span className="text-text-secondary">{getValue() as string}</span>,
},
{
id: 'industry',
header: 'Industry',
accessorKey: 'industry',
size: 150,
cell: ({ getValue }) => <span className="text-text-secondary">{getValue() as string}</span>,
},
{
id: 'country',
header: 'Country',
accessorKey: 'country',
size: 100,
cell: ({ getValue }) => <span className="text-text-secondary">{getValue() as string}</span>,
},
{
id: 'exchange',
header: 'Exchange',
accessorKey: 'exchange',
size: 100,
cell: ({ getValue }) => <span className="text-text-secondary">{getValue() as string}</span>,
},
{
id: 'currency',
header: 'Currency',
accessorKey: 'currency',
size: 80,
cell: ({ getValue }) => (
<span className="font-mono text-text-secondary">{getValue() as string}</span>
),
},
{
id: 'lastUpdate',
header: 'Last Update',
accessorKey: 'lastUpdate',
size: 150,
cell: ({ getValue }) => (
<span className="text-xs text-text-muted">
{new Date(getValue() as string).toLocaleString()}
</span>
),
},
{
id: 'analyst1',
header: 'Analyst 1',
accessorKey: 'analyst1',
size: 120,
cell: ({ getValue }) => (
<span className="text-xs text-text-secondary">{getValue() as string}</span>
),
},
{
id: 'analyst2',
header: 'Analyst 2',
accessorKey: 'analyst2',
size: 120,
cell: ({ getValue }) => (
<span className="text-xs text-text-secondary">{getValue() as string}</span>
),
},
{
id: 'analyst3',
header: 'Analyst 3',
accessorKey: 'analyst3',
size: 120,
cell: ({ getValue }) => (
<span className="text-xs text-text-secondary">{getValue() as string}</span>
),
},
{
id: 'rating1',
header: 'Rating 1',
accessorKey: 'rating1',
size: 80,
cell: ({ getValue }) => {
const value = getValue() as number;
const color = value >= 4 ? 'text-success' : value >= 3 ? 'text-warning' : 'text-danger';
return <span className={`font-mono ${color}`}>{value.toFixed(1)}</span>;
},
},
{
id: 'rating2',
header: 'Rating 2',
accessorKey: 'rating2',
size: 80,
cell: ({ getValue }) => {
const value = getValue() as number;
const color = value >= 4 ? 'text-success' : value >= 3 ? 'text-warning' : 'text-danger';
return <span className={`font-mono ${color}`}>{value.toFixed(1)}</span>;
},
},
{
id: 'rating3',
header: 'Rating 3',
accessorKey: 'rating3',
size: 80,
cell: ({ getValue }) => {
const value = getValue() as number;
const color = value >= 4 ? 'text-success' : value >= 3 ? 'text-warning' : 'text-danger';
return <span className={`font-mono ${color}`}>{value.toFixed(1)}</span>;
},
},
{
id: 'target1',
header: 'Target 1',
accessorKey: 'target1',
size: 100,
cell: ({ getValue }) => (
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'target2',
header: 'Target 2',
accessorKey: 'target2',
size: 100,
cell: ({ getValue }) => (
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'target3',
header: 'Target 3',
accessorKey: 'target3',
size: 100,
cell: ({ getValue }) => (
<span className="font-mono">${(getValue() as number).toFixed(2)}</span>
),
},
{
id: 'risk',
header: 'Risk Level',
accessorKey: 'risk',
size: 100,
cell: ({ getValue }) => {
const value = getValue() as string;
const color =
value === 'Low' ? 'text-success' : value === 'Medium' ? 'text-warning' : 'text-danger';
return <span className={`px-2 py-1 rounded text-xs font-medium ${color}`}>{value}</span>;
},
},
{
id: 'esg',
header: 'ESG Score',
accessorKey: 'esg',
size: 100,
cell: ({ getValue }) => {
const value = getValue() as number;
const color = value >= 70 ? 'text-success' : value >= 40 ? 'text-warning' : 'text-danger';
return <span className={`font-mono ${color}`}>{value.toFixed(0)}</span>;
},
},
];
return (
<div className="mt-6">
<h3 className="text-lg font-bold text-text-primary mb-2">Portfolio Holdings</h3>
<p className="text-sm text-text-secondary mb-4">
Performance test: 100,000 rows × {columns.length} columns ={' '}
{(100000 * columns.length).toLocaleString()} cells
</p>
<div className="h-96 w-full border border-border rounded-lg overflow-hidden">
<DataTable
data={data}
columns={columns}
height="100%"
onRowClick={row => console.log('Clicked:', row.symbol)}
className="w-full h-full"
/>
</div>
</div>
);
}

View file

@ -0,0 +1,3 @@
export { DashboardStats } from './DashboardStats';
export { DashboardActivity } from './DashboardActivity';
export { PortfolioTable } from './PortfolioTable';

View file

@ -0,0 +1 @@
export { DashboardPage } from './DashboardPage';

120
apps/web/src/index.css Normal file
View file

@ -0,0 +1,120 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0 0;
--background-secondary: 0 0 0;
--background-tertiary: 0 0 0;
--surface: 0 0 0;
--surface-secondary: 10 10 10;
--surface-tertiary: 17 17 17;
--border: 26 26 26;
--border-secondary: 42 42 42;
--text-primary: 255 255 255;
--text-secondary: 161 161 170;
--text-muted: 113 113 122;
--primary: 99 102 241;
--primary-hover: 79 70 229;
--secondary: 16 185 129;
--accent: 245 158 11;
--destructive: 239 68 68;
--success: 34 197 94;
--warning: 251 191 36;
}
* {
box-sizing: border-box;
}
html {
@apply h-full;
}
body {
@apply h-full bg-black text-white font-sans antialiased text-sm leading-snug;
font-family:
'Inter',
system-ui,
-apple-system,
sans-serif;
}
#root {
@apply h-full;
}
}
@layer components {
/* Custom scrollbar for dark theme */
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: rgb(42 42 45) transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 6px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: rgb(42 42 45);
border-radius: 3px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: rgb(51 51 54);
}
/* Trading specific styles */
.profit {
@apply text-success;
}
.loss {
@apply text-danger;
}
.neutral {
@apply text-text-secondary;
}
/* Compact component utilities */
.compact-card {
@apply bg-surface-secondary p-3 rounded border border-border text-sm;
}
.compact-button {
@apply px-2 py-1 text-sm rounded border border-border bg-surface hover:bg-surface-secondary transition-colors;
}
.compact-input {
@apply px-2 py-1 text-sm rounded border border-border bg-surface focus:ring-1 focus:ring-primary-500 focus:border-primary-500;
}
.compact-nav-item {
@apply group flex gap-x-2 rounded-md px-2 py-1.5 text-sm leading-tight font-medium transition-colors;
}
}
.sticky {
position: sticky !important;
position: -webkit-sticky !important;
z-index: 2;
}
.row,
.sticky {
display: flex;
align-items: center;
background-color: white;
border-bottom: 1px solid #eee;
box-sizing: border-box;
}

View file

@ -0,0 +1 @@
export * from './navigation';

View file

@ -0,0 +1,40 @@
import {
ChartBarIcon,
CogIcon,
CurrencyDollarIcon,
DocumentTextIcon,
HomeIcon,
} from '@heroicons/react/24/outline';
export const navigation = [
{
name: 'Dashboard',
href: '/',
icon: HomeIcon,
current: true,
},
{
name: 'Portfolio',
href: '/portfolio',
icon: CurrencyDollarIcon,
current: false,
},
{
name: 'Analytics',
href: '/analytics',
icon: ChartBarIcon,
current: false,
},
{
name: 'Reports',
href: '/reports',
icon: DocumentTextIcon,
current: false,
},
{
name: 'Settings',
href: '/settings',
icon: CogIcon,
current: false,
},
];

View file

@ -0,0 +1,6 @@
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View file

@ -0,0 +1,47 @@
export * from './cn';
/**
* Format currency values
*/
export function formatCurrency(value: number, currency = 'USD'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(value);
}
/**
* Format percentage values
*/
export function formatPercentage(value: number, decimals = 2): string {
return `${value >= 0 ? '+' : ''}${value.toFixed(decimals)}%`;
}
/**
* Format large numbers with K, M, B suffixes
*/
export function formatNumber(num: number): string {
if (num >= 1e9) return (num / 1e9).toFixed(1) + 'B';
if (num >= 1e6) return (num / 1e6).toFixed(1) + 'M';
if (num >= 1e3) return (num / 1e3).toFixed(1) + 'K';
return num.toString();
}
/**
* Get color class based on numeric value (profit/loss)
*/
export function getValueColor(value: number): string {
if (value > 0) return 'text-success';
if (value < 0) return 'text-danger';
return 'text-text-secondary';
}
/**
* Truncate text to specified length
*/
export function truncateText(text: string, length: number): string {
if (text.length <= length) return text;
return text.slice(0, length) + '...';
}

10
apps/web/src/main.tsx Normal file
View file

@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './app';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);