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

View file

@ -1,17 +0,0 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View file

@ -1,42 +0,0 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db

View file

@ -1,5 +0,0 @@
{
"plugins": {
"@tailwindcss/postcss": {}
}
}

View file

@ -1,4 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

View file

@ -1,20 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

View file

@ -1,42 +0,0 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View file

@ -1,59 +0,0 @@
# TradingDashboard
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.0.0.
## Development server
To start a local development server, run:
```bash
ng serve
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the project run:
```bash
ng build
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

View file

@ -1,91 +0,0 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"packageManager": "npm",
"analytics": false
},
"newProjectRoot": "projects",
"projects": {
"trading-dashboard": {
"projectType": "application",
"schematics": {
"@schematics/angular:component": {
"style": "css"
}
},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"inlineStyleLanguage": "css",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": ["src/styles.css"]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": false
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "trading-dashboard:build:production"
},
"development": {
"buildTarget": "trading-dashboard:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "tsconfig.spec.json",
"inlineStyleLanguage": "css",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": ["src/styles.css"]
}
}
}
}
}
}

View file

@ -1,44 +0,0 @@
{
"name": "trading-dashboard",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"devvvv": "ng serve --port 5173 --host 0.0.0.0",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"private": true,
"dependencies": {
"@angular/animations": "^20.0.0",
"@angular/cdk": "^20.0.1",
"@angular/common": "^20.0.0",
"@angular/compiler": "^20.0.0",
"@angular/core": "^20.0.0",
"@angular/forms": "^20.0.0",
"@angular/material": "^20.0.1",
"@angular/platform-browser": "^20.0.0",
"@angular/router": "^20.0.0",
"rxjs": "~7.8.2",
"tslib": "^2.8.1",
"zone.js": "~0.15.1"
},
"devDependencies": {
"@angular/build": "^20.0.0",
"@angular/cli": "^20.0.0",
"@angular/compiler-cli": "^20.0.0",
"@tailwindcss/postcss": "^4.1.8",
"@types/jasmine": "~5.1.8",
"autoprefixer": "^10.4.21",
"jasmine-core": "~5.7.1",
"karma": "~6.4.4",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.1",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"postcss": "^8.5.4",
"tailwindcss": "^4.1.8",
"typescript": "~5.8.3"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -1,19 +0,0 @@
import { provideHttpClient } from '@angular/common/http';
import {
ApplicationConfig,
provideBrowserGlobalErrorListeners,
provideZonelessChangeDetection,
} from '@angular/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideRouter(routes),
provideHttpClient(),
provideAnimationsAsync(),
],
};

View file

@ -1,174 +0,0 @@
/* Custom Angular Material integration styles */
/* Sidenav styles */
.mat-sidenav-container {
background-color: transparent;
}
.mat-sidenav {
border-radius: 0;
width: 16rem;
background-color: white !important;
border-right: 1px solid #e5e7eb !important;
}
/* Toolbar styles */
.mat-toolbar {
background-color: white;
color: #374151;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
/* Button styles */
.mat-mdc-button.nav-button {
width: 100%;
text-align: left;
justify-content: flex-start;
padding: 0.75rem 1rem;
border-radius: 0.375rem;
margin-bottom: 0.25rem;
background-color: transparent;
transition: background-color 0.15s ease-in-out;
}
.mat-mdc-button.nav-button:hover {
background-color: #f3f4f6;
}
.mat-mdc-button.nav-button.bg-blue-50 {
background-color: #eff6ff !important;
color: #1d4ed8 !important;
}
/* Card styles */
.mat-mdc-card {
border-radius: 0.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
border: 1px solid #f3f4f6;
background-color: white !important;
}
/* Tab styles */
.mat-mdc-tab-group .mat-mdc-tab-header {
border-bottom: 1px solid #e5e7eb;
}
.mat-mdc-tab-label {
color: #6b7280;
}
.mat-mdc-tab-label:hover {
color: #111827;
}
.mat-mdc-tab-label-active {
color: #2563eb;
font-weight: 500;
}
/* Chip styles for status indicators */
.mat-mdc-chip-set .mat-mdc-chip {
background-color: white;
border: 1px solid #e5e7eb;
}
.chip-green {
background-color: #dcfce7 !important;
color: #166534 !important;
border: 1px solid #bbf7d0 !important;
}
.chip-blue {
background-color: #dbeafe !important;
color: #1e40af !important;
border: 1px solid #bfdbfe !important;
}
.status-chip-active {
background-color: #dcfce7;
color: #166534;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
display: inline-block;
}
.status-chip-medium {
background-color: #dbeafe;
color: #1e40af;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}
/* Table styles */
.mat-mdc-table {
border-radius: 0.5rem;
overflow: hidden;
border: 1px solid #f3f4f6;
background-color: white;
}
.mat-mdc-header-row {
background-color: #f9fafb;
}
.mat-mdc-header-cell {
font-weight: 500;
color: #374151;
font-size: 0.875rem;
}
.mat-mdc-cell {
color: #111827;
font-size: 0.875rem;
padding: 1rem 0;
}
.mat-mdc-row:hover {
background-color: #f9fafb;
transition: background-color 0.15s ease;
}
/* Custom utility classes for the dashboard */
.portfolio-card {
background-color: white !important;
border-radius: 0.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
border: 1px solid #f3f4f6;
padding: 1.5rem;
}
.metric-value {
font-size: 1.5rem;
font-weight: 700;
}
.metric-label {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.metric-change-positive {
color: #16a34a;
font-weight: 500;
}
.metric-change-negative {
color: #dc2626;
font-weight: 500;
}
/* Responsive styles */
@media (max-width: 768px) {
.mat-sidenav {
width: 100%;
}
.hide-mobile {
display: none;
}
}

View file

@ -1,67 +0,0 @@
<!-- Trading Dashboard App -->
<div class="app-layout">
<!-- Sidebar -->
<app-sidebar [opened]="sidenavOpened()" (navigationItemClick)="onNavigationClick($event)"></app-sidebar>
<!-- Main Content Area -->
<div class="main-content" [class.main-content-closed]="!sidenavOpened()">
<!-- Top Navigation Bar -->
<mat-toolbar class="top-toolbar">
<button mat-icon-button (click)="toggleSidenav()" class="mr-2">
<mat-icon>menu</mat-icon>
</button> <span class="text-lg font-semibold text-gray-800">{{ title }}</span>
<span class="spacer"></span>
<app-notifications></app-notifications>
<button mat-icon-button>
<mat-icon>account_circle</mat-icon>
</button>
</mat-toolbar>
<!-- Page Content -->
<div class="page-content">
<router-outlet></router-outlet>
</div>
</div>
</div>
<style>
.app-layout {
display: flex;
height: 100vh;
background-color: #f9fafb;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
margin-left: 256px; /* Width of sidebar */
transition: margin-left 0.3s ease;
}
.main-content-closed {
margin-left: 0;
}
.top-toolbar {
background-color: white;
border-bottom: 1px solid #e5e7eb;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
}
.spacer {
flex: 1;
}
.page-content {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
}
@media (max-width: 768px) {
.main-content {
margin-left: 0;
}
}
</style>

View file

@ -1,15 +0,0 @@
import { Routes } from '@angular/router';
import { DashboardComponent } from './pages/dashboard/dashboard.component';
import { ExchangesComponent } from './pages/exchanges/exchanges.component';
import { MarketDataComponent } from './pages/market-data/market-data.component';
import { SettingsComponent } from './pages/settings/settings.component';
export const routes: Routes = [
{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
{ path: 'dashboard', component: DashboardComponent },
{ path: 'market-data', component: MarketDataComponent },
{ path: 'exchanges', component: ExchangesComponent },
{ path: 'settings', component: SettingsComponent },
{ path: '**', redirectTo: '/dashboard' },
];

View file

@ -1,25 +0,0 @@
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [provideZonelessChangeDetection()],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, trading-dashboard');
});
});

View file

@ -1,43 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { filter } from 'rxjs/operators';
import { BreadcrumbComponent } from './components/breadcrumb/breadcrumb.component';
import { NotificationsComponent } from './components/notifications/notifications';
import { SidebarComponent } from './components/sidebar/sidebar.component';
import { BreadcrumbService } from './services/breadcrumb.service';
@Component({
selector: 'app-root',
imports: [
RouterOutlet,
CommonModule,
MatSidenavModule,
MatToolbarModule,
MatButtonModule,
MatIconModule,
MatChipsModule,
SidebarComponent,
NotificationsComponent,
],
templateUrl: './app.html',
styleUrl: './app.css',
})
export class App {
protected title = 'Trading Dashboard';
protected sidenavOpened = signal(true);
toggleSidenav() {
this.sidenavOpened.set(!this.sidenavOpened());
}
onNavigationClick(route: string) {
// Handle navigation if needed
console.log('Navigating to:', route);
}
}

View file

@ -1,93 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { BreadcrumbItem, BreadcrumbService } from '../../services/breadcrumb.service';
@Component({
selector: 'app-breadcrumb',
standalone: true,
imports: [CommonModule, MatIconModule],
template: `
<nav class="breadcrumb-nav" *ngIf="breadcrumbs$ | async as breadcrumbs">
<ol class="breadcrumb-list">
<li
*ngFor="let item of breadcrumbs; let last = last"
class="breadcrumb-item"
[class.active]="item.isActive"
>
<a
*ngIf="!item.isActive; else activeItem"
[routerLink]="item.url"
class="breadcrumb-link"
>
{{ item.label }}
</a>
<ng-template #activeItem>
<span class="breadcrumb-active">{{ item.label }}</span>
</ng-template>
<mat-icon *ngIf="!last" class="breadcrumb-separator">chevron_right</mat-icon>
</li>
</ol>
</nav>
`,
styles: [`
.breadcrumb-nav {
padding: 0.5rem 0;
}
.breadcrumb-list {
display: flex;
align-items: center;
list-style: none;
margin: 0;
padding: 0;
gap: 0.5rem;
}
.breadcrumb-item {
display: flex;
align-items: center;
gap: 0.5rem;
}
.breadcrumb-link {
color: #6b7280;
text-decoration: none;
font-size: 0.875rem;
font-weight: 500;
transition: color 0.2s ease;
}
.breadcrumb-link:hover {
color: #374151;
}
.breadcrumb-active {
color: #111827;
font-size: 0.875rem;
font-weight: 600;
}
.breadcrumb-separator {
color: #9ca3af;
font-size: 1rem;
}
`]
})
export class BreadcrumbComponent implements OnInit {
breadcrumbs$: Observable<BreadcrumbItem[]>;
constructor(
private breadcrumbService: BreadcrumbService,
private router: Router
) {
this.breadcrumbs$ = this.breadcrumbService.breadcrumbs$;
}
ngOnInit() {
// Initialize breadcrumbs for current route
this.breadcrumbService.breadcrumbs$.subscribe();
}
}

View file

@ -1,45 +0,0 @@
::ng-deep .notification-menu {
width: 380px;
max-width: 90vw;
}
.notification-header {
padding: 12px 16px !important;
height: auto !important;
line-height: normal !important;
}
.notification-empty {
padding: 16px !important;
height: auto !important;
line-height: normal !important;
}
.notification-item {
padding: 12px 16px !important;
height: auto !important;
line-height: normal !important;
white-space: normal !important;
border-left: 3px solid transparent;
transition: all 0.2s ease;
}
.notification-item:hover {
background-color: #f5f5f5;
}
.notification-item.unread {
background-color: #f0f9ff;
border-left-color: #0ea5e9;
}
.notification-item.unread .font-medium {
font-weight: 600;
}
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

View file

@ -1,75 +0,0 @@
<button
mat-icon-button
[matMenuTriggerFor]="notificationMenu"
[matBadge]="unreadCount"
[matBadgeHidden]="unreadCount === 0"
matBadgeColor="warn"
matBadgeSize="small">
<mat-icon>notifications</mat-icon>
</button>
<mat-menu #notificationMenu="matMenu" class="notification-menu">
<div mat-menu-item disabled class="notification-header">
<div class="flex items-center justify-between w-full px-2">
<span class="font-semibold">Notifications</span>
@if (notifications.length > 0) {
<div class="flex gap-2">
<button mat-button (click)="markAllAsRead()" class="text-xs">
Mark all read
</button>
<button mat-button (click)="clearAll()" class="text-xs">
Clear all
</button>
</div>
}
</div>
</div>
<mat-divider></mat-divider>
@if (notifications.length === 0) {
<div mat-menu-item disabled class="notification-empty">
<div class="text-center py-4 text-gray-500">
<mat-icon class="text-2xl">notifications_none</mat-icon>
<p class="mt-1 text-sm">No notifications</p>
</div>
</div>
} @else {
@for (notification of notifications.slice(0, 5); track notification.id) {
<div
mat-menu-item
class="notification-item"
[class.unread]="!notification.read"
(click)="markAsRead(notification)">
<div class="flex items-start gap-3 w-full">
<mat-icon [class]="getNotificationColor(notification.type)" class="mt-1">
{{ getNotificationIcon(notification.type) }}
</mat-icon>
<div class="flex-1 min-w-0">
<div class="flex items-center justify-between">
<p class="font-medium text-sm truncate">{{ notification.title }}</p>
<button
mat-icon-button
(click)="clearNotification(notification); $event.stopPropagation()"
class="text-gray-400 hover:text-gray-600 ml-2">
<mat-icon class="text-lg">close</mat-icon>
</button>
</div>
<p class="text-gray-600 text-xs mt-1 line-clamp-2">{{ notification.message }}</p>
<p class="text-gray-400 text-xs mt-1">{{ formatTime(notification.timestamp) }}</p>
</div>
</div>
</div>
@if (!$last) {
<mat-divider></mat-divider>
}
}
@if (notifications.length > 5) {
<mat-divider></mat-divider>
<div mat-menu-item disabled class="text-center text-sm text-gray-500">
{{ notifications.length - 5 }} more notifications...
</div>
}
}
</mat-menu>

View file

@ -1,100 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { MatBadgeModule } from '@angular/material/badge';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { Notification, NotificationService } from '../../services/notification.service';
@Component({
selector: 'app-notifications',
imports: [
CommonModule,
MatIconModule,
MatButtonModule,
MatBadgeModule,
MatMenuModule,
MatListModule,
MatDividerModule,
],
templateUrl: './notifications.html',
styleUrl: './notifications.css',
})
export class NotificationsComponent {
private notificationService = inject(NotificationService);
get notifications() {
return this.notificationService.notifications();
}
get unreadCount() {
return this.notificationService.unreadCount();
}
markAsRead(notification: Notification) {
this.notificationService.markAsRead(notification.id);
}
markAllAsRead() {
this.notificationService.markAllAsRead();
}
clearNotification(notification: Notification) {
this.notificationService.clearNotification(notification.id);
}
clearAll() {
this.notificationService.clearAllNotifications();
}
getNotificationIcon(type: string): string {
switch (type) {
case 'error':
return 'error';
case 'warning':
return 'warning';
case 'success':
return 'check_circle';
case 'info':
default:
return 'info';
}
}
getNotificationColor(type: string): string {
switch (type) {
case 'error':
return 'text-red-600';
case 'warning':
return 'text-yellow-600';
case 'success':
return 'text-green-600';
case 'info':
default:
return 'text-blue-600';
}
}
formatTime(timestamp: Date): string {
const now = new Date();
const diff = now.getTime() - timestamp.getTime();
const minutes = Math.floor(diff / 60000);
if (minutes < 1) {
return 'Just now';
}
if (minutes < 60) {
return `${minutes}m ago`;
}
const hours = Math.floor(minutes / 60);
if (hours < 24) {
return `${hours}h ago`;
}
const days = Math.floor(hours / 24);
return `${days}d ago`;
}
}

View file

@ -1,38 +0,0 @@
/* Sidebar specific styles */
.sidebar {
position: fixed;
top: 0;
left: 0;
width: 16rem; /* 256px */
height: 100vh;
background-color: white;
border-right: 1px solid #e5e7eb;
transform: translateX(0);
transition: transform 0.3s ease-in-out;
z-index: 1000;
overflow-y: auto;
}
.sidebar-closed {
transform: translateX(-100%);
}
.nav-button {
width: 100%;
text-align: left;
justify-content: flex-start;
padding: 0.75rem 1rem;
border-radius: 0.375rem;
margin-bottom: 0.25rem;
background-color: transparent;
transition: background-color 0.15s ease-in-out;
}
.nav-button:hover {
background-color: #f3f4f6;
}
.nav-button.bg-blue-50 {
background-color: #eff6ff !important;
color: #1d4ed8 !important;
}

View file

@ -1,30 +0,0 @@
<!-- Sidebar Navigation -->
<aside class="sidebar" [class.sidebar-closed]="!opened()">
<!-- Logo/Brand -->
<div class="p-6 border-b border-gray-200">
<h2 class="text-xl font-bold text-gray-900">
📈 Trading Bot
</h2>
<p class="text-sm text-gray-600 mt-1">
Real-time Dashboard
</p>
</div>
<!-- Navigation Menu -->
<nav class="p-6">
<div class="space-y-2">
@for (item of navigationItems; track item.route) {
<button
mat-button
class="nav-button"
[class.bg-blue-50]="item.active"
[class.text-blue-700]="item.active"
[class.text-gray-600]="!item.active"
(click)="onNavigationClick(item.route)">
<mat-icon class="mr-3">{{ item.icon }}</mat-icon>
{{ item.label }}
</button>
}
</div>
</nav>
</aside>

View file

@ -1,57 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, input, output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatSidenavModule } from '@angular/material/sidenav';
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs/operators';
export interface NavigationItem {
label: string;
icon: string;
route: string;
active?: boolean;
}
@Component({
selector: 'app-sidebar',
standalone: true,
imports: [CommonModule, MatSidenavModule, MatButtonModule, MatIconModule],
templateUrl: './sidebar.component.html',
styleUrl: './sidebar.component.css',
})
export class SidebarComponent {
opened = input<boolean>(true);
navigationItemClick = output<string>();
protected navigationItems: NavigationItem[] = [
{ label: 'Dashboard', icon: 'dashboard', route: '/dashboard', active: true },
{ label: 'Market Data', icon: 'trending_up', route: '/market-data' },
{ label: 'Exchanges', icon: 'account_balance', route: '/exchanges' },
{ label: 'Portfolio', icon: 'account_balance_wallet', route: '/portfolio' },
{ label: 'Strategies', icon: 'psychology', route: '/strategies' },
{ label: 'Risk Management', icon: 'security', route: '/risk-management' },
{ label: 'Settings', icon: 'settings', route: '/settings' },
];
constructor(private router: Router) {
// Listen to route changes to update active state
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
this.updateActiveRoute(event.urlAfterRedirects);
});
}
onNavigationClick(route: string) {
this.navigationItemClick.emit(route);
this.router.navigate([route]);
this.updateActiveRoute(route);
}
private updateActiveRoute(currentRoute: string) {
this.navigationItems.forEach(item => {
item.active = item.route === currentRoute;
});
}
}

View file

@ -1,48 +0,0 @@
/* Dashboard specific styles */
.portfolio-card {
background-color: white !important;
border-radius: 0.5rem;
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
border: 1px solid #f3f4f6;
padding: 1.5rem;
}
.metric-value {
font-size: 1.5rem;
font-weight: 700;
}
.metric-label {
font-size: 0.875rem;
color: #6b7280;
margin-top: 0.25rem;
}
.metric-change-positive {
color: #16a34a;
font-weight: 500;
}
.metric-change-negative {
color: #dc2626;
font-weight: 500;
}
.status-chip-active {
background-color: #dcfce7;
color: #166534;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
display: inline-block;
}
.status-chip-medium {
background-color: #dbeafe;
color: #1e40af;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
}

View file

@ -1,154 +0,0 @@
<!-- Portfolio Summary Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-6">
<!-- Total Portfolio Value -->
<mat-card class="portfolio-card">
<div class="flex items-center justify-between">
<div>
<p class="metric-label">Portfolio Value</p>
<p class="metric-value text-gray-900">
${{ portfolioValue().toLocaleString('en-US', {minimumFractionDigits: 2}) }}
</p>
</div>
<mat-icon class="text-blue-600 text-3xl">account_balance_wallet</mat-icon>
</div>
</mat-card>
<!-- Day Change -->
<mat-card class="portfolio-card">
<div class="flex items-center justify-between">
<div>
<p class="metric-label">Day Change</p>
<p class="metric-value"
[class.metric-change-positive]="dayChange() > 0"
[class.metric-change-negative]="dayChange() < 0">
${{ dayChange().toLocaleString('en-US', {minimumFractionDigits: 2}) }}
</p>
<p class="text-sm font-medium"
[class.metric-change-positive]="dayChangePercent() > 0"
[class.metric-change-negative]="dayChangePercent() < 0">
{{ dayChangePercent() > 0 ? '+' : '' }}{{ dayChangePercent().toFixed(2) }}%
</p>
</div>
<mat-icon
class="text-3xl"
[class.metric-change-positive]="dayChange() > 0"
[class.metric-change-negative]="dayChange() < 0">
{{ dayChange() > 0 ? 'trending_up' : 'trending_down' }}
</mat-icon>
</div>
</mat-card>
<!-- Active Strategies -->
<mat-card class="portfolio-card">
<div class="flex items-center justify-between">
<div>
<p class="metric-label">Active Strategies</p>
<p class="metric-value text-gray-900">3</p>
<span class="status-chip-active">Running</span>
</div>
<mat-icon class="text-purple-600 text-3xl">psychology</mat-icon>
</div>
</mat-card>
<!-- Risk Level -->
<mat-card class="portfolio-card">
<div class="flex items-center justify-between">
<div>
<p class="metric-label">Risk Level</p>
<p class="metric-value text-green-600">Low</p>
<span class="status-chip-medium">Moderate</span>
</div>
<mat-icon class="text-green-600 text-3xl">security</mat-icon>
</div>
</mat-card>
</div>
<!-- Market Data Table -->
<mat-card class="p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Market Watchlist</h3>
<button mat-raised-button color="primary">
<mat-icon>refresh</mat-icon>
Refresh
</button>
</div>
<div class="overflow-x-auto">
<table mat-table [dataSource]="marketData()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
<td mat-cell *matCellDef="let stock" class="font-semibold text-gray-900">{{ stock.symbol }}</td>
</ng-container>
<!-- Price Column -->
<ng-container matColumnDef="price">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Price</th>
<td mat-cell *matCellDef="let stock" class="text-right font-medium text-gray-900">
${{ stock.price.toFixed(2) }}
</td>
</ng-container>
<!-- Change Column -->
<ng-container matColumnDef="change">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Change</th>
<td mat-cell *matCellDef="let stock"
class="text-right font-medium"
[class.text-green-600]="stock.change > 0"
[class.text-red-600]="stock.change < 0">
{{ stock.change > 0 ? '+' : '' }}${{ stock.change.toFixed(2) }}
</td>
</ng-container>
<!-- Change Percent Column -->
<ng-container matColumnDef="changePercent">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Change %</th>
<td mat-cell *matCellDef="let stock"
class="text-right font-medium"
[class.text-green-600]="stock.changePercent > 0"
[class.text-red-600]="stock.changePercent < 0">
{{ stock.changePercent > 0 ? '+' : '' }}{{ stock.changePercent.toFixed(2) }}%
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-card>
<!-- Tabs for Additional Content -->
<mat-tab-group>
<mat-tab label="Chart">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">show_chart</mat-icon>
<p class="mb-4">Chart visualization will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="Orders">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">receipt_long</mat-icon>
<p class="mb-4">Order history and management will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="Analytics">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">analytics</mat-icon>
<p class="mb-4">Advanced analytics and performance metrics will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
</mat-tab-group>

View file

@ -1,44 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
export interface MarketDataItem {
symbol: string;
price: number;
change: number;
changePercent: number;
}
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatTabsModule,
MatButtonModule,
MatIconModule,
MatTableModule,
],
templateUrl: './dashboard.component.html',
styleUrl: './dashboard.component.css',
})
export class DashboardComponent {
// Mock data for the dashboard
protected marketData = signal<MarketDataItem[]>([
{ symbol: 'AAPL', price: 192.53, change: 2.41, changePercent: 1.27 },
{ symbol: 'GOOGL', price: 138.21, change: -1.82, changePercent: -1.3 },
{ symbol: 'MSFT', price: 378.85, change: 4.12, changePercent: 1.1 },
{ symbol: 'TSLA', price: 248.42, change: -3.21, changePercent: -1.28 },
]);
protected portfolioValue = signal(125420.5);
protected dayChange = signal(2341.2);
protected dayChangePercent = signal(1.9);
protected displayedColumns: string[] = ['symbol', 'price', 'change', 'changePercent'];
}

View file

@ -1,39 +0,0 @@
.dialog-content {
display: flex;
flex-direction: column;
gap: 1rem;
min-width: 400px;
padding: 1rem 0;
}
.dialog-description {
color: #6b7280;
margin: 0 0 1rem 0;
font-size: 0.95rem;
}
.full-width {
width: 100%;
}
mat-dialog-actions {
padding: 1rem 0 0 0;
gap: 0.5rem;
}
/* Form field spacing */
mat-form-field + mat-form-field {
margin-top: 0.5rem;
}
/* Custom validation styles */
.mat-mdc-form-field.mat-form-field-invalid .mat-mdc-text-field-wrapper {
background-color: rgba(244, 67, 54, 0.04);
}
/* Dialog title styling */
h2[mat-dialog-title] {
margin: 0 0 1rem 0;
color: #1f2937;
font-weight: 600;
}

View file

@ -1,60 +0,0 @@
<h2 mat-dialog-title>Add Source Mapping</h2>
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<mat-dialog-content>
<div class="dialog-content">
<p class="dialog-description">
Add a source mapping for exchange: <strong>{{ data.masterExchangeId }}</strong>
</p>
<!-- Source Selection -->
<mat-form-field appearance="outline" class="full-width">
<mat-label>Data Source</mat-label>
<mat-select formControlName="sourceName" required>
@for (source of availableSourcesFiltered; track source.value) {
<mat-option [value]="source.value">{{ source.label }}</mat-option>
}
</mat-select>
<mat-error>Please select a data source</mat-error>
</mat-form-field>
<!-- Custom Source Name (shown when 'custom' is selected) -->
@if (isCustomSource) {
<mat-form-field appearance="outline" class="full-width">
<mat-label>Custom Source Name</mat-label>
<input matInput formControlName="sourceName_custom" required />
<mat-error>Please enter a custom source name</mat-error>
</mat-form-field>
}
<!-- Source ID -->
<mat-form-field appearance="outline" class="full-width">
<mat-label>Source Exchange ID</mat-label>
<input matInput formControlName="sourceId" required />
<mat-hint>The unique identifier for this exchange in the source system</mat-hint>
<mat-error>Please enter the source exchange ID</mat-error>
</mat-form-field>
<!-- Optional Name -->
<mat-form-field appearance="outline" class="full-width">
<mat-label>Display Name (Optional)</mat-label>
<input matInput formControlName="name" />
<mat-hint>Human-readable name for this exchange mapping</mat-hint>
</mat-form-field>
<!-- Optional Description -->
<mat-form-field appearance="outline" class="full-width">
<mat-label>Description (Optional)</mat-label>
<textarea matInput formControlName="description" rows="3"></textarea>
<mat-hint>Additional details about this exchange mapping</mat-hint>
</mat-form-field>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button type="button" (click)="onCancel()">Cancel</button>
<button mat-raised-button color="primary" type="submit" [disabled]="!form.valid">
Add Mapping
</button>
</mat-dialog-actions>
</form>

View file

@ -1,116 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, Inject } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
export interface AddMappingDialogData {
masterExchangeId: string;
existingSources: string[];
}
export interface AddMappingResult {
sourceName: string;
sourceData: {
id: string;
name?: string;
description?: string;
};
}
@Component({
selector: 'app-add-mapping-dialog',
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatDialogModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
],
templateUrl: './add-mapping-dialog.component.html',
styleUrl: './add-mapping-dialog.component.css',
})
export class AddMappingDialogComponent {
form: FormGroup;
availableSources = [
{ value: 'ib', label: 'Interactive Brokers' },
{ value: 'yahoo', label: 'Yahoo Finance' },
{ value: 'alpha', label: 'Alpha Vantage' },
{ value: 'polygon', label: 'Polygon.io' },
{ value: 'finnhub', label: 'Finnhub' },
{ value: 'custom', label: 'Custom Source' },
];
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<AddMappingDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: AddMappingDialogData
) {
this.form = this.fb.group({
sourceName: ['', [Validators.required]],
sourceId: ['', [Validators.required]],
sourceName_custom: [''],
name: [''],
description: [''],
});
// Add custom validator for custom source name
this.form.get('sourceName')?.valueChanges.subscribe(value => {
const customNameControl = this.form.get('sourceName_custom');
if (value === 'custom') {
customNameControl?.setValidators([Validators.required]);
} else {
customNameControl?.clearValidators();
}
customNameControl?.updateValueAndValidity();
});
}
get availableSourcesFiltered() {
return this.availableSources.filter(
source => !this.data.existingSources.includes(source.value)
);
}
get isCustomSource() {
return this.form.get('sourceName')?.value === 'custom';
}
onSubmit() {
if (this.form.valid) {
let sourceName = this.form.get('sourceName')?.value;
// If custom source, use the custom name
if (sourceName === 'custom') {
sourceName = this.form.get('sourceName_custom')?.value;
// Validate custom source name
if (!sourceName || sourceName.trim() === '') {
this.form.get('sourceName_custom')?.setErrors({ required: true });
return;
}
}
const result: AddMappingResult = {
sourceName,
sourceData: {
id: this.form.get('sourceId')?.value,
name: this.form.get('name')?.value || undefined,
description: this.form.get('description')?.value || undefined,
},
};
this.dialogRef.close(result);
}
}
onCancel() {
this.dialogRef.close();
}
}

View file

@ -1,122 +0,0 @@
.exchanges-container {
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.page-header {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 2rem;
}
.page-title {
font-size: 2rem;
font-weight: 600;
color: #1f2937;
margin: 0;
}
.page-description {
color: #6b7280;
margin: 0;
font-size: 1rem;
}
.page-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem;
gap: 1rem;
}
.exchanges-card {
margin-bottom: 2rem;
}
.exchanges-table {
width: 100%;
}
.exchange-id {
font-family: 'Monaco', 'Consolas', monospace;
font-weight: 600;
color: #1f2937;
}
.source-mappings {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.no-mappings {
color: #9ca3af;
font-style: italic;
}
.source-name {
font-weight: 600;
margin-right: 0.5rem;
}
.source-id {
color: #6b7280;
font-family: 'Monaco', 'Consolas', monospace;
font-size: 0.9em;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem;
text-align: center;
gap: 1rem;
}
.empty-icon {
font-size: 4rem;
color: #d1d5db;
width: 4rem;
height: 4rem;
}
.empty-state h3 {
color: #1f2937;
margin: 0;
}
.empty-state p {
color: #6b7280;
margin: 0;
}
/* Responsive design */
@media (max-width: 768px) {
.exchanges-container {
padding: 1rem;
}
.page-header {
text-align: center;
}
.page-actions {
justify-content: center;
}
.exchanges-table {
font-size: 0.9rem;
}
}

View file

@ -1,88 +0,0 @@
<!-- Exchanges Page -->
<div class="exchanges-container">
<!-- Page Header -->
<div class="page-header">
<h1 class="page-title">Exchange Management</h1>
<p class="page-description">
Manage master exchanges and their source mappings across different data providers.
</p>
<div class="page-actions">
<button mat-raised-button color="primary" (click)="syncExchanges()">
<mat-icon>sync</mat-icon>
Sync Exchanges
</button>
</div>
</div>
<!-- Loading State -->
<div *ngIf="loading" class="loading-container">
<mat-progress-spinner></mat-progress-spinner>
<p>Loading exchanges...</p>
</div>
<!-- Exchanges Table -->
<mat-card *ngIf="!loading" class="exchanges-card">
<mat-card-header>
<mat-card-title>Master Exchanges ({{ exchanges.length }})</mat-card-title>
</mat-card-header>
<mat-card-content>
<table mat-table [dataSource]="exchanges" class="exchanges-table">
<!-- Exchange ID Column -->
<ng-container matColumnDef="masterExchangeId">
<th mat-header-cell *matHeaderCellDef>Exchange ID</th>
<td mat-cell *matCellDef="let exchange">
<span class="exchange-id">{{ exchange.masterExchangeId }}</span>
</td>
</ng-container>
<!-- Source Mappings Column -->
<ng-container matColumnDef="sourceMappings">
<th mat-header-cell *matHeaderCellDef>Source Mappings</th>
<td mat-cell *matCellDef="let exchange">
<div class="source-mappings">
@if (getSourceMappingKeys(exchange.sourceMappings).length === 0) {
<span class="no-mappings">No mappings</span>
} @else {
<mat-chip-set>
@for (source of getSourceMappingKeys(exchange.sourceMappings); track source) {
<mat-chip-option>
<span class="source-name">{{ source }}</span>
<span class="source-id">{{ exchange.sourceMappings[source]?.id }}</span>
</mat-chip-option>
}
</mat-chip-set>
}
</div>
</td>
</ng-container>
<!-- Actions Column -->
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>Actions</th>
<td mat-cell *matCellDef="let exchange">
<button mat-button color="primary" (click)="openAddMappingDialog(exchange)">
<mat-icon>add</mat-icon>
Add Mapping
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
@if (exchanges.length === 0 && !loading) {
<div class="empty-state">
<mat-icon class="empty-icon">account_balance</mat-icon>
<h3>No exchanges found</h3>
<p>Start by syncing exchanges from your data providers.</p>
<button mat-raised-button color="primary" (click)="syncExchanges()">
<mat-icon>sync</mat-icon>
Sync Exchanges
</button>
</div>
}
</mat-card-content>
</mat-card>
</div>

View file

@ -1,110 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatChipsModule } from '@angular/material/chips';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { ExchangeService } from '../../services/exchange.service';
import { AddMappingDialogComponent } from './dialogs/add-mapping-dialog.component';
export interface SourceMapping {
id: string;
name?: string;
description?: string;
[key: string]: unknown;
}
export interface MasterExchange {
masterExchangeId: string;
sourceMappings: Record<string, SourceMapping>;
createdAt?: Date;
updatedAt?: Date;
}
@Component({
selector: 'app-exchanges',
standalone: true,
imports: [
CommonModule,
MatButtonModule,
MatCardModule,
MatDialogModule,
MatIconModule,
MatTableModule,
MatChipsModule,
MatProgressSpinnerModule,
MatSnackBarModule,
],
templateUrl: './exchanges.component.html',
styleUrl: './exchanges.component.css',
})
export class ExchangesComponent implements OnInit {
exchanges: MasterExchange[] = [];
loading = false;
displayedColumns: string[] = ['masterExchangeId', 'sourceMappings', 'actions'];
constructor(
private exchangeService: ExchangeService,
private dialog: MatDialog,
private snackBar: MatSnackBar
) {}
ngOnInit() {
this.loadExchanges();
}
async loadExchanges() {
this.loading = true;
try {
const response = await this.exchangeService.getAllExchanges();
this.exchanges = response.data;
} catch {
this.snackBar.open('Error loading exchanges', 'Close', { duration: 3000 });
} finally {
this.loading = false;
}
}
getSourceMappingKeys(sourceMappings: Record<string, SourceMapping>): string[] {
return Object.keys(sourceMappings || {});
}
openAddMappingDialog(exchange: MasterExchange) {
const dialogRef = this.dialog.open(AddMappingDialogComponent, {
width: '500px',
data: {
masterExchangeId: exchange.masterExchangeId,
existingSources: this.getSourceMappingKeys(exchange.sourceMappings),
},
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.addSourceMapping(exchange.masterExchangeId, result.sourceName, result.sourceData);
}
});
}
async addSourceMapping(masterExchangeId: string, sourceName: string, sourceData: SourceMapping) {
try {
await this.exchangeService.addSourceMapping(masterExchangeId, sourceName, sourceData);
this.snackBar.open('Source mapping added successfully', 'Close', { duration: 3000 });
this.loadExchanges(); // Refresh the list
} catch {
this.snackBar.open('Error adding source mapping', 'Close', { duration: 3000 });
}
}
async syncExchanges() {
try {
await this.exchangeService.syncExchanges();
this.snackBar.open('Exchange sync initiated', 'Close', { duration: 3000 });
} catch {
this.snackBar.open('Error syncing exchanges', 'Close', { duration: 3000 });
}
}
}

View file

@ -1 +0,0 @@
/* Market Data specific styles */

View file

@ -1,172 +0,0 @@
<div class="space-y-6"> <!-- Page Header -->
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Market Data</h1>
<p class="text-gray-600 mt-1">Real-time market information and analytics</p>
</div>
<button mat-raised-button color="primary" (click)="refreshData()" [disabled]="isLoading()">
<mat-icon>refresh</mat-icon>
@if (isLoading()) {
<mat-spinner diameter="20" class="mr-2"></mat-spinner>
}
Refresh Data
</button>
</div>
<!-- Market Overview Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Market Status</p>
<p class="text-lg font-semibold text-green-600">Open</p>
</div>
<mat-icon class="text-green-600 text-3xl">schedule</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600">Active Instruments</p>
<p class="text-lg font-semibold text-gray-900">{{ marketData().length }}</p>
</div>
<mat-icon class="text-blue-600 text-3xl">trending_up</mat-icon>
</div>
</mat-card>
<mat-card class="p-6">
<div class="flex items-center justify-between"> <div>
<p class="text-sm text-gray-600">Last Update</p>
<p class="text-lg font-semibold text-gray-900">{{ currentTime() }}</p>
</div>
<mat-icon class="text-purple-600 text-3xl">access_time</mat-icon>
</div>
</mat-card>
</div>
<!-- Market Data Table -->
<mat-card class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900">Live Market Data</h3>
<div class="flex gap-2">
<button mat-button>
<mat-icon>filter_list</mat-icon>
Filter
</button>
<button mat-button>
<mat-icon>file_download</mat-icon>
Export
</button>
</div>
</div>
@if (isLoading()) {
<div class="flex justify-center items-center py-8">
<mat-spinner diameter="40"></mat-spinner>
<span class="ml-3 text-gray-600">Loading market data...</span>
</div>
} @else if (error()) {
<div class="text-center py-8">
<mat-icon class="text-red-500 text-4xl">error</mat-icon>
<p class="text-red-600 mt-2">{{ error() }}</p>
<button mat-button color="primary" (click)="refreshData()" class="mt-2">
<mat-icon>refresh</mat-icon>
Try Again
</button>
</div>
} @else {
<div class="overflow-x-auto">
<table mat-table [dataSource]="marketData()" class="mat-elevation-z0 w-full">
<!-- Symbol Column -->
<ng-container matColumnDef="symbol">
<th mat-header-cell *matHeaderCellDef class="font-medium text-gray-900">Symbol</th>
<td mat-cell *matCellDef="let stock" class="font-semibold text-gray-900">{{ stock.symbol }}</td>
</ng-container>
<!-- Price Column -->
<ng-container matColumnDef="price">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Price</th>
<td mat-cell *matCellDef="let stock" class="text-right font-medium text-gray-900">
${{ stock.price.toFixed(2) }}
</td>
</ng-container>
<!-- Change Column -->
<ng-container matColumnDef="change">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Change</th>
<td mat-cell *matCellDef="let stock"
class="text-right font-medium"
[class.text-green-600]="stock.change > 0"
[class.text-red-600]="stock.change < 0">
{{ stock.change > 0 ? '+' : '' }}${{ stock.change.toFixed(2) }}
</td>
</ng-container>
<!-- Change Percent Column -->
<ng-container matColumnDef="changePercent">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Change %</th>
<td mat-cell *matCellDef="let stock"
class="text-right font-medium"
[class.text-green-600]="stock.changePercent > 0"
[class.text-red-600]="stock.changePercent < 0">
{{ stock.changePercent > 0 ? '+' : '' }}{{ stock.changePercent.toFixed(2) }}%
</td>
</ng-container>
<!-- Volume Column -->
<ng-container matColumnDef="volume">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Volume</th>
<td mat-cell *matCellDef="let stock" class="text-right text-gray-600">
{{ stock.volume.toLocaleString() }}
</td>
</ng-container>
<!-- Market Cap Column -->
<ng-container matColumnDef="marketCap">
<th mat-header-cell *matHeaderCellDef class="text-right font-medium text-gray-900">Market Cap</th>
<td mat-cell *matCellDef="let stock" class="text-right text-gray-600">
${{ stock.marketCap }}
</td>
</ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
}
</mat-card>
<!-- Market Analytics Tabs -->
<mat-tab-group>
<mat-tab label="Technical Analysis">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">bar_chart</mat-icon>
<p class="mb-4">Technical analysis charts and indicators will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="Market Trends">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">timeline</mat-icon>
<p class="mb-4">Market trends and sector analysis will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
<mat-tab label="News & Events">
<div class="p-6">
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">article</mat-icon>
<p class="mb-4">Market news and economic events will be implemented here</p>
</div>
</mat-card>
</div>
</mat-tab>
</mat-tab-group>
</div>

View file

@ -1,205 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { MatTableModule } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { interval, Subscription } from 'rxjs';
import { ApiService } from '../../services/api.service';
import { WebSocketService } from '../../services/websocket.service';
export interface ExtendedMarketData {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
marketCap: string;
high52Week: number;
low52Week: number;
}
@Component({
selector: 'app-market-data',
standalone: true,
imports: [
CommonModule,
MatCardModule,
MatButtonModule,
MatIconModule,
MatTableModule,
MatTabsModule,
MatProgressSpinnerModule,
MatSnackBarModule,
],
templateUrl: './market-data.component.html',
styleUrl: './market-data.component.css',
})
export class MarketDataComponent implements OnInit, OnDestroy {
private apiService = inject(ApiService);
private webSocketService = inject(WebSocketService);
private snackBar = inject(MatSnackBar);
private subscriptions: Subscription[] = [];
protected marketData = signal<ExtendedMarketData[]>([]);
protected currentTime = signal<string>(new Date().toLocaleTimeString());
protected isLoading = signal<boolean>(true);
protected error = signal<string | null>(null);
protected displayedColumns: string[] = [
'symbol',
'price',
'change',
'changePercent',
'volume',
'marketCap',
];
ngOnInit() {
// Update time every second
const timeSubscription = interval(1000).subscribe(() => {
this.currentTime.set(new Date().toLocaleTimeString());
});
this.subscriptions.push(timeSubscription);
// Load initial market data
this.loadMarketData();
// Subscribe to real-time market data updates
const wsSubscription = this.webSocketService.getMarketDataUpdates().subscribe({
next: update => {
this.updateMarketData(update);
},
error: err => {
console.error('WebSocket market data error:', err);
},
});
this.subscriptions.push(wsSubscription);
// Fallback: Refresh market data every 30 seconds if WebSocket fails
const dataSubscription = interval(30000).subscribe(() => {
if (!this.webSocketService.isConnected()) {
this.loadMarketData();
}
});
this.subscriptions.push(dataSubscription);
}
ngOnDestroy() {
this.subscriptions.forEach(sub => sub.unsubscribe());
}
private loadMarketData() {
this.apiService.getMarketData().subscribe({
next: response => {
// Convert MarketData to ExtendedMarketData with mock extended properties
const extendedData: ExtendedMarketData[] = response.data.map(item => ({
...item,
marketCap: this.getMockMarketCap(item.symbol),
high52Week: item.price * 1.3, // Mock 52-week high (30% above current)
low52Week: item.price * 0.7, // Mock 52-week low (30% below current)
}));
this.marketData.set(extendedData);
this.isLoading.set(false);
this.error.set(null);
},
error: err => {
console.error('Failed to load market data:', err);
this.error.set('Failed to load market data');
this.isLoading.set(false);
this.snackBar.open('Failed to load market data', 'Dismiss', { duration: 5000 });
// Use mock data as fallback
this.marketData.set(this.getMockData());
},
});
}
private getMockMarketCap(symbol: string): string {
const marketCaps: { [key: string]: string } = {
AAPL: '2.98T',
GOOGL: '1.78T',
MSFT: '3.08T',
TSLA: '789.2B',
AMZN: '1.59T',
};
return marketCaps[symbol] || '1.00T';
}
private getMockData(): ExtendedMarketData[] {
return [
{
symbol: 'AAPL',
price: 192.53,
change: 2.41,
changePercent: 1.27,
volume: 45230000,
marketCap: '2.98T',
high52Week: 199.62,
low52Week: 164.08,
},
{
symbol: 'GOOGL',
price: 2847.56,
change: -12.34,
changePercent: -0.43,
volume: 12450000,
marketCap: '1.78T',
high52Week: 3030.93,
low52Week: 2193.62,
},
{
symbol: 'MSFT',
price: 415.26,
change: 8.73,
changePercent: 2.15,
volume: 23180000,
marketCap: '3.08T',
high52Week: 468.35,
low52Week: 309.45,
},
{
symbol: 'TSLA',
price: 248.5,
change: -5.21,
changePercent: -2.05,
volume: 89760000,
marketCap: '789.2B',
high52Week: 299.29,
low52Week: 152.37,
},
{
symbol: 'AMZN',
price: 152.74,
change: 3.18,
changePercent: 2.12,
volume: 34520000,
marketCap: '1.59T',
high52Week: 170.17,
low52Week: 118.35,
},
];
}
refreshData() {
this.isLoading.set(true);
this.loadMarketData();
}
private updateMarketData(update: any) {
const currentData = this.marketData();
const updatedData = currentData.map(item => {
if (item.symbol === update.symbol) {
return {
...item,
price: update.price,
change: update.change,
changePercent: update.changePercent,
volume: update.volume,
};
}
return item;
});
this.marketData.set(updatedData);
}
}

View file

@ -1 +0,0 @@
/* Settings specific styles */

View file

@ -1,15 +0,0 @@
<div class="space-y-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-bold text-gray-900">Settings</h1>
<p class="text-gray-600 mt-1">Configure application preferences and system settings</p>
</div>
</div>
<mat-card class="p-6 h-96 flex items-center">
<div class="text-center text-gray-500 w-full">
<mat-icon style="font-size: 4rem; width: 4rem; height: 4rem;">settings</mat-icon>
<p class="mb-4">Application settings and configuration will be implemented here</p>
</div>
</mat-card>
</div>

View file

@ -1,13 +0,0 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
@Component({
selector: 'app-settings',
standalone: true,
imports: [CommonModule, MatCardModule, MatIconModule],
templateUrl: './settings.component.html',
styleUrl: './settings.component.css',
})
export class SettingsComponent {}

View file

@ -1,104 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
export interface RiskThresholds {
maxPositionSize: number;
maxDailyLoss: number;
maxPortfolioRisk: number;
volatilityLimit: number;
}
export interface RiskEvaluation {
symbol: string;
positionValue: number;
positionRisk: number;
violations: string[];
riskLevel: 'LOW' | 'MEDIUM' | 'HIGH';
}
export interface MarketData {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
timestamp: string;
}
@Injectable({
providedIn: 'root',
})
export class ApiService {
private readonly baseUrls = {
riskGuardian: 'http://localhost:3002',
strategyOrchestrator: 'http://localhost:3003',
marketDataGateway: 'http://localhost:3001',
};
constructor(private http: HttpClient) {}
// Risk Guardian API
getRiskThresholds(): Observable<{ success: boolean; data: RiskThresholds }> {
return this.http.get<{ success: boolean; data: RiskThresholds }>(
`${this.baseUrls.riskGuardian}/api/risk/thresholds`
);
}
updateRiskThresholds(
thresholds: RiskThresholds
): Observable<{ success: boolean; data: RiskThresholds }> {
return this.http.put<{ success: boolean; data: RiskThresholds }>(
`${this.baseUrls.riskGuardian}/api/risk/thresholds`,
thresholds
);
}
evaluateRisk(params: {
symbol: string;
quantity: number;
price: number;
portfolioValue: number;
}): Observable<{ success: boolean; data: RiskEvaluation }> {
return this.http.post<{ success: boolean; data: RiskEvaluation }>(
`${this.baseUrls.riskGuardian}/api/risk/evaluate`,
params
);
}
getRiskHistory(): Observable<{ success: boolean; data: RiskEvaluation[] }> {
return this.http.get<{ success: boolean; data: RiskEvaluation[] }>(
`${this.baseUrls.riskGuardian}/api/risk/history`
);
}
// Strategy Orchestrator API
getStrategies(): Observable<{ success: boolean; data: any[] }> {
return this.http.get<{ success: boolean; data: any[] }>(
`${this.baseUrls.strategyOrchestrator}/api/strategies`
);
}
createStrategy(strategy: any): Observable<{ success: boolean; data: any }> {
return this.http.post<{ success: boolean; data: any }>(
`${this.baseUrls.strategyOrchestrator}/api/strategies`,
strategy
);
}
// Market Data Gateway API
getMarketData(
symbols: string[] = ['AAPL', 'GOOGL', 'MSFT', 'TSLA', 'AMZN']
): Observable<{ success: boolean; data: MarketData[] }> {
const symbolsParam = symbols.join(',');
return this.http.get<{ success: boolean; data: MarketData[] }>(
`${this.baseUrls.marketDataGateway}/api/market-data?symbols=${symbolsParam}`
);
}
// Health checks
checkServiceHealth(
service: 'riskGuardian' | 'strategyOrchestrator' | 'marketDataGateway'
): Observable<any> {
return this.http.get(`${this.baseUrls[service]}/health`);
}
}

View file

@ -1,89 +0,0 @@
import { Injectable } from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs';
import { filter } from 'rxjs/operators';
export interface BreadcrumbItem {
label: string;
url: string;
isActive: boolean;
}
@Injectable({
providedIn: 'root',
})
export class BreadcrumbService {
private breadcrumbsSubject = new BehaviorSubject<BreadcrumbItem[]>([]);
public breadcrumbs$ = this.breadcrumbsSubject.asObservable();
private routeLabels: Record<string, string> = {
'/dashboard': 'Dashboard',
'/market-data': 'Market Data',
'/exchanges': 'Exchanges',
'/settings': 'Settings',
};
constructor(private router: Router) {
this.router.events
.pipe(filter(event => event instanceof NavigationEnd))
.subscribe((event: NavigationEnd) => {
this.updateBreadcrumbs(event.url);
});
}
private updateBreadcrumbs(url: string) {
const pathSegments = url.split('/').filter(segment => segment);
const breadcrumbs: BreadcrumbItem[] = [];
// Always add home/dashboard as first breadcrumb
breadcrumbs.push({
label: 'Home',
url: '/dashboard',
isActive: url === '/dashboard' || url === '/',
});
// Build breadcrumbs for current path
let currentPath = '';
pathSegments.forEach((segment, index) => {
currentPath += `/${segment}`;
const isLast = index === pathSegments.length - 1;
// Skip if it's the dashboard route (already added as Home)
if (currentPath === '/dashboard') {
return;
}
const label = this.getRouteLabel(currentPath, segment);
breadcrumbs.push({
label,
url: currentPath,
isActive: isLast,
});
});
this.breadcrumbsSubject.next(breadcrumbs);
}
private getRouteLabel(path: string, segment: string): string {
// Check if we have a custom label for this path
if (this.routeLabels[path]) {
return this.routeLabels[path];
}
// Convert kebab-case to Title Case
return segment
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
public getCurrentPageTitle(): string {
const breadcrumbs = this.breadcrumbsSubject.value;
const activeBreadcrumb = breadcrumbs.find(item => item.isActive);
return activeBreadcrumb?.label || 'Dashboard';
}
public addRouteLabel(path: string, label: string) {
this.routeLabels[path] = label;
}
}

View file

@ -1,116 +0,0 @@
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
export interface SourceMapping {
id: string;
name?: string;
description?: string;
[key: string]: unknown;
}
export interface ExchangeResponse {
data: Array<{
masterExchangeId: string;
sourceMappings: Record<string, SourceMapping>;
createdAt?: Date;
updatedAt?: Date;
}>;
count: number;
status: string;
}
export interface AddMappingResponse {
status: string;
message: string;
data: {
masterExchangeId: string;
sourceName: string;
sourceData: SourceMapping;
};
}
export interface SyncResponse {
status: string;
message: string;
jobId: string;
operation: string;
}
export interface GetExchangeResponse {
status: string;
data: {
masterExchangeId: string;
sourceMappings: Record<string, SourceMapping>;
createdAt?: Date;
updatedAt?: Date;
};
}
export interface GetExchangesBySourceResponse {
data: Array<{
masterExchangeId: string;
sourceMapping: SourceMapping;
}>;
count: number;
status: string;
sourceName: string;
}
export interface GetSourceMappingResponse {
status: string;
data: {
masterExchangeId: string;
sourceName: string;
sourceId: string;
};
}
@Injectable({
providedIn: 'root',
})
export class ExchangeService {
private readonly baseUrl = 'http://localhost:2001/api/exchanges';
constructor(private http: HttpClient) {}
async getAllExchanges(): Promise<ExchangeResponse> {
return this.http.get<ExchangeResponse>(this.baseUrl).toPromise() as Promise<ExchangeResponse>;
}
async getExchange(masterExchangeId: string): Promise<GetExchangeResponse> {
return this.http
.get<GetExchangeResponse>(`${this.baseUrl}/${masterExchangeId}`)
.toPromise() as Promise<GetExchangeResponse>;
}
async addSourceMapping(
masterExchangeId: string,
sourceName: string,
sourceData: SourceMapping
): Promise<AddMappingResponse> {
return this.http
.post<AddMappingResponse>(`${this.baseUrl}/${masterExchangeId}/mappings`, {
sourceName,
sourceData,
})
.toPromise() as Promise<AddMappingResponse>;
}
async syncExchanges(): Promise<SyncResponse> {
return this.http
.post<SyncResponse>(`${this.baseUrl}/sync`, {})
.toPromise() as Promise<SyncResponse>;
}
async getExchangesBySource(sourceName: string): Promise<GetExchangesBySourceResponse> {
return this.http
.get<GetExchangesBySourceResponse>(`${this.baseUrl}/source/${sourceName}`)
.toPromise() as Promise<GetExchangesBySourceResponse>;
}
async getSourceMapping(sourceName: string, sourceId: string): Promise<GetSourceMappingResponse> {
return this.http
.get<GetSourceMappingResponse>(`${this.baseUrl}/mapping/${sourceName}/${sourceId}`)
.toPromise() as Promise<GetSourceMappingResponse>;
}
}

View file

@ -1,191 +0,0 @@
import { inject, Injectable, signal } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Subscription } from 'rxjs';
import { RiskAlert, WebSocketService } from './websocket.service';
export interface Notification {
id: string;
type: 'info' | 'warning' | 'error' | 'success';
title: string;
message: string;
timestamp: Date;
read: boolean;
}
@Injectable({
providedIn: 'root',
})
export class NotificationService {
private snackBar = inject(MatSnackBar);
private webSocketService = inject(WebSocketService);
private riskAlertsSubscription?: Subscription;
// Reactive state
public notifications = signal<Notification[]>([]);
public unreadCount = signal<number>(0);
constructor() {
this.initializeRiskAlerts();
}
private initializeRiskAlerts() {
// Subscribe to risk alerts from WebSocket
this.riskAlertsSubscription = this.webSocketService.getRiskAlerts().subscribe({
next: (alert: RiskAlert) => {
this.handleRiskAlert(alert);
},
error: err => {
console.error('Risk alert subscription error:', err);
},
});
}
private handleRiskAlert(alert: RiskAlert) {
const notification: Notification = {
id: alert.id,
type: this.mapSeverityToType(alert.severity),
title: `Risk Alert: ${alert.symbol}`,
message: alert.message,
timestamp: new Date(alert.timestamp),
read: false,
};
this.addNotification(notification);
this.showSnackBarAlert(notification);
}
private mapSeverityToType(severity: string): 'info' | 'warning' | 'error' | 'success' {
switch (severity) {
case 'HIGH':
return 'error';
case 'MEDIUM':
return 'warning';
case 'LOW':
return 'info';
default:
return 'info';
}
}
private showSnackBarAlert(notification: Notification) {
const actionText = notification.type === 'error' ? 'Review' : 'Dismiss';
const duration = notification.type === 'error' ? 10000 : 5000;
this.snackBar.open(`${notification.title}: ${notification.message}`, actionText, {
duration,
panelClass: [`snack-${notification.type}`],
});
}
// Public methods
addNotification(notification: Notification) {
const current = this.notifications();
const updated = [notification, ...current].slice(0, 50); // Keep only latest 50
this.notifications.set(updated);
this.updateUnreadCount();
}
markAsRead(notificationId: string) {
const current = this.notifications();
const updated = current.map(n => (n.id === notificationId ? { ...n, read: true } : n));
this.notifications.set(updated);
this.updateUnreadCount();
}
markAllAsRead() {
const current = this.notifications();
const updated = current.map(n => ({ ...n, read: true }));
this.notifications.set(updated);
this.updateUnreadCount();
}
clearNotification(notificationId: string) {
const current = this.notifications();
const updated = current.filter(n => n.id !== notificationId);
this.notifications.set(updated);
this.updateUnreadCount();
}
clearAllNotifications() {
this.notifications.set([]);
this.unreadCount.set(0);
}
private updateUnreadCount() {
const unread = this.notifications().filter(n => !n.read).length;
this.unreadCount.set(unread);
}
// Manual notification methods
showSuccess(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'success',
title,
message,
timestamp: new Date(),
read: false,
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 3000,
panelClass: ['snack-success'],
});
}
showError(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'error',
title,
message,
timestamp: new Date(),
read: false,
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 8000,
panelClass: ['snack-error'],
});
}
showWarning(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'warning',
title,
message,
timestamp: new Date(),
read: false,
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 5000,
panelClass: ['snack-warning'],
});
}
showInfo(title: string, message: string) {
const notification: Notification = {
id: this.generateId(),
type: 'info',
title,
message,
timestamp: new Date(),
read: false,
};
this.addNotification(notification);
this.snackBar.open(`${title}: ${message}`, 'Dismiss', {
duration: 4000,
panelClass: ['snack-info'],
});
}
private generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
ngOnDestroy() {
this.riskAlertsSubscription?.unsubscribe();
}
}

View file

@ -1,127 +0,0 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
export interface RiskAlert {
id: string;
symbol: string;
severity: 'low' | 'medium' | 'high' | 'critical';
message: string;
timestamp: string;
}
export interface MarketDataUpdate {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
timestamp: string;
}
interface WebSocketMessage {
type: string;
payload?: MarketDataUpdate | RiskAlert;
channel?: string;
}
@Injectable({
providedIn: 'root',
})
export class WebSocketService {
private socket?: WebSocket;
private connected = new BehaviorSubject<boolean>(false);
private marketDataSubject = new Subject<MarketDataUpdate>();
private riskAlertSubject = new Subject<RiskAlert>();
private readonly WS_URL = 'ws://localhost:3001/ws'; // Adjust based on your backend
constructor() {
this.connect();
}
private connect() {
try {
this.socket = new WebSocket(this.WS_URL);
this.socket.onopen = () => {
this.connected.next(true);
};
this.socket.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch {
// Handle parsing error silently
}
};
this.socket.onclose = () => {
this.connected.next(false);
// Reconnect after 5 seconds
setTimeout(() => this.connect(), 5000);
};
this.socket.onerror = () => {
this.connected.next(false);
};
} catch {
this.connected.next(false);
// Retry connection after 5 seconds
setTimeout(() => this.connect(), 5000);
}
}
private handleMessage(data: WebSocketMessage) {
switch (data.type) {
case 'marketData':
this.marketDataSubject.next(data.payload as MarketDataUpdate);
break;
case 'riskAlert':
this.riskAlertSubject.next(data.payload as RiskAlert);
break;
default:
// Unknown message type - ignore
}
}
public getMarketDataUpdates(): Observable<MarketDataUpdate> {
return this.marketDataSubject.asObservable();
}
public getRiskAlerts(): Observable<RiskAlert> {
return this.riskAlertSubject.asObservable();
}
public isConnected(): boolean {
return this.connected.value;
}
public getConnectionStatus(): Observable<boolean> {
return this.connected.asObservable();
}
public disconnect() {
if (this.socket) {
this.socket.close();
}
}
public subscribe(channel: string) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({
type: 'subscribe',
channel: channel
}));
}
}
public unsubscribe(channel: string) {
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
this.socket.send(JSON.stringify({
type: 'unsubscribe',
channel: channel
}));
}
}
}

View file

@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Trading Dashboard</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link rel="preconnect" href="https://fonts.gstatic.com">
</head>
<body class="mat-typography">
<app-root></app-root>
</body>
</html>

View file

@ -1,5 +0,0 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { App } from './app/app';
import { appConfig } from './app/app.config';
bootstrapApplication(App, appConfig).catch(err => console.error(err));

View file

@ -1,89 +0,0 @@
@import "tailwindcss";
/* Custom base styles */
html, body {
height: 100%;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background-color: #f9fafb;
}
/* Angular Material integration styles */
.mat-sidenav-container {
background-color: transparent;
}
.mat-sidenav {
border-radius: 0;
}
.mat-toolbar {
background-color: white;
color: #374151;
}
.mat-mdc-button.w-full {
width: 100%;
text-align: left;
justify-content: flex-start;
}
.mat-mdc-card {
border-radius: 8px;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
}
.mat-mdc-tab-group .mat-mdc-tab-header {
border-bottom: 1px solid #e5e7eb;
}
.mat-mdc-chip.chip-green {
background-color: #dcfce7 !important;
color: #166534 !important;
}
.mat-mdc-chip.chip-blue {
background-color: #dbeafe !important;
color: #1e40af !important;
}
.mat-mdc-table {
border-radius: 8px;
overflow: hidden;
}
.mat-mdc-header-row {
background-color: #f9fafb;
}
.mat-mdc-row:hover {
background-color: #f9fafb;
}
/* Dark mode overrides */
.dark .mat-toolbar {
background-color: #1f2937;
color: #f9fafb;
}
.dark .mat-mdc-tab-group .mat-mdc-tab-header {
border-bottom: 1px solid #4b5563;
}
.dark .mat-mdc-header-row {
background-color: #1f2937;
}
.dark .mat-mdc-row:hover {
background-color: #374151;
}
.dark .mat-mdc-card {
background-color: #1f2937;
color: #f9fafb;
}
.dark .mat-mdc-table {
background-color: #1f2937;
color: #f9fafb;
}

View file

@ -1,52 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{html,ts}",
],
theme: {
extend: {
colors: {
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
success: {
50: '#f0fdf4',
100: '#dcfce7',
200: '#bbf7d0',
300: '#86efac',
400: '#4ade80',
500: '#22c55e',
600: '#16a34a',
700: '#15803d',
800: '#166534',
900: '#14532d',
950: '#052e16',
},
danger: {
50: '#fef2f2',
100: '#fee2e2',
200: '#fecaca',
300: '#fca5a5',
400: '#f87171',
500: '#ef4444',
600: '#dc2626',
700: '#b91c1c',
800: '#991b1b',
900: '#7f1d1d',
950: '#450a0a',
},
},
},
},
plugins: [],
}

View file

@ -1,11 +0,0 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": []
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.spec.ts"]
}

View file

@ -1,32 +0,0 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "../../tsconfig.json",
"compileOnSave": false,
"compilerOptions": {
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"isolatedModules": true,
"experimentalDecorators": true,
"importHelpers": true,
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}

View file

@ -1,10 +0,0 @@
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/spec",
"types": ["jasmine"]
},
"include": ["src/**/*.ts"]
}

View file

@ -19,12 +19,15 @@ loadEnvVariables();
const app = new Hono();
// Add CORS middleware
app.use('*', cors({
app.use(
'*',
cors({
origin: ['http://localhost:4200', 'http://localhost:5173'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
credentials: true,
}));
})
);
const logger = getLogger('data-service');
const PORT = parseInt(process.env.DATA_SERVICE_PORT || '3002');

9
apps/web/.env.example Normal file
View file

@ -0,0 +1,9 @@
# API Configuration
VITE_API_BASE_URL=http://localhost:8080
VITE_DATA_SERVICE_URL=http://localhost:3001
VITE_PORTFOLIO_SERVICE_URL=http://localhost:3002
VITE_STRATEGY_SERVICE_URL=http://localhost:3003
VITE_EXECUTION_SERVICE_URL=http://localhost:3004
# Environment
VITE_NODE_ENV=development

95
apps/web/README.md Normal file
View file

@ -0,0 +1,95 @@
# Stock Bot Web Dashboard
A modern React web dashboard for the Stock Bot trading system.
## Features
- **Modern UI**: Built with React 18 and TypeScript
- **Tailwind CSS**: For beautiful, responsive styling
- **Headless UI**: Accessible components for dialogs and interactions
- **Heroicons**: Beautiful SVG icons
- **Responsive Design**: Works on desktop and mobile
- **Side Navigation**: Clean navigation with mobile-responsive sidebar
## Tech Stack
- **React 18** with TypeScript
- **Vite** for fast development and building
- **Tailwind CSS** for styling
- **Headless UI** for accessible components
- **Heroicons** for icons
- **Bun** for package management
## Getting Started
### Prerequisites
- Bun (for package management)
- Node.js 18+
### Installation
```bash
# Install dependencies
bun install
# Start development server
bun run dev
# Build for production
bun run build
# Preview production build
bun run preview
```
## Available Scripts
- `bun run dev` - Start development server
- `bun run build` - Build for production
- `bun run preview` - Preview production build
- `bun run lint` - Run ESLint
- `bun run type-check` - Run TypeScript type checking
## Project Structure
```
src/
├── App.tsx # Main app component with sidebar navigation
├── main.tsx # React app entry point
└── index.css # Global styles with Tailwind directives
```
## Dashboard Features
The dashboard includes:
- **Responsive Sidebar**: Navigation that works on desktop and mobile
- **Dashboard Overview**: Portfolio value, returns, and active strategies
- **Navigation Sections**:
- Dashboard (current page)
- Portfolio
- Analytics
- Reports
- Settings
## Development
The app is set up with:
- TypeScript for type safety
- ESLint for code quality
- Tailwind CSS for utility-first styling
- Headless UI for accessible components
- Hot module replacement for fast development
## Integration
This web app is part of the larger Stock Bot system and will integrate with:
- Data Service (for market data)
- Portfolio Service (for portfolio management)
- Strategy Service (for trading strategies)
- Execution Service (for trade execution)
The web dashboard will communicate with these services via REST APIs to provide a complete trading dashboard experience.

31
apps/web/eslint.config.js Normal file
View file

@ -0,0 +1,31 @@
import js from '@eslint/js';
import tseslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import globals from 'globals';
export default [
{
ignores: ['dist'],
},
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parser: tsParser,
},
plugins: {
'@typescript-eslint': tseslint,
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...js.configs.recommended.rules,
...tseslint.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
},
},
];

15
apps/web/index.html Normal file
View file

@ -0,0 +1,15 @@
<!doctype html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Stock Bot - Advanced Trading Dashboard" />
<title>Stock Bot - Trading Dashboard</title>
</head>
<body class="dark">
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

43
apps/web/package.json Normal file
View file

@ -0,0 +1,43 @@
{
"name": "@stock-bot/web",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@headlessui/react": "^1.7.17",
"@heroicons/react": "^2.0.18",
"@tanstack/react-table": "^8.21.3",
"@types/react-virtualized-auto-sizer": "^1.0.8",
"@types/react-window": "^1.8.8",
"clsx": "^2.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^7.6.2",
"react-virtualized-auto-sizer": "^1.0.26",
"react-window": "^1.8.11",
"react-window-infinite-loader": "^1.0.10",
"tailwind-merge": "^3.3.1"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"@vitejs/plugin-react": "^4.0.3",
"autoprefixer": "^10.4.14",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.27",
"tailwindcss": "^3.3.3",
"typescript": "^5.0.2",
"vite": "^4.4.5"
}
}

View file

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

4
apps/web/public/vite.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
<circle cx="12" cy="12" r="10" stroke="#1e40af" stroke-width="1" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 314 B

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>
);

View file

@ -0,0 +1,93 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
darkMode: 'class',
theme: {
extend: {
colors: {
// Dark theme color palette for trading/finance - All black backgrounds
background: {
DEFAULT: '#000000',
secondary: '#000000',
tertiary: '#000000',
},
surface: {
DEFAULT: '#000000',
secondary: '#0a0a0a',
tertiary: '#111111',
},
border: {
DEFAULT: '#1a1a1a',
secondary: '#2a2a2a',
},
text: {
primary: '#ffffff',
secondary: '#a1a1aa',
muted: '#71717a',
},
primary: {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
950: '#172554',
},
success: {
DEFAULT: '#10b981',
50: '#ecfdf5',
500: '#10b981',
600: '#059669',
},
danger: {
DEFAULT: '#ef4444',
50: '#fef2f2',
500: '#ef4444',
600: '#dc2626',
},
warning: {
DEFAULT: '#f59e0b',
50: '#fffbeb',
500: '#f59e0b',
600: '#d97706',
},
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
spacing: {
'0.5': '0.125rem', // 2px
'1.5': '0.375rem', // 6px
'2.5': '0.625rem', // 10px
'3.5': '0.875rem', // 14px
'4.5': '1.125rem', // 18px
'5.5': '1.375rem', // 22px
},
fontSize: {
'xs': ['0.6875rem', { lineHeight: '0.875rem' }], // 11px
'sm': ['0.75rem', { lineHeight: '1rem' }], // 12px
'base': ['0.8125rem', { lineHeight: '1.125rem' }], // 13px
'lg': ['0.875rem', { lineHeight: '1.25rem' }], // 14px
'xl': ['1rem', { lineHeight: '1.375rem' }], // 16px
'2xl': ['1.125rem', { lineHeight: '1.5rem' }], // 18px
'3xl': ['1.25rem', { lineHeight: '1.625rem' }], // 20px
},
lineHeight: {
'tight': '1.1',
'snug': '1.2',
'normal': '1.3',
'relaxed': '1.4',
},
},
},
plugins: [],
}

26
apps/web/tsconfig.json Normal file
View file

@ -0,0 +1,26 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

20
apps/web/turbo.json Normal file
View file

@ -0,0 +1,20 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
},
"type-check": {
"dependsOn": ["^type-check"]
}
}
}

17
apps/web/vite.config.ts Normal file
View file

@ -0,0 +1,17 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { fileURLToPath, URL } from 'node:url'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
server: {
port: 3000,
host: true
}
})

1218
bun.lock

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more