Files
auto-trade/features/dashboard/components/HoldingsList.tsx

124 lines
4.5 KiB
TypeScript
Raw Normal View History

2026-02-12 14:20:07 +09:00
import { AlertCircle, Wallet2 } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import type { DashboardHoldingItem } from "@/features/dashboard/types/dashboard.types";
import {
formatCurrency,
formatPercent,
getChangeToneClass,
} from "@/features/dashboard/utils/dashboard-format";
import { cn } from "@/lib/utils";
interface HoldingsListProps {
holdings: DashboardHoldingItem[];
selectedSymbol: string | null;
isLoading: boolean;
error: string | null;
onSelect: (symbol: string) => void;
}
/**
* @description .
* @see features/dashboard/components/DashboardContainer.tsx .
*/
export function HoldingsList({
holdings,
selectedSymbol,
isLoading,
error,
onSelect,
}: HoldingsListProps) {
return (
<Card>
<CardHeader className="pb-3">
{/* ========== TITLE ========== */}
<CardTitle className="flex items-center gap-2 text-base">
<Wallet2 className="h-4 w-4 text-brand-600 dark:text-brand-400" />
</CardTitle>
<CardDescription>
.
</CardDescription>
</CardHeader>
<CardContent>
{isLoading && holdings.length === 0 && (
<p className="text-sm text-muted-foreground"> .</p>
)}
{error && (
<p className="mb-2 flex items-start gap-1.5 text-sm text-red-600 dark:text-red-400">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0" />
<span>{error}</span>
</p>
)}
{!isLoading && holdings.length === 0 && !error && (
<p className="text-sm text-muted-foreground"> .</p>
)}
{holdings.length > 0 && (
<ScrollArea className="h-[420px] pr-3">
<div className="space-y-2">
{holdings.map((holding) => {
const isSelected = selectedSymbol === holding.symbol;
const toneClass = getChangeToneClass(holding.profitLoss);
return (
<button
key={holding.symbol}
type="button"
onClick={() => onSelect(holding.symbol)}
className={cn(
"w-full rounded-xl border px-3 py-3 text-left transition-all",
isSelected
? "border-brand-400 bg-brand-50/60 ring-1 ring-brand-300 dark:border-brand-600 dark:bg-brand-900/20 dark:ring-brand-700"
: "border-border/70 bg-background hover:border-brand-200 hover:bg-brand-50/30 dark:hover:border-brand-700 dark:hover:bg-brand-900/15",
)}
>
{/* ========== ROW TOP ========== */}
<div className="flex items-center justify-between gap-2">
<div>
<p className="text-sm font-semibold text-foreground">
{holding.name}
</p>
<p className="text-xs text-muted-foreground">
{holding.symbol} · {holding.market} · {holding.quantity}
</p>
</div>
<div className="text-right">
<p className="text-sm font-semibold text-foreground">
{formatCurrency(holding.currentPrice)}
</p>
<p className={cn("text-xs font-medium", toneClass)}>
{formatPercent(holding.profitRate)}
</p>
</div>
</div>
{/* ========== ROW BOTTOM ========== */}
<div className="mt-2 flex items-center justify-between text-xs">
<span className="text-muted-foreground">
{formatCurrency(holding.evaluationAmount)}
</span>
<span className={cn("font-medium", toneClass)}>
{formatCurrency(holding.profitLoss)}
</span>
</div>
</button>
);
})}
</div>
</ScrollArea>
)}
</CardContent>
</Card>
);
}