TradingAgents/frontend/src/components/StockCard.tsx

148 lines
5.5 KiB
TypeScript

import { Link } from 'react-router-dom';
import { TrendingUp, TrendingDown, Minus, ChevronRight } from 'lucide-react';
import type { StockAnalysis, Decision } from '../types';
interface StockCardProps {
stock: StockAnalysis;
showDetails?: boolean;
compact?: boolean;
}
export function DecisionBadge({ decision, size = 'default' }: { decision: Decision | null; size?: 'small' | 'default' }) {
if (!decision) return null;
const config = {
BUY: {
bg: 'bg-green-100 dark:bg-green-900/30',
text: 'text-green-800 dark:text-green-400',
icon: TrendingUp,
},
SELL: {
bg: 'bg-red-100 dark:bg-red-900/30',
text: 'text-red-800 dark:text-red-400',
icon: TrendingDown,
},
HOLD: {
bg: 'bg-amber-100 dark:bg-amber-900/30',
text: 'text-amber-800 dark:text-amber-400',
icon: Minus,
},
};
const { bg, text, icon: Icon } = config[decision];
const sizeClasses = size === 'small'
? 'px-2 py-0.5 text-xs gap-1'
: 'px-2.5 py-0.5 text-xs gap-1';
const iconSize = size === 'small' ? 'w-3 h-3' : 'w-3.5 h-3.5';
return (
<span className={`inline-flex items-center rounded-full font-semibold ${bg} ${text} ${sizeClasses}`}>
<Icon className={iconSize} />
{decision}
</span>
);
}
export function ConfidenceBadge({ confidence }: { confidence?: string }) {
if (!confidence) return null;
const colors = {
HIGH: 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800',
MEDIUM: 'bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 border-amber-200 dark:border-amber-800',
LOW: 'bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600',
};
return (
<span className={`text-xs px-2 py-0.5 rounded border ${colors[confidence as keyof typeof colors] || colors.MEDIUM}`}>
{confidence} Confidence
</span>
);
}
export function RiskBadge({ risk }: { risk?: string }) {
if (!risk) return null;
const colors = {
HIGH: 'text-red-600 dark:text-red-400',
MEDIUM: 'text-amber-600 dark:text-amber-400',
LOW: 'text-green-600 dark:text-green-400',
};
return (
<span className={`text-xs ${colors[risk as keyof typeof colors] || colors.MEDIUM}`}>
{risk} Risk
</span>
);
}
export default function StockCard({ stock, showDetails = true, compact = false }: StockCardProps) {
if (compact) {
return (
<Link
to={`/stock/${stock.symbol}`}
className="flex items-center justify-between px-3 py-2 hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors group focus:outline-none focus:bg-nifty-50 dark:focus:bg-nifty-900/30"
role="listitem"
aria-label={`${stock.symbol} - ${stock.company_name} - ${stock.decision} recommendation`}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<div className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
stock.decision === 'BUY' ? 'bg-green-500' :
stock.decision === 'SELL' ? 'bg-red-500' : 'bg-amber-500'
}`} aria-hidden="true" />
<span className="font-medium text-gray-900 dark:text-gray-100 text-sm">{stock.symbol}</span>
<span className="text-gray-400 dark:text-gray-500 text-xs hidden sm:inline" aria-hidden="true">·</span>
<span className="text-xs text-gray-500 dark:text-gray-400 truncate hidden sm:inline">{stock.company_name}</span>
</div>
<div className="flex items-center gap-2">
<DecisionBadge decision={stock.decision} />
<ChevronRight className="w-4 h-4 text-gray-300 dark:text-gray-600 group-hover:text-nifty-600 dark:group-hover:text-nifty-400 transition-colors" aria-hidden="true" />
</div>
</Link>
);
}
return (
<Link
to={`/stock/${stock.symbol}`}
className="card-hover p-3 flex items-center justify-between group"
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 truncate text-sm">{stock.symbol}</h3>
<DecisionBadge decision={stock.decision} />
</div>
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">{stock.company_name}</p>
{showDetails && (
<div className="flex items-center gap-2 mt-1.5">
<ConfidenceBadge confidence={stock.confidence} />
<RiskBadge risk={stock.risk} />
</div>
)}
</div>
<ChevronRight className="w-4 h-4 text-gray-400 dark:text-gray-500 group-hover:text-nifty-600 dark:group-hover:text-nifty-400 transition-colors flex-shrink-0" />
</Link>
);
}
export function StockCardCompact({ stock }: { stock: StockAnalysis }) {
return (
<Link
to={`/stock/${stock.symbol}`}
className="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors"
>
<div className="flex items-center gap-3">
<div className={`w-2 h-2 rounded-full ${
stock.decision === 'BUY' ? 'bg-green-500' :
stock.decision === 'SELL' ? 'bg-red-500' : 'bg-amber-500'
}`} />
<div>
<span className="font-medium text-gray-900 dark:text-gray-100">{stock.symbol}</span>
<span className="text-gray-400 dark:text-gray-500 mx-2">·</span>
<span className="text-sm text-gray-500 dark:text-gray-400">{stock.company_name}</span>
</div>
</div>
<DecisionBadge decision={stock.decision} />
</Link>
);
}