removed angular app and switched to react
This commit is contained in:
parent
56e3938561
commit
3a9d45c543
101 changed files with 2697 additions and 4075 deletions
232
apps/web/src/App.tsx
Normal file
232
apps/web/src/App.tsx
Normal 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
10
apps/web/src/app/App.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { Layout } from '@/components/layout';
|
||||
import { DashboardPage } from '@/features/dashboard';
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<Layout title="Dashboard">
|
||||
<DashboardPage />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
1
apps/web/src/app/index.ts
Normal file
1
apps/web/src/app/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { App } from './App';
|
||||
2
apps/web/src/components/index.ts
Normal file
2
apps/web/src/components/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './ui';
|
||||
export * from './layout';
|
||||
22
apps/web/src/components/layout/Header.tsx
Normal file
22
apps/web/src/components/layout/Header.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
23
apps/web/src/components/layout/Layout.tsx
Normal file
23
apps/web/src/components/layout/Layout.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
117
apps/web/src/components/layout/Sidebar.tsx
Normal file
117
apps/web/src/components/layout/Sidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
3
apps/web/src/components/layout/index.ts
Normal file
3
apps/web/src/components/layout/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { Layout } from './Layout';
|
||||
export { Sidebar } from './Sidebar';
|
||||
export { Header } from './Header';
|
||||
40
apps/web/src/components/ui/Card.tsx
Normal file
40
apps/web/src/components/ui/Card.tsx
Normal 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>;
|
||||
}
|
||||
191
apps/web/src/components/ui/DataTable/DataTable.tsx
Normal file
191
apps/web/src/components/ui/DataTable/DataTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
89
apps/web/src/components/ui/DataTable/ExampleTable.tsx
Normal file
89
apps/web/src/components/ui/DataTable/ExampleTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
145
apps/web/src/components/ui/DataTable/TableControls.tsx
Normal file
145
apps/web/src/components/ui/DataTable/TableControls.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
83
apps/web/src/components/ui/DataTable/TableHeader.tsx
Normal file
83
apps/web/src/components/ui/DataTable/TableHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
1
apps/web/src/components/ui/DataTable/index.ts
Normal file
1
apps/web/src/components/ui/DataTable/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { DataTable } from './DataTable';
|
||||
66
apps/web/src/components/ui/DataTable/types.ts
Normal file
66
apps/web/src/components/ui/DataTable/types.ts
Normal 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'>;
|
||||
}
|
||||
35
apps/web/src/components/ui/StatCard.tsx
Normal file
35
apps/web/src/components/ui/StatCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
3
apps/web/src/components/ui/index.ts
Normal file
3
apps/web/src/components/ui/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { Card, CardHeader, CardContent } from './Card';
|
||||
export { StatCard } from './StatCard';
|
||||
export { DataTable } from './DataTable';
|
||||
16
apps/web/src/features/dashboard/DashboardPage.tsx
Normal file
16
apps/web/src/features/dashboard/DashboardPage.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
652
apps/web/src/features/dashboard/components/PortfolioTable.tsx
Normal file
652
apps/web/src/features/dashboard/components/PortfolioTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
3
apps/web/src/features/dashboard/components/index.ts
Normal file
3
apps/web/src/features/dashboard/components/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { DashboardStats } from './DashboardStats';
|
||||
export { DashboardActivity } from './DashboardActivity';
|
||||
export { PortfolioTable } from './PortfolioTable';
|
||||
1
apps/web/src/features/dashboard/index.ts
Normal file
1
apps/web/src/features/dashboard/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { DashboardPage } from './DashboardPage';
|
||||
120
apps/web/src/index.css
Normal file
120
apps/web/src/index.css
Normal 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;
|
||||
}
|
||||
1
apps/web/src/lib/constants/index.ts
Normal file
1
apps/web/src/lib/constants/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './navigation';
|
||||
40
apps/web/src/lib/constants/navigation.ts
Normal file
40
apps/web/src/lib/constants/navigation.ts
Normal 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,
|
||||
},
|
||||
];
|
||||
6
apps/web/src/lib/utils/cn.ts
Normal file
6
apps/web/src/lib/utils/cn.ts
Normal 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));
|
||||
}
|
||||
47
apps/web/src/lib/utils/index.ts
Normal file
47
apps/web/src/lib/utils/index.ts
Normal 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
10
apps/web/src/main.tsx
Normal 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>
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue