/** * Shared reusable UI components: StatusBadge, ConfidenceBar, TrendArrow, * DateRangeSelector, TickerFilter, LoadingSpinner, ErrorBoundary. * Requirements: 13.1, 13.2 */ import { Component, type ReactNode, type ChangeEvent } from 'react'; import { TrendingUp, TrendingDown, Minus, Loader2 } from 'lucide-react'; // --------------------------------------------------------------------------- // StatusBadge // --------------------------------------------------------------------------- const statusColors: Record = { completed: 'bg-green-900/40 text-green-400 border-green-700/50', success: 'bg-green-900/40 text-green-400 border-green-700/50', valid: 'bg-green-900/40 text-green-400 border-green-700/50', active: 'bg-green-900/40 text-green-400 border-green-700/50', approved: 'bg-green-900/40 text-green-400 border-green-700/50', filled: 'bg-green-900/40 text-green-400 border-green-700/50', extracted: 'bg-green-900/40 text-green-400 border-green-700/50', running: 'bg-blue-900/40 text-blue-400 border-blue-700/50', pending: 'bg-yellow-900/40 text-yellow-400 border-yellow-700/50', parsed: 'bg-yellow-900/40 text-yellow-400 border-yellow-700/50', failed: 'bg-red-900/40 text-red-400 border-red-700/50', rejected: 'bg-red-900/40 text-red-400 border-red-700/50', extraction_failed: 'bg-red-900/40 text-red-400 border-red-700/50', low_quality: 'bg-orange-900/40 text-orange-400 border-orange-700/50', cancelled: 'bg-gray-800/40 text-gray-400 border-gray-700/50', disabled: 'bg-gray-800/40 text-gray-400 border-gray-700/50', paper: 'bg-purple-900/40 text-purple-400 border-purple-700/50', live: 'bg-orange-900/40 text-orange-400 border-orange-700/50', buy: 'bg-green-900/40 text-green-400 border-green-700/50', sell: 'bg-red-900/40 text-red-400 border-red-700/50', hold: 'bg-yellow-900/40 text-yellow-400 border-yellow-700/50', watch: 'bg-blue-900/40 text-blue-400 border-blue-700/50', high: 'bg-green-900/40 text-green-400 border-green-700/50', medium: 'bg-yellow-900/40 text-yellow-400 border-yellow-700/50', low: 'bg-red-900/40 text-red-400 border-red-700/50', }; export function StatusBadge({ status }: { status: string | null | undefined }) { const s = (status ?? 'unknown').toLowerCase(); const cls = statusColors[s] ?? 'bg-gray-800/40 text-gray-400 border-gray-700/50'; return ( {s} ); } // --------------------------------------------------------------------------- // ConfidenceBar // --------------------------------------------------------------------------- export function ConfidenceBar({ value, className = '' }: { value: number | null | undefined; className?: string }) { const pct = Math.round((value ?? 0) * 100); const color = pct >= 70 ? 'bg-green-500' : pct >= 40 ? 'bg-yellow-500' : 'bg-red-500'; return (
{pct}%
); } // --------------------------------------------------------------------------- // TrendArrow // --------------------------------------------------------------------------- export function TrendArrow({ direction }: { direction: string | null | undefined }) { const d = (direction ?? 'neutral').toLowerCase(); if (d === 'bullish') return ; if (d === 'bearish') return ; return ; } // --------------------------------------------------------------------------- // DateRangeSelector // --------------------------------------------------------------------------- export function DateRangeSelector({ value, onChange }: { value: number; onChange: (hours: number) => void }) { const options = [ { label: '1h', hours: 1 }, { label: '6h', hours: 6 }, { label: '24h', hours: 24 }, { label: '7d', hours: 168 }, ]; return (
{options.map((opt) => ( ))}
); } // --------------------------------------------------------------------------- // TickerFilter // --------------------------------------------------------------------------- export function TickerFilter({ value, onChange }: { value: string; onChange: (v: string) => void }) { return ( ) => onChange(e.target.value.toUpperCase())} className="w-24 rounded-md border border-surface-700 bg-surface-900 px-2 py-1 text-xs text-gray-200 placeholder-gray-500 focus:border-brand-500 focus:outline-none" aria-label="Filter by ticker" /> ); } // --------------------------------------------------------------------------- // LoadingSpinner // --------------------------------------------------------------------------- export function LoadingSpinner({ className = '' }: { className?: string }) { return (
); } // --------------------------------------------------------------------------- // ErrorBoundary // --------------------------------------------------------------------------- interface EBProps { children: ReactNode; fallback?: ReactNode } interface EBState { error: Error | null } export class ErrorBoundary extends Component { state: EBState = { error: null }; static getDerivedStateFromError(error: Error) { return { error }; } render() { if (this.state.error) { return this.props.fallback ?? (
Something went wrong: {this.state.error.message}
); } return this.props.children; } } // --------------------------------------------------------------------------- // Card — generic container // --------------------------------------------------------------------------- export function Card({ children, className = '' }: { children: ReactNode; className?: string }) { return (
{children}
); }