UI: ensure dark text on job detail and job history cards (avoid white-on-light backgrounds)\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
# Most Active Stocks Table Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Replace the StockViewer on `/stocks` with a table of most active stocks from Alpaca's screener API.
|
||||
|
||||
**Architecture:** Server proxy route calls Alpaca screener, React component fetches and auto-refreshes every 30s with clickable rows linking to `/analyze/TICKER`.
|
||||
|
||||
**Tech Stack:** React Router 7, React, TailwindCSS, fetch API, Alpaca Markets API
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Add MostActiveStock type to types.ts
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/types.ts`
|
||||
|
||||
- [ ] **Step 1: Add MostActiveStock interface**
|
||||
|
||||
Add to `app/types.ts` after the `AlpacaAccount` interface:
|
||||
|
||||
```typescript
|
||||
export interface MostActiveStock {
|
||||
symbol: string;
|
||||
name: string;
|
||||
price: number;
|
||||
changePercent: number;
|
||||
volume: number;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add app/types.ts
|
||||
git commit -m "types: add MostActiveStock interface"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Create API route for most active stocks
|
||||
|
||||
**Files:**
|
||||
- Create: `app/routes/api/stocks/most-actives.ts`
|
||||
|
||||
- [ ] **Step 1: Create the server proxy route**
|
||||
|
||||
Create `app/routes/api/stocks/most-actives.ts`:
|
||||
|
||||
```typescript
|
||||
import type { MostActiveStock } from "../../../types";
|
||||
|
||||
const ALPACA_API_KEY = process.env.ALPACA_API_KEY!;
|
||||
const ALPACA_SECRET_KEY = process.env.ALPACA_SECRET_KEY!;
|
||||
const ALPACA_DATA_URL = process.env.ALPACA_DATA_URL || "https://data.alpaca.markets";
|
||||
|
||||
export async function loader() {
|
||||
try {
|
||||
const response = await fetch(`${ALPACA_DATA_URL}/v1beta1/screener/stocks/most-actives`, {
|
||||
headers: {
|
||||
"APCA-API-KEY-ID": ALPACA_API_KEY,
|
||||
"APCA-API-SECRET-KEY": ALPACA_SECRET_KEY,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Alpaca API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const stocks: MostActiveStock[] = (data.most_actives || []).map((item: any) => ({
|
||||
symbol: item.symbol,
|
||||
name: item.name || item.symbol,
|
||||
price: parseFloat(item.price) || 0,
|
||||
changePercent: parseFloat(item.change_percent) || 0,
|
||||
volume: parseInt(item.volume) || 0,
|
||||
}));
|
||||
|
||||
return Response.json(stocks);
|
||||
} catch (error) {
|
||||
console.error("Most active stocks API error:", error);
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
return Response.json(
|
||||
{ error: `Failed to fetch most active stocks: ${message}` },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add app/routes/api/stocks/most-actives.ts
|
||||
git commit -m "feat: add most-actives API proxy route"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Register the new API route
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/routes.ts`
|
||||
|
||||
- [ ] **Step 1: Add route entry**
|
||||
|
||||
Add this line to `app/routes.ts` after the existing `api/stocks` route (line 11):
|
||||
|
||||
```typescript
|
||||
route("api/stocks/most-actives", "routes/api/stocks/most-actives.ts"),
|
||||
```
|
||||
|
||||
The routes array should look like:
|
||||
|
||||
```typescript
|
||||
export default [
|
||||
index("routes/landing.tsx"),
|
||||
route("api/alpaca/account", "routes/api/alpaca/account.ts"),
|
||||
route("api/alpaca/quote/:ticker", "routes/api/alpaca/quote.ts"),
|
||||
route("api/alpaca/orders", "routes/api/alpaca/orders.ts"),
|
||||
route("api/alpaca/positions", "routes/api/alpaca/positions.ts"),
|
||||
route("api/indicators", "routes/api/indicators.ts"),
|
||||
route("api/analyze", "routes/api/analyze.ts"),
|
||||
route("api/stocks", "routes/api/stocks/index.ts"),
|
||||
route("api/stocks/most-actives", "routes/api/stocks/most-actives.ts"),
|
||||
route("stocks", "routes/stocks.tsx"),
|
||||
route("analyze", "routes/analyze.tsx"),
|
||||
route("analyze/:ticker", "routes/analyze.ticker.tsx"),
|
||||
] satisfies RouteConfig;
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add app/routes.ts
|
||||
git commit -m "routes: register most-actives API endpoint"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Create MostActiveStocks component
|
||||
|
||||
**Files:**
|
||||
- Create: `app/components/MostActiveStocks.tsx`
|
||||
|
||||
- [ ] **Step 1: Create the component**
|
||||
|
||||
Create `app/components/MostActiveStocks.tsx`:
|
||||
|
||||
```typescript
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Link } from "react-router";
|
||||
import type { MostActiveStock } from "../types";
|
||||
|
||||
function formatVolume(vol: number): string {
|
||||
if (vol >= 1_000_000_000) return `${(vol / 1_000_000_000).toFixed(1)}B`;
|
||||
if (vol >= 1_000_000) return `${(vol / 1_000_000).toFixed(1)}M`;
|
||||
if (vol >= 1_000) return `${(vol / 1_000).toFixed(1)}K`;
|
||||
return vol.toString();
|
||||
}
|
||||
|
||||
function formatPrice(price: number): string {
|
||||
return `$${price.toFixed(2)}`;
|
||||
}
|
||||
|
||||
function formatChangePercent(pct: number): string {
|
||||
return `${pct >= 0 ? "+" : ""}${pct.toFixed(2)}%`;
|
||||
}
|
||||
|
||||
export default function MostActiveStocks() {
|
||||
const [stocks, setStocks] = useState<MostActiveStock[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
setError(null);
|
||||
const res = await fetch("/api/stocks/most-actives");
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || "Failed to fetch data");
|
||||
}
|
||||
const data = await res.json();
|
||||
setStocks(data);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : "Failed to fetch most active stocks.";
|
||||
setError(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData();
|
||||
const interval = setInterval(fetchData, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, [fetchData]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
|
||||
<div className="space-y-3">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div key={i} className="animate-pulse flex gap-4 py-3 border-b border-gray-100 last:border-0">
|
||||
<div className="h-5 bg-gray-200 rounded w-16" />
|
||||
<div className="h-5 bg-gray-200 rounded w-32" />
|
||||
<div className="h-5 bg-gray-200 rounded w-20" />
|
||||
<div className="h-5 bg-gray-200 rounded w-20" />
|
||||
<div className="h-5 bg-gray-200 rounded w-24" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error && stocks.length === 0) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-lg p-6 border border-gray-200">
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-red-600 text-sm mb-3">{error}</p>
|
||||
<button
|
||||
onClick={fetchData}
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-lg border border-gray-200 overflow-hidden">
|
||||
{error && (
|
||||
<div className="bg-red-50 border-b border-red-200 px-6 py-3">
|
||||
<p className="text-red-600 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="bg-gray-50 border-b border-gray-200">
|
||||
<th className="text-left px-6 py-4 text-sm font-semibold text-gray-600">Symbol</th>
|
||||
<th className="text-left px-6 py-4 text-sm font-semibold text-gray-600">Name</th>
|
||||
<th className="text-right px-6 py-4 text-sm font-semibold text-gray-600">Price</th>
|
||||
<th className="text-right px-6 py-4 text-sm font-semibold text-gray-600">Change %</th>
|
||||
<th className="text-right px-6 py-4 text-sm font-semibold text-gray-600">Volume</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stocks.map((stock) => (
|
||||
<tr key={stock.symbol} className="border-b border-gray-100 hover:bg-gray-50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<Link
|
||||
to={`/analyze/${stock.symbol}`}
|
||||
className="text-blue-600 font-semibold hover:text-blue-700 hover:underline"
|
||||
>
|
||||
{stock.symbol}
|
||||
</Link>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-gray-600">{stock.name}</td>
|
||||
<td className="px-6 py-4 text-right font-mono text-gray-900">{formatPrice(stock.price)}</td>
|
||||
<td className={`px-6 py-4 text-right font-mono font-medium ${stock.changePercent >= 0 ? "text-green-600" : "text-red-600"}`}>
|
||||
{formatChangePercent(stock.changePercent)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right text-gray-600">{formatVolume(stock.volume)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add app/components/MostActiveStocks.tsx
|
||||
git commit -m "feat: add MostActiveStocks table component with auto-refresh"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Update stocks.tsx page
|
||||
|
||||
**Files:**
|
||||
- Modify: `app/routes/stocks.tsx`
|
||||
|
||||
- [ ] **Step 1: Replace StockViewer with MostActiveStocks**
|
||||
|
||||
Replace the entire contents of `app/routes/stocks.tsx`:
|
||||
|
||||
```typescript
|
||||
import MostActiveStocks from "../components/MostActiveStocks";
|
||||
import Navbar from "../components/Navbar";
|
||||
|
||||
export default function Stocks() {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-blue-50">
|
||||
<Navbar />
|
||||
<div className="py-20">
|
||||
<div className="mx-auto max-w-7xl px-6 sm:px-8 lg:px-8">
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-4xl font-bold text-gray-900 mb-4">
|
||||
Most Active Stocks
|
||||
</h1>
|
||||
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
|
||||
Real-time view of the most actively traded stocks, auto-refreshing every 30 seconds.
|
||||
</p>
|
||||
</div>
|
||||
<MostActiveStocks />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add app/routes/stocks.tsx
|
||||
git commit -m "feat: replace StockViewer with MostActiveStocks on stocks page"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Verify and test
|
||||
|
||||
**Files:**
|
||||
- All modified files
|
||||
|
||||
- [ ] **Step 1: Run typecheck**
|
||||
|
||||
```bash
|
||||
npm run typecheck
|
||||
```
|
||||
|
||||
Expected: No type errors. If there are errors, fix them before proceeding.
|
||||
|
||||
- [ ] **Step 2: Run dev server and verify**
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Navigate to `http://localhost:5173/stocks` and verify:
|
||||
- Table loads with most active stocks
|
||||
- Symbol links navigate to `/analyze/TICKER`
|
||||
- Change % is color-coded (green for positive, red for negative)
|
||||
- Data auto-refreshes every 30 seconds
|
||||
- Loading skeleton shows on initial load
|
||||
- Error state shows with retry button if API fails
|
||||
|
||||
- [ ] **Step 3: Final commit (if any fixes needed)**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "fix: address typecheck/dev issues"
|
||||
```
|
||||
@@ -0,0 +1,72 @@
|
||||
y# Design: Most Active Stocks Table
|
||||
|
||||
**Date:** 2026-05-16
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
|
||||
Replace the current StockViewer on the `/stocks` page with a table displaying the most active stocks from Alpaca's screener API.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Server Route: `api/stocks/most-actives`
|
||||
- **File:** `app/routes/api/stocks/most-actives.ts`
|
||||
- **Method:** GET (loader)
|
||||
- **Function:** Proxies request to `https://data.alpaca.markets/v1beta1/screener/stocks/most-actives`
|
||||
- **Response:** Normalized JSON array with fields: `symbol`, `name`, `price`, `changePercent`, `volume`
|
||||
- **Auth:** Uses server-side `ALPACA_API_KEY` and `ALPACA_SECRET_KEY`
|
||||
|
||||
### Component: `MostActiveStocks`
|
||||
- **File:** `app/components/MostActiveStocks.tsx`
|
||||
- **Behavior:**
|
||||
- Fetches from `/api/stocks/most-actives` on mount
|
||||
- Auto-refreshes every 30 seconds via `setInterval`
|
||||
- Cleans up interval on unmount
|
||||
- **States:** loading (skeleton rows), success (table), error (red banner with retry button)
|
||||
|
||||
### Page: `stocks.tsx`
|
||||
- Replaces `<StockViewer />` with `<MostActiveStocks />`
|
||||
- Updates heading and description text to match new content
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
MostActiveStocks component
|
||||
→ fetch /api/stocks/most-actives
|
||||
→ server calls Alpaca screener API
|
||||
→ returns normalized data
|
||||
→ renders table
|
||||
→ setInterval re-fetches every 30s
|
||||
```
|
||||
|
||||
## Table Columns
|
||||
|
||||
| Column | Source | Format |
|
||||
|--------|--------|--------|
|
||||
| Symbol | `symbol` | Clickable link → `/analyze/TICKER` |
|
||||
| Name | `name` | Plain text |
|
||||
| Price | `price` | `$X.XX` |
|
||||
| Change % | `changePercent` | `+X.XX%` / `-X.XX%` (color-coded green/red) |
|
||||
| Volume | `volume` | Formatted number (e.g., `12.3M`) |
|
||||
|
||||
## Error Handling
|
||||
|
||||
- API errors: display red banner with error message and retry button
|
||||
- Network failures: show last data if available, otherwise error state
|
||||
- Empty response: show "No data available" message
|
||||
|
||||
## Route Changes
|
||||
|
||||
Add to `routes.ts`:
|
||||
```
|
||||
route("api/stocks/most-actives", "routes/api/stocks/most-actives.ts"),
|
||||
```
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Action | Purpose |
|
||||
|------|--------|---------|
|
||||
| `app/routes/api/stocks/most-actives.ts` | Create | Server proxy to Alpaca |
|
||||
| `app/components/MostActiveStocks.tsx` | Create | Table component with auto-refresh |
|
||||
| `app/routes/stocks.tsx` | Modify | Replace StockViewer with MostActiveStocks |
|
||||
| `app/routes.ts` | Modify | Add new API route |
|
||||
Reference in New Issue
Block a user