feat: Add basic styling
This commit is contained in:
@@ -1,9 +1,959 @@
|
||||
import "./index.css";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function PortfolioView() {
|
||||
// Placeholder portfolio data - ideally would come from props
|
||||
const [portfolio] = useState({
|
||||
totalValue: 156842.75,
|
||||
cashBalance: 12467.32,
|
||||
performance: {
|
||||
daily: 1.24,
|
||||
weekly: -0.52,
|
||||
monthly: 3.87,
|
||||
yearly: 14.28,
|
||||
},
|
||||
holdings: [
|
||||
{
|
||||
symbol: "AAPL",
|
||||
name: "Apple Inc.",
|
||||
shares: 45,
|
||||
price: 187.32,
|
||||
value: 8429.4,
|
||||
change: 1.2,
|
||||
allocation: 5.8,
|
||||
avgCost: 162.5,
|
||||
},
|
||||
{
|
||||
symbol: "MSFT",
|
||||
name: "Microsoft Corporation",
|
||||
shares: 30,
|
||||
price: 403.78,
|
||||
value: 12113.4,
|
||||
change: 0.5,
|
||||
allocation: 8.4,
|
||||
avgCost: 340.25,
|
||||
},
|
||||
{
|
||||
symbol: "AMZN",
|
||||
name: "Amazon.com Inc.",
|
||||
shares: 25,
|
||||
price: 178.75,
|
||||
value: 4468.75,
|
||||
change: -0.8,
|
||||
allocation: 3.1,
|
||||
avgCost: 145.3,
|
||||
},
|
||||
{
|
||||
symbol: "GOOGL",
|
||||
name: "Alphabet Inc.",
|
||||
shares: 20,
|
||||
price: 164.85,
|
||||
value: 3297.0,
|
||||
change: 2.1,
|
||||
allocation: 2.3,
|
||||
avgCost: 125.75,
|
||||
},
|
||||
{
|
||||
symbol: "NVDA",
|
||||
name: "NVIDIA Corporation",
|
||||
shares: 35,
|
||||
price: 875.28,
|
||||
value: 30634.8,
|
||||
change: 3.4,
|
||||
allocation: 21.3,
|
||||
avgCost: 520.4,
|
||||
},
|
||||
{
|
||||
symbol: "TSLA",
|
||||
name: "Tesla, Inc.",
|
||||
shares: 40,
|
||||
price: 175.9,
|
||||
value: 7036.0,
|
||||
change: -1.2,
|
||||
allocation: 4.9,
|
||||
avgCost: 190.75,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const [activeTab, setActiveTab] = useState<"holdings" | "performance">(
|
||||
"holdings",
|
||||
);
|
||||
const [sortConfig, setSortConfig] = useState<{
|
||||
key: string;
|
||||
direction: "asc" | "desc";
|
||||
}>({
|
||||
key: "allocation",
|
||||
direction: "desc",
|
||||
});
|
||||
const [selectedHolding, setSelectedHolding] = useState<string | null>(null);
|
||||
|
||||
const sortedHoldings = [...portfolio.holdings].sort((a, b) => {
|
||||
if (
|
||||
a[sortConfig.key as keyof typeof a] < b[sortConfig.key as keyof typeof b]
|
||||
) {
|
||||
return sortConfig.direction === "asc" ? -1 : 1;
|
||||
}
|
||||
if (
|
||||
a[sortConfig.key as keyof typeof a] > b[sortConfig.key as keyof typeof b]
|
||||
) {
|
||||
return sortConfig.direction === "asc" ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const requestSort = (key: string) => {
|
||||
let direction: "asc" | "desc" = "asc";
|
||||
if (sortConfig.key === key && sortConfig.direction === "asc") {
|
||||
direction = "desc";
|
||||
}
|
||||
setSortConfig({ key, direction });
|
||||
};
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const formatPercent = (value: number) => {
|
||||
return `${value > 0 ? "+" : ""}${value.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
// Faux chart data for selected holding
|
||||
const generateChartData = (symbol: string) => {
|
||||
const data = [];
|
||||
const basePrice =
|
||||
portfolio.holdings.find((h) => h.symbol === symbol)?.price || 100;
|
||||
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() - 30 + i);
|
||||
|
||||
const randomFactor = (Math.sin(i / 5) + Math.random() - 0.5) * 0.05;
|
||||
const price = basePrice * (1 + randomFactor * (i / 3));
|
||||
|
||||
data.push({
|
||||
date: date.toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
}),
|
||||
price: parseFloat(price.toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// Calculate total value and percent change for display
|
||||
const totalValue = portfolio.totalValue;
|
||||
const totalChange = portfolio.holdings.reduce(
|
||||
(acc, curr) => acc + (curr.price - curr.avgCost) * curr.shares,
|
||||
0,
|
||||
);
|
||||
const totalPercentChange =
|
||||
(totalChange / (portfolio.totalValue - totalChange)) * 100;
|
||||
|
||||
const selectedStock = selectedHolding
|
||||
? portfolio.holdings.find((h) => h.symbol === selectedHolding)
|
||||
: null;
|
||||
const chartData = selectedHolding ? generateChartData(selectedHolding) : [];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 border border-solid border-slate-500 p-4 rounded-md">
|
||||
Portfolio View
|
||||
<div className="w-full max-w-4xl bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200">
|
||||
<div className="bg-gradient-to-r from-indigo-700 to-indigo-500 px-6 py-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<h2 className="text-white font-bold text-xl tracking-tight flex items-center">
|
||||
<svg
|
||||
className="w-6 h-6 mr-2"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path>
|
||||
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z"></path>
|
||||
</svg>
|
||||
Portfolio Summary
|
||||
</h2>
|
||||
<div className="bg-indigo-800/50 text-white px-3 py-1 rounded-md text-sm backdrop-blur-sm border border-indigo-400/30 flex items-center">
|
||||
<svg
|
||||
className="w-3 h-3 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
Updated: {new Date().toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-gradient-to-b from-indigo-50 to-white">
|
||||
<div className="grid grid-cols-3 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
|
||||
<div className="flex justify-between">
|
||||
<p className="text-gray-500 text-sm font-medium">Total Value</p>
|
||||
<svg
|
||||
className="w-5 h-5 text-indigo-400"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4 4a2 2 0 00-2 2v4a2 2 0 002 2V6h10a2 2 0 00-2-2H4zm2 6a2 2 0 012-2h8a2 2 0 012 2v4a2 2 0 01-2 2H8a2 2 0 01-2-2v-4zm6 4a2 2 0 100-4 2 2 0 000 4z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-1">
|
||||
{formatCurrency(portfolio.totalValue)}
|
||||
</p>
|
||||
<p
|
||||
className={`text-xs mt-1 flex items-center ${totalPercentChange >= 0 ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{totalPercentChange >= 0 ? (
|
||||
<svg
|
||||
className="w-3 h-3 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className="w-3 h-3 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M14.707 10.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 12.586V5a1 1 0 012 0v7.586l2.293-2.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
{formatPercent(totalPercentChange)} All Time
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
|
||||
<div className="flex justify-between">
|
||||
<p className="text-gray-500 text-sm font-medium">Cash Balance</p>
|
||||
<svg
|
||||
className="w-5 h-5 text-indigo-400"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M8.433 7.418c.155-.103.346-.196.567-.267v1.698a2.305 2.305 0 01-.567-.267C8.07 8.34 8 8.114 8 8c0-.114.07-.34.433-.582zM11 12.849v-1.698c.22.071.412.164.567.267.364.243.433.468.433.582 0 .114-.07.34-.433.582a2.305 2.305 0 01-.567.267z"></path>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-13a1 1 0 10-2 0v.092a4.535 4.535 0 00-1.676.662C6.602 6.234 6 7.009 6 8c0 .99.602 1.765 1.324 2.246.48.32 1.054.545 1.676.662v1.941c-.391-.127-.68-.317-.843-.504a1 1 0 10-1.51 1.31c.562.649 1.413 1.076 2.353 1.253V15a1 1 0 102 0v-.092a4.535 4.535 0 001.676-.662C13.398 13.766 14 12.991 14 12c0-.99-.602-1.765-1.324-2.246A4.535 4.535 0 0011 9.092V7.151c.391.127.68.317.843.504a1 1 0 101.511-1.31c-.563-.649-1.413-1.076-2.354-1.253V5z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-gray-900 mt-1">
|
||||
{formatCurrency(portfolio.cashBalance)}
|
||||
</p>
|
||||
<p className="text-xs mt-1 text-gray-500">
|
||||
{((portfolio.cashBalance / portfolio.totalValue) * 100).toFixed(
|
||||
1,
|
||||
)}
|
||||
% of portfolio
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 shadow-sm border border-gray-100 hover:shadow-md transition-shadow">
|
||||
<div className="flex justify-between">
|
||||
<p className="text-gray-500 text-sm font-medium">Daily Change</p>
|
||||
<svg
|
||||
className="w-5 h-5 text-indigo-400"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 7a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0V8.414l-4.293 4.293a1 1 0 01-1.414 0L8 10.414l-4.293 4.293a1 1 0 01-1.414-1.414l5-5a1 1 0 011.414 0L11 10.586 14.586 7H12z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<p
|
||||
className={`text-2xl font-bold mt-1 ${portfolio.performance.daily >= 0 ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{formatPercent(portfolio.performance.daily)}
|
||||
</p>
|
||||
<p
|
||||
className={`text-xs mt-1 ${portfolio.performance.daily >= 0 ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{formatCurrency(
|
||||
(portfolio.totalValue * portfolio.performance.daily) / 100,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-gray-200 mb-4">
|
||||
<div className="flex space-x-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab("holdings");
|
||||
setSelectedHolding(null);
|
||||
}}
|
||||
className={`px-4 py-2 font-medium text-sm focus:outline-none ${
|
||||
activeTab === "holdings"
|
||||
? "text-indigo-600 border-b-2 border-indigo-600 font-semibold"
|
||||
: "text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
Holdings
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab("performance");
|
||||
setSelectedHolding(null);
|
||||
}}
|
||||
className={`px-4 py-2 font-medium text-sm focus:outline-none ${
|
||||
activeTab === "performance"
|
||||
? "text-indigo-600 border-b-2 border-indigo-600 font-semibold"
|
||||
: "text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
Performance
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeTab === "holdings" && !selectedHolding && (
|
||||
<div className="overflow-x-auto rounded-lg border border-gray-200 shadow-sm">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
onClick={() => requestSort("symbol")}
|
||||
className="group px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span>Symbol</span>
|
||||
<span className="ml-1 text-gray-400 group-hover:text-gray-700">
|
||||
{sortConfig.key === "symbol"
|
||||
? sortConfig.direction === "asc"
|
||||
? "↑"
|
||||
: "↓"
|
||||
: "↕"}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Company
|
||||
</th>
|
||||
<th
|
||||
onClick={() => requestSort("shares")}
|
||||
className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
<div className="flex items-center justify-end">
|
||||
<span>Shares</span>
|
||||
<span className="ml-1 text-gray-400 group-hover:text-gray-700">
|
||||
{sortConfig.key === "shares"
|
||||
? sortConfig.direction === "asc"
|
||||
? "↑"
|
||||
: "↓"
|
||||
: "↕"}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
onClick={() => requestSort("price")}
|
||||
className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
<div className="flex items-center justify-end">
|
||||
<span>Price</span>
|
||||
<span className="ml-1 text-gray-400 group-hover:text-gray-700">
|
||||
{sortConfig.key === "price"
|
||||
? sortConfig.direction === "asc"
|
||||
? "↑"
|
||||
: "↓"
|
||||
: "↕"}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
onClick={() => requestSort("change")}
|
||||
className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
<div className="flex items-center justify-end">
|
||||
<span>Change</span>
|
||||
<span className="ml-1 text-gray-400 group-hover:text-gray-700">
|
||||
{sortConfig.key === "change"
|
||||
? sortConfig.direction === "asc"
|
||||
? "↑"
|
||||
: "↓"
|
||||
: "↕"}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
onClick={() => requestSort("value")}
|
||||
className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
<div className="flex items-center justify-end">
|
||||
<span>Value</span>
|
||||
<span className="ml-1 text-gray-400 group-hover:text-gray-700">
|
||||
{sortConfig.key === "value"
|
||||
? sortConfig.direction === "asc"
|
||||
? "↑"
|
||||
: "↓"
|
||||
: "↕"}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th
|
||||
onClick={() => requestSort("allocation")}
|
||||
className="group px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100"
|
||||
>
|
||||
<div className="flex items-center justify-end">
|
||||
<span>Allocation</span>
|
||||
<span className="ml-1 text-gray-400 group-hover:text-gray-700">
|
||||
{sortConfig.key === "allocation"
|
||||
? sortConfig.direction === "asc"
|
||||
? "↑"
|
||||
: "↓"
|
||||
: "↕"}
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{sortedHoldings.map((holding) => (
|
||||
<tr
|
||||
key={holding.symbol}
|
||||
className="hover:bg-indigo-50 cursor-pointer transition-colors"
|
||||
onClick={() => setSelectedHolding(holding.symbol)}
|
||||
>
|
||||
<td className="px-4 py-4 text-sm font-medium text-indigo-600">
|
||||
{holding.symbol}
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-gray-900">
|
||||
{holding.name}
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-gray-900 text-right">
|
||||
{holding.shares.toLocaleString()}
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-gray-900 text-right">
|
||||
{formatCurrency(holding.price)}
|
||||
</td>
|
||||
<td
|
||||
className={`px-4 py-4 text-sm text-right font-medium flex items-center justify-end ${holding.change >= 0 ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{holding.change >= 0 ? (
|
||||
<svg
|
||||
className="w-3 h-3 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className="w-3 h-3 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M14.707 10.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 12.586V5a1 1 0 012 0v7.586l2.293-2.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
{formatPercent(holding.change)}
|
||||
</td>
|
||||
<td className="px-4 py-4 text-sm text-gray-900 text-right font-medium">
|
||||
{formatCurrency(holding.value)}
|
||||
</td>
|
||||
<td className="px-4 py-4 text-right">
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="w-16 bg-gray-200 h-2 rounded-full overflow-hidden mr-2">
|
||||
<div
|
||||
className={`h-2 ${holding.change >= 0 ? "bg-green-500" : "bg-red-500"}`}
|
||||
style={{
|
||||
width: `${Math.min(100, holding.allocation * 3)}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<span className="text-sm text-gray-900">
|
||||
{holding.allocation.toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "holdings" && selectedHolding && selectedStock && (
|
||||
<div className="rounded-lg border border-gray-200 shadow-sm bg-white">
|
||||
<div className="p-4 flex justify-between items-start">
|
||||
<div>
|
||||
<div className="flex items-center">
|
||||
<h3 className="text-xl font-bold text-gray-900">
|
||||
{selectedStock.symbol}
|
||||
</h3>
|
||||
<span className="ml-2 text-gray-600">
|
||||
{selectedStock.name}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center mt-1">
|
||||
<span className="text-2xl font-bold text-gray-900">
|
||||
{formatCurrency(selectedStock.price)}
|
||||
</span>
|
||||
<span
|
||||
className={`ml-2 text-sm font-medium ${selectedStock.change >= 0 ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{selectedStock.change >= 0 ? "▲" : "▼"}{" "}
|
||||
{formatPercent(selectedStock.change)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedHolding(null)}
|
||||
className="bg-gray-100 hover:bg-gray-200 p-1 rounded-md"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 p-4">
|
||||
<div className="h-40 bg-white">
|
||||
<div className="flex items-end h-full space-x-1">
|
||||
{chartData.map((point, index) => {
|
||||
const maxPrice = Math.max(...chartData.map((d) => d.price));
|
||||
const minPrice = Math.min(...chartData.map((d) => d.price));
|
||||
const range = maxPrice - minPrice;
|
||||
const heightPercent =
|
||||
range === 0
|
||||
? 50
|
||||
: ((point.price - minPrice) / range) * 80 + 10;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col items-center flex-1"
|
||||
>
|
||||
<div
|
||||
className={`w-full rounded-sm ${point.price >= chartData[Math.max(0, index - 1)].price ? "bg-green-500" : "bg-red-500"}`}
|
||||
style={{ height: `${heightPercent}%` }}
|
||||
></div>
|
||||
{index % 5 === 0 && (
|
||||
<span className="text-xs text-gray-500 mt-1">
|
||||
{point.date}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 p-4">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Shares Owned</p>
|
||||
<p className="text-sm font-medium">
|
||||
{selectedStock.shares.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Market Value</p>
|
||||
<p className="text-sm font-medium">
|
||||
{formatCurrency(selectedStock.value)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Avg. Cost</p>
|
||||
<p className="text-sm font-medium">
|
||||
{formatCurrency(selectedStock.avgCost)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Cost Basis</p>
|
||||
<p className="text-sm font-medium">
|
||||
{formatCurrency(
|
||||
selectedStock.avgCost * selectedStock.shares,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Gain/Loss</p>
|
||||
<p
|
||||
className={`text-sm font-medium ${selectedStock.price - selectedStock.avgCost >= 0 ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{formatCurrency(
|
||||
(selectedStock.price - selectedStock.avgCost) *
|
||||
selectedStock.shares,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-500">Allocation</p>
|
||||
<p className="text-sm font-medium">
|
||||
{selectedStock.allocation.toFixed(2)}%
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-gray-200 p-4 flex space-x-2">
|
||||
<button className="flex-1 bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-md transition-colors text-sm">
|
||||
Buy More
|
||||
</button>
|
||||
<button className="flex-1 bg-red-600 hover:bg-red-700 text-white font-medium py-2 px-4 rounded-md transition-colors text-sm">
|
||||
Sell
|
||||
</button>
|
||||
<button className="flex items-center justify-center w-10 h-10 border border-gray-300 rounded-md hover:bg-gray-100 transition-colors">
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M8 12h.01M12 12h.01M16 12h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "performance" && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2 text-indigo-500"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M2 11a1 1 0 011-1h2a1 1 0 011 1v5a1 1 0 01-1 1H3a1 1 0 01-1-1v-5zM8 7a1 1 0 011-1h2a1 1 0 011 1v9a1 1 0 01-1 1H9a1 1 0 01-1-1V7zM14 4a1 1 0 011-1h2a1 1 0 011 1v12a1 1 0 01-1 1h-2a1 1 0 01-1-1V4z"></path>
|
||||
</svg>
|
||||
Performance Overview
|
||||
</h3>
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<p className="text-gray-500 text-sm font-medium">Daily</p>
|
||||
<p
|
||||
className={`text-lg font-bold flex items-center ${portfolio.performance.daily >= 0 ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{portfolio.performance.daily >= 0 ? (
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 7a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0V8.414l-4.293 4.293a1 1 0 01-1.414 0L8 10.414l-4.293 4.293a1 1 0 01-1.414-1.414l5-5a1 1 0 011.414 0L11 10.586 14.586 7H12z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 13a1 1 0 100 2h5a1 1 0 001-1V9a1 1 0 10-2 0v2.586l-4.293-4.293a1 1 0 00-1.414 0L8 9.586 3.707 5.293a1 1 0 00-1.414 1.414l5 5a1 1 0 001.414 0L11 9.414 14.586 13H12z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
{formatPercent(portfolio.performance.daily)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<p className="text-gray-500 text-sm font-medium">Weekly</p>
|
||||
<p
|
||||
className={`text-lg font-bold flex items-center ${portfolio.performance.weekly >= 0 ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{portfolio.performance.weekly >= 0 ? (
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 7a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0V8.414l-4.293 4.293a1 1 0 01-1.414 0L8 10.414l-4.293 4.293a1 1 0 01-1.414-1.414l5-5a1 1 0 011.414 0L11 10.586 14.586 7H12z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 13a1 1 0 100 2h5a1 1 0 001-1V9a1 1 0 10-2 0v2.586l-4.293-4.293a1 1 0 00-1.414 0L8 9.586 3.707 5.293a1 1 0 00-1.414 1.414l5 5a1 1 0 001.414 0L11 9.414 14.586 13H12z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
{formatPercent(portfolio.performance.weekly)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<p className="text-gray-500 text-sm font-medium">Monthly</p>
|
||||
<p
|
||||
className={`text-lg font-bold flex items-center ${portfolio.performance.monthly >= 0 ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{portfolio.performance.monthly >= 0 ? (
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 7a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0V8.414l-4.293 4.293a1 1 0 01-1.414 0L8 10.414l-4.293 4.293a1 1 0 01-1.414-1.414l5-5a1 1 0 011.414 0L11 10.586 14.586 7H12z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 13a1 1 0 100 2h5a1 1 0 001-1V9a1 1 0 10-2 0v2.586l-4.293-4.293a1 1 0 00-1.414 0L8 9.586 3.707 5.293a1 1 0 00-1.414 1.414l5 5a1 1 0 001.414 0L11 9.414 14.586 13H12z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
{formatPercent(portfolio.performance.monthly)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3">
|
||||
<p className="text-gray-500 text-sm font-medium">Yearly</p>
|
||||
<p
|
||||
className={`text-lg font-bold flex items-center ${portfolio.performance.yearly >= 0 ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{portfolio.performance.yearly >= 0 ? (
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 7a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0V8.414l-4.293 4.293a1 1 0 01-1.414 0L8 10.414l-4.293 4.293a1 1 0 01-1.414-1.414l5-5a1 1 0 011.414 0L11 10.586 14.586 7H12z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
) : (
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 13a1 1 0 100 2h5a1 1 0 001-1V9a1 1 0 10-2 0v2.586l-4.293-4.293a1 1 0 00-1.414 0L8 9.586 3.707 5.293a1 1 0 00-1.414 1.414l5 5a1 1 0 001.414 0L11 9.414 14.586 13H12z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
)}
|
||||
{formatPercent(portfolio.performance.yearly)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl p-5 shadow-sm border border-gray-200">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4 flex items-center">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2 text-indigo-500"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z"></path>
|
||||
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z"></path>
|
||||
</svg>
|
||||
Portfolio Allocation
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{sortedHoldings.map((holding) => (
|
||||
<div
|
||||
key={holding.symbol}
|
||||
className="flex items-center group hover:bg-indigo-50 p-2 rounded-lg transition-colors"
|
||||
>
|
||||
<div className="w-24 text-sm font-medium text-indigo-600 flex items-center">
|
||||
<div
|
||||
className={`w-3 h-3 rounded-full mr-2 ${holding.change >= 0 ? "bg-green-500" : "bg-red-500"}`}
|
||||
></div>
|
||||
{holding.symbol}
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<div className="bg-gray-200 h-4 rounded-full overflow-hidden shadow-inner">
|
||||
<div
|
||||
className="h-4 bg-gradient-to-r from-indigo-500 to-indigo-600"
|
||||
style={{ width: `${holding.allocation}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-16 text-sm font-medium text-gray-900 text-right ml-3">
|
||||
{holding.allocation.toFixed(1)}%
|
||||
</div>
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity ml-2">
|
||||
<button className="p-1 text-gray-400 hover:text-indigo-600">
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 bg-gray-50 p-3 rounded-lg">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">
|
||||
Portfolio Diversification
|
||||
</h4>
|
||||
<div className="flex h-4 rounded-full overflow-hidden">
|
||||
{[
|
||||
"Technology",
|
||||
"Consumer Cyclical",
|
||||
"Communication Services",
|
||||
"Financial",
|
||||
"Other",
|
||||
].map((sector, index) => {
|
||||
const widths = [42, 23, 18, 10, 7]; // example percentages
|
||||
const colors = [
|
||||
"bg-indigo-600",
|
||||
"bg-blue-500",
|
||||
"bg-green-500",
|
||||
"bg-yellow-500",
|
||||
"bg-red-500",
|
||||
];
|
||||
return (
|
||||
<div
|
||||
key={sector}
|
||||
className={`${colors[index]} h-full`}
|
||||
style={{ width: `${widths[index]}%` }}
|
||||
title={`${sector}: ${widths[index]}%`}
|
||||
></div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-wrap mt-2 text-xs">
|
||||
{[
|
||||
"Technology",
|
||||
"Consumer Cyclical",
|
||||
"Communication Services",
|
||||
"Financial",
|
||||
"Other",
|
||||
].map((sector, index) => {
|
||||
const widths = [42, 23, 18, 10, 7]; // example percentages
|
||||
const colors = [
|
||||
"text-indigo-600",
|
||||
"text-blue-500",
|
||||
"text-green-500",
|
||||
"text-yellow-500",
|
||||
"text-red-500",
|
||||
];
|
||||
return (
|
||||
<div key={sector} className="mr-3 flex items-center">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${colors[index].replace("text", "bg")} mr-1`}
|
||||
></div>
|
||||
<span className={`${colors[index]} font-medium`}>
|
||||
{sector} {widths[index]}%
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button className="px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50 shadow-sm flex items-center">
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
Export Data
|
||||
</button>
|
||||
<button className="px-4 py-2 bg-indigo-600 border border-indigo-600 rounded-md text-sm font-medium text-white hover:bg-indigo-700 shadow-sm flex items-center">
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
></path>
|
||||
</svg>
|
||||
View Full Report
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,54 @@
|
||||
import "./index.css";
|
||||
import { useStream } from "@langchain/langgraph-sdk/react";
|
||||
import type { AIMessage, Message } from "@langchain/langgraph-sdk";
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
|
||||
export default function StockPrice(props: {
|
||||
instruction: string;
|
||||
logo: string;
|
||||
}) {
|
||||
const [counter, setCounter] = useState(0);
|
||||
const [quantity, setQuantity] = useState(1);
|
||||
const [stockData, setStockData] = useState({
|
||||
symbol: "AAPL",
|
||||
name: "Apple Inc.",
|
||||
price: 187.32,
|
||||
change: 1.24,
|
||||
changePercent: 0.67,
|
||||
previousClose: 186.08,
|
||||
open: 186.75,
|
||||
dayHigh: 188.45,
|
||||
dayLow: 186.2,
|
||||
volume: 54320000,
|
||||
marketCap: "2.92T",
|
||||
peRatio: 29.13,
|
||||
dividend: 0.96,
|
||||
dividendYield: 0.51,
|
||||
moving50Day: 182.46,
|
||||
moving200Day: 175.8,
|
||||
fiftyTwoWeekHigh: 201.48,
|
||||
fiftyTwoWeekLow: 143.9,
|
||||
analystRating: "Buy",
|
||||
analystCount: 32,
|
||||
priceTarget: 210.5,
|
||||
});
|
||||
|
||||
const [priceHistory, setPriceHistory] = useState<
|
||||
{ time: string; price: number }[]
|
||||
>([]);
|
||||
const [orderType, setOrderType] = useState<"buy" | "sell">("buy");
|
||||
const [showOrder, setShowOrder] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<"chart" | "details" | "analysis">(
|
||||
"chart",
|
||||
);
|
||||
const [isLiveUpdating, setIsLiveUpdating] = useState(false);
|
||||
const [orderTypeOptions] = useState([
|
||||
{ value: "market", label: "Market" },
|
||||
{ value: "limit", label: "Limit" },
|
||||
{ value: "stop", label: "Stop" },
|
||||
]);
|
||||
const [selectedOrderTypeOption, setSelectedOrderTypeOption] =
|
||||
useState("market");
|
||||
const [limitPrice, setLimitPrice] = useState(stockData.price.toFixed(2));
|
||||
const [showOrderSuccess, setShowOrderSuccess] = useState(false);
|
||||
|
||||
// useStream should be able to be infered from context
|
||||
const thread = useStream<{ messages: Message[] }>({
|
||||
@@ -27,33 +68,889 @@ export default function StockPrice(props: {
|
||||
|
||||
const toolCallId = aiTool?.tool_calls?.[0]?.id;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 border border-solid border-slate-500 p-4 rounded-md">
|
||||
Request: {props.instruction}
|
||||
<button className="text-left" onClick={() => setCounter(counter + 1)}>
|
||||
Click me
|
||||
</button>
|
||||
<p>Counter: {counter}</p>
|
||||
{toolCallId && (
|
||||
<button
|
||||
className="text-left"
|
||||
onClick={() => {
|
||||
thread.submit({
|
||||
messages: [
|
||||
{
|
||||
type: "tool",
|
||||
tool_call_id: toolCallId!,
|
||||
name: "stockbroker",
|
||||
content: "hey",
|
||||
},
|
||||
{ type: "human", content: `Buy ${counter}` },
|
||||
],
|
||||
// Simulated price history generation on component mount
|
||||
useEffect(() => {
|
||||
generatePriceHistory();
|
||||
}, []);
|
||||
|
||||
const generatePriceHistory = () => {
|
||||
const now = new Date();
|
||||
const history = [];
|
||||
|
||||
for (let i = 30; i >= 0; i--) {
|
||||
const time = new Date(now.getTime() - i * 15 * 60000); // 15-minute intervals
|
||||
const basePrice = 187.32;
|
||||
// Make the price movement more interesting with some trends
|
||||
const trend = Math.sin(i / 5) * 1.5;
|
||||
const randomFactor = (Math.random() - 0.5) * 1.5;
|
||||
const price = basePrice + trend + randomFactor;
|
||||
|
||||
history.push({
|
||||
time: time.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
price: parseFloat(price.toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
||||
setPriceHistory(history);
|
||||
};
|
||||
|
||||
// Simulate live price updates
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
if (isLiveUpdating) {
|
||||
interval = setInterval(() => {
|
||||
setStockData((prev) => {
|
||||
// Random small price movement
|
||||
const priceChange = (Math.random() - 0.5) * 0.3;
|
||||
const newPrice = parseFloat((prev.price + priceChange).toFixed(2));
|
||||
|
||||
// Update price history
|
||||
setPriceHistory((history) => {
|
||||
const now = new Date();
|
||||
const newHistory = [...history];
|
||||
if (newHistory.length > 30) {
|
||||
newHistory.shift(); // Remove oldest entry to keep array length consistent
|
||||
}
|
||||
newHistory.push({
|
||||
time: now.toLocaleTimeString([], {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}),
|
||||
price: newPrice,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Buy
|
||||
</button>
|
||||
)}
|
||||
return newHistory;
|
||||
});
|
||||
|
||||
return {
|
||||
...prev,
|
||||
price: newPrice,
|
||||
change: parseFloat((newPrice - prev.previousClose).toFixed(2)),
|
||||
changePercent: parseFloat(
|
||||
(
|
||||
((newPrice - prev.previousClose) / prev.previousClose) *
|
||||
100
|
||||
).toFixed(2),
|
||||
),
|
||||
dayHigh: Math.max(prev.dayHigh, newPrice),
|
||||
dayLow: Math.min(prev.dayLow, newPrice),
|
||||
};
|
||||
});
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (interval) clearInterval(interval);
|
||||
};
|
||||
}, [isLiveUpdating]);
|
||||
|
||||
const handleQuantityChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = parseInt(e.target.value);
|
||||
if (value > 0 && value <= 1000) {
|
||||
setQuantity(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLimitPriceChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value;
|
||||
if (/^\d*\.?\d*$/.test(value)) {
|
||||
setLimitPrice(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOrderTypeChange = (type: "buy" | "sell") => {
|
||||
setOrderType(type);
|
||||
};
|
||||
|
||||
const toggleLiveUpdates = () => {
|
||||
setIsLiveUpdating((prev) => !prev);
|
||||
};
|
||||
|
||||
const handleOrder = () => {
|
||||
// Submit the order through the thread
|
||||
if (toolCallId) {
|
||||
const orderDetails = {
|
||||
action: orderType,
|
||||
quantity,
|
||||
symbol: stockData.symbol,
|
||||
orderType: selectedOrderTypeOption,
|
||||
...(selectedOrderTypeOption !== "market" && {
|
||||
limitPrice: parseFloat(limitPrice),
|
||||
}),
|
||||
};
|
||||
|
||||
thread.submit({
|
||||
messages: [
|
||||
{
|
||||
type: "tool",
|
||||
tool_call_id: toolCallId,
|
||||
name: "stockbroker",
|
||||
content: JSON.stringify(orderDetails),
|
||||
},
|
||||
{
|
||||
type: "human",
|
||||
content: `${orderType === "buy" ? "Bought" : "Sold"} ${quantity} shares of ${stockData.symbol} at ${
|
||||
selectedOrderTypeOption === "market"
|
||||
? formatCurrency(stockData.price)
|
||||
: formatCurrency(parseFloat(limitPrice))
|
||||
}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
setShowOrderSuccess(true);
|
||||
setTimeout(() => {
|
||||
setShowOrderSuccess(false);
|
||||
setShowOrder(false);
|
||||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
const formatPercent = (value: number) => {
|
||||
return `${value > 0 ? "+" : ""}${value.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
const getMinMax = () => {
|
||||
if (priceHistory.length === 0) return { min: 0, max: 0 };
|
||||
const prices = priceHistory.map((item) => item.price);
|
||||
return {
|
||||
min: Math.min(...prices),
|
||||
max: Math.max(...prices),
|
||||
};
|
||||
};
|
||||
|
||||
// Generate a simple price chart with the last price as endpoint
|
||||
const generateChartPath = useCallback(() => {
|
||||
if (priceHistory.length === 0) return "";
|
||||
|
||||
const { min, max } = getMinMax();
|
||||
const range = max - min || 1;
|
||||
const width = 100; // % of container width
|
||||
const height = 100; // % of container height
|
||||
|
||||
const points = priceHistory.map((point, i) => {
|
||||
const x = (i / (priceHistory.length - 1)) * width;
|
||||
const y = height - ((point.price - min) / range) * height;
|
||||
return `${x},${y}`;
|
||||
});
|
||||
|
||||
return `M ${points.join(" L ")}`;
|
||||
}, [priceHistory]);
|
||||
|
||||
const { min, max } = getMinMax();
|
||||
const range = max - min || 1;
|
||||
const chartPath = generateChartPath();
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-md bg-white rounded-xl shadow-lg overflow-hidden border border-gray-200">
|
||||
<div
|
||||
className={`px-4 py-3 ${stockData.change >= 0 ? "bg-gradient-to-r from-green-600 to-green-500" : "bg-gradient-to-r from-red-600 to-red-500"}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center">
|
||||
{props.logo && (
|
||||
<img
|
||||
src={props.logo}
|
||||
alt="Logo"
|
||||
className="h-6 w-6 mr-2 rounded-full bg-white p-0.5"
|
||||
/>
|
||||
)}
|
||||
<h3 className="text-white font-bold">
|
||||
{stockData.symbol}{" "}
|
||||
<span className="font-normal text-white/80 text-sm">
|
||||
{stockData.name}
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="text-white font-bold text-xl mr-2">
|
||||
{formatCurrency(stockData.price)}
|
||||
</span>
|
||||
<div
|
||||
className={`flex items-center px-2 py-1 rounded text-xs font-medium text-white ${stockData.change >= 0 ? "bg-green-700/50" : "bg-red-700/50"} backdrop-blur-sm border ${stockData.change >= 0 ? "border-green-400/30" : "border-red-400/30"}`}
|
||||
>
|
||||
{stockData.change >= 0 ? "▲" : "▼"}{" "}
|
||||
{formatCurrency(Math.abs(stockData.change))} (
|
||||
{formatPercent(stockData.changePercent)})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center text-xs text-white/80 mt-1">
|
||||
<span className="italic">{props.instruction}</span>
|
||||
<button
|
||||
onClick={toggleLiveUpdates}
|
||||
className={`flex items-center text-xs px-2 py-0.5 rounded ${isLiveUpdating ? "bg-white/30 text-white" : "bg-white/10 text-white/70"} hover:bg-white/20 transition-colors`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block w-2 h-2 rounded-full mr-1 ${isLiveUpdating ? "bg-green-400 animate-pulse" : "bg-gray-400"}`}
|
||||
></span>
|
||||
{isLiveUpdating ? "Live" : "Static"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-gray-200">
|
||||
<div className="flex">
|
||||
<button
|
||||
onClick={() => setActiveTab("chart")}
|
||||
className={`px-4 py-2 text-sm font-medium ${
|
||||
activeTab === "chart"
|
||||
? "text-indigo-600 border-b-2 border-indigo-600"
|
||||
: "text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
Chart
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("details")}
|
||||
className={`px-4 py-2 text-sm font-medium ${
|
||||
activeTab === "details"
|
||||
? "text-indigo-600 border-b-2 border-indigo-600"
|
||||
: "text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab("analysis")}
|
||||
className={`px-4 py-2 text-sm font-medium ${
|
||||
activeTab === "analysis"
|
||||
? "text-indigo-600 border-b-2 border-indigo-600"
|
||||
: "text-gray-500 hover:text-gray-700"
|
||||
}`}
|
||||
>
|
||||
Analysis
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
{activeTab === "chart" && (
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<div className="border rounded-lg p-3 bg-white shadow-sm">
|
||||
<div className="flex justify-between text-xs text-gray-500 mb-1">
|
||||
<span className="font-medium">Price Chart</span>
|
||||
<div className="flex space-x-1">
|
||||
<button className="px-1.5 py-0.5 bg-indigo-100 text-indigo-700 rounded text-xs font-medium">
|
||||
1D
|
||||
</button>
|
||||
<button className="px-1.5 py-0.5 hover:bg-gray-100 rounded text-xs text-gray-600">
|
||||
1W
|
||||
</button>
|
||||
<button className="px-1.5 py-0.5 hover:bg-gray-100 rounded text-xs text-gray-600">
|
||||
1M
|
||||
</button>
|
||||
<button className="px-1.5 py-0.5 hover:bg-gray-100 rounded text-xs text-gray-600">
|
||||
1Y
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative h-40 mt-2">
|
||||
<div className="absolute inset-0">
|
||||
{/* Chart grid lines */}
|
||||
<div className="h-full flex flex-col justify-between">
|
||||
{[0, 1, 2, 3].map((line) => (
|
||||
<div
|
||||
key={line}
|
||||
className="border-t border-gray-100 w-full h-0"
|
||||
></div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* SVG Line Chart */}
|
||||
<svg
|
||||
className="absolute inset-0 w-full h-full"
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="none"
|
||||
>
|
||||
{/* Chart path */}
|
||||
<path
|
||||
d={chartPath}
|
||||
stroke={stockData.change >= 0 ? "#10b981" : "#ef4444"}
|
||||
strokeWidth="1.5"
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
|
||||
{/* Gradient area under the chart */}
|
||||
<path
|
||||
d={`${chartPath} L 100,100 L 0,100 Z`}
|
||||
fill={
|
||||
stockData.change >= 0
|
||||
? "url(#greenGradient)"
|
||||
: "url(#redGradient)"
|
||||
}
|
||||
fillOpacity="0.2"
|
||||
/>
|
||||
|
||||
{/* Gradient definitions */}
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="greenGradient"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="0%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor="#10b981"
|
||||
stopOpacity="0.8"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor="#10b981"
|
||||
stopOpacity="0"
|
||||
/>
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="redGradient"
|
||||
x1="0%"
|
||||
y1="0%"
|
||||
x2="0%"
|
||||
y2="100%"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor="#ef4444"
|
||||
stopOpacity="0.8"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor="#ef4444"
|
||||
stopOpacity="0"
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
{/* Price labels on Y-axis */}
|
||||
<div className="absolute right-0 inset-y-0 flex flex-col justify-between text-right pr-1 text-xs text-gray-400 pointer-events-none">
|
||||
<span>{formatCurrency(max)}</span>
|
||||
<span>{formatCurrency(min + range * 0.66)}</span>
|
||||
<span>{formatCurrency(min + range * 0.33)}</span>
|
||||
<span>{formatCurrency(min)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-xs text-gray-500 mt-2">
|
||||
<div className="flex items-center">
|
||||
<span className="inline-block w-2 h-2 bg-gray-300 rounded-full mr-1"></span>
|
||||
{priceHistory[0]?.time || "9:30 AM"}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span>Vol: {stockData.volume.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="inline-block w-2 h-2 bg-gray-300 rounded-full mr-1"></span>
|
||||
{priceHistory[priceHistory.length - 1]?.time || "4:00 PM"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Key Stock Information */}
|
||||
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||
<div className="bg-gray-50 rounded-lg p-2.5 shadow-sm border border-gray-100">
|
||||
<p className="text-xs text-gray-500 font-medium">Day Range</p>
|
||||
<div className="relative pt-1">
|
||||
<div className="overflow-hidden h-1.5 mb-1 text-xs flex rounded bg-gray-200 mt-1">
|
||||
<div
|
||||
className={`shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center ${stockData.change >= 0 ? "bg-green-500" : "bg-red-500"}`}
|
||||
style={{
|
||||
width: `${((stockData.price - stockData.dayLow) / (stockData.dayHigh - stockData.dayLow)) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-600">
|
||||
<span>{formatCurrency(stockData.dayLow)}</span>
|
||||
<span>{formatCurrency(stockData.dayHigh)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-2.5 shadow-sm border border-gray-100">
|
||||
<p className="text-xs text-gray-500 font-medium">
|
||||
52-Week Range
|
||||
</p>
|
||||
<div className="relative pt-1">
|
||||
<div className="overflow-hidden h-1.5 mb-1 text-xs flex rounded bg-gray-200 mt-1">
|
||||
<div
|
||||
className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-indigo-500"
|
||||
style={{
|
||||
width: `${((stockData.price - stockData.fiftyTwoWeekLow) / (stockData.fiftyTwoWeekHigh - stockData.fiftyTwoWeekLow)) * 100}%`,
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-600">
|
||||
<span>{formatCurrency(stockData.fiftyTwoWeekLow)}</span>
|
||||
<span>{formatCurrency(stockData.fiftyTwoWeekHigh)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{activeTab === "details" && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
|
||||
<p className="text-xs text-gray-500 font-medium mb-1">Open</p>
|
||||
<p className="font-medium">{formatCurrency(stockData.open)}</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
|
||||
<p className="text-xs text-gray-500 font-medium mb-1">
|
||||
Previous Close
|
||||
</p>
|
||||
<p className="font-medium">
|
||||
{formatCurrency(stockData.previousClose)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
|
||||
<p className="text-xs text-gray-500 font-medium mb-1">
|
||||
Market Cap
|
||||
</p>
|
||||
<p className="font-medium">{stockData.marketCap}</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
|
||||
<p className="text-xs text-gray-500 font-medium mb-1">Volume</p>
|
||||
<p className="font-medium">
|
||||
{stockData.volume.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
|
||||
<p className="text-xs text-gray-500 font-medium mb-1">
|
||||
P/E Ratio
|
||||
</p>
|
||||
<p className="font-medium">{stockData.peRatio}</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
|
||||
<p className="text-xs text-gray-500 font-medium mb-1">
|
||||
Dividend Yield
|
||||
</p>
|
||||
<p className="font-medium">{stockData.dividendYield}%</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
|
||||
<p className="text-xs text-gray-500 font-medium mb-1">
|
||||
50-Day Avg
|
||||
</p>
|
||||
<p className="font-medium">
|
||||
{formatCurrency(stockData.moving50Day)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gray-50 rounded-lg p-3 border border-gray-100 shadow-sm">
|
||||
<p className="text-xs text-gray-500 font-medium mb-1">
|
||||
200-Day Avg
|
||||
</p>
|
||||
<p className="font-medium">
|
||||
{formatCurrency(stockData.moving200Day)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-indigo-50 rounded-lg p-4 border border-indigo-100">
|
||||
<h4 className="font-medium text-indigo-900 text-sm mb-2">
|
||||
Company Overview
|
||||
</h4>
|
||||
<p className="text-xs text-indigo-700">
|
||||
{stockData.name} is a leading technology company that designs,
|
||||
manufactures, and markets consumer electronics, computer
|
||||
software, and online services. The company has a strong global
|
||||
presence and is known for its innovation in the industry.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "analysis" && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white rounded-lg p-3 border border-gray-200 shadow-sm">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h4 className="font-medium text-gray-900">Analyst Consensus</h4>
|
||||
<div
|
||||
className={`px-2 py-0.5 rounded text-xs font-medium ${
|
||||
stockData.analystRating === "Buy"
|
||||
? "bg-green-100 text-green-800"
|
||||
: stockData.analystRating === "Hold"
|
||||
? "bg-yellow-100 text-yellow-800"
|
||||
: "bg-red-100 text-red-800"
|
||||
}`}
|
||||
>
|
||||
{stockData.analystRating}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex mb-1">
|
||||
<div className="w-20 text-xs text-gray-500">Strong Buy</div>
|
||||
<div className="flex-grow">
|
||||
<div className="h-4 bg-gray-200 rounded-sm overflow-hidden">
|
||||
<div
|
||||
className="h-4 bg-green-600"
|
||||
style={{ width: "65%" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-8 text-xs text-right text-gray-600">65%</div>
|
||||
</div>
|
||||
<div className="flex mb-1">
|
||||
<div className="w-20 text-xs text-gray-500">Buy</div>
|
||||
<div className="flex-grow">
|
||||
<div className="h-4 bg-gray-200 rounded-sm overflow-hidden">
|
||||
<div
|
||||
className="h-4 bg-green-500"
|
||||
style={{ width: "20%" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-8 text-xs text-right text-gray-600">20%</div>
|
||||
</div>
|
||||
<div className="flex mb-1">
|
||||
<div className="w-20 text-xs text-gray-500">Hold</div>
|
||||
<div className="flex-grow">
|
||||
<div className="h-4 bg-gray-200 rounded-sm overflow-hidden">
|
||||
<div
|
||||
className="h-4 bg-yellow-500"
|
||||
style={{ width: "10%" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-8 text-xs text-right text-gray-600">10%</div>
|
||||
</div>
|
||||
<div className="flex mb-1">
|
||||
<div className="w-20 text-xs text-gray-500">Sell</div>
|
||||
<div className="flex-grow">
|
||||
<div className="h-4 bg-gray-200 rounded-sm overflow-hidden">
|
||||
<div
|
||||
className="h-4 bg-red-500"
|
||||
style={{ width: "5%" }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-8 text-xs text-right text-gray-600">5%</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 text-xs text-gray-500">
|
||||
Based on {stockData.analystCount} analyst ratings
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-lg p-3 border border-gray-200 shadow-sm">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h4 className="font-medium text-gray-900">Price Target</h4>
|
||||
<span className="text-sm font-bold text-indigo-600">
|
||||
{formatCurrency(stockData.priceTarget)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="relative pt-1">
|
||||
<div className="flex mb-2 items-center justify-between">
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className="text-green-600 font-medium">
|
||||
+
|
||||
{(
|
||||
(stockData.priceTarget / stockData.price - 1) *
|
||||
100
|
||||
).toFixed(2)}
|
||||
%
|
||||
</span>{" "}
|
||||
Upside
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-hidden h-1.5 mb-1 text-xs flex rounded bg-gray-200">
|
||||
<div
|
||||
style={{
|
||||
width: `${(stockData.price / stockData.priceTarget) * 100}%`,
|
||||
}}
|
||||
className="shadow-none flex flex-col text-center whitespace-nowrap text-white justify-center bg-indigo-600"
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between text-xs text-gray-500">
|
||||
<span>Current: {formatCurrency(stockData.price)}</span>
|
||||
<span>Target: {formatCurrency(stockData.priceTarget)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-indigo-50 rounded-lg p-3 border border-indigo-100">
|
||||
<h4 className="font-medium text-indigo-900 mb-2">Recent News</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs text-indigo-800">
|
||||
<p className="font-medium">
|
||||
{stockData.name} Reports Strong Quarterly Earnings
|
||||
</p>
|
||||
<p className="text-indigo-600 text-xs">2 days ago</p>
|
||||
</div>
|
||||
<div className="text-xs text-indigo-800">
|
||||
<p className="font-medium">
|
||||
New Product Launch Expected Next Month
|
||||
</p>
|
||||
<p className="text-indigo-600 text-xs">5 days ago</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Order Interface */}
|
||||
{!showOrder && !showOrderSuccess ? (
|
||||
<div className="flex space-x-2 mt-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowOrder(true);
|
||||
setOrderType("buy");
|
||||
}}
|
||||
className="flex-1 bg-gradient-to-r from-green-600 to-green-500 hover:from-green-700 hover:to-green-600 text-white font-medium py-2 px-4 rounded-lg shadow-sm transition-all flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
Buy
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowOrder(true);
|
||||
setOrderType("sell");
|
||||
}}
|
||||
className="flex-1 bg-gradient-to-r from-red-600 to-red-500 hover:from-red-700 hover:to-red-600 text-white font-medium py-2 px-4 rounded-lg shadow-sm transition-all flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 mr-1"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M14.707 10.293a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 111.414-1.414L9 12.586V5a1 1 0 012 0v7.586l2.293-2.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
Sell
|
||||
</button>
|
||||
</div>
|
||||
) : showOrderSuccess ? (
|
||||
<div className="mt-4 flex items-center justify-center bg-green-50 rounded-lg p-4 border border-green-200 text-green-800">
|
||||
<svg
|
||||
className="w-5 h-5 mr-2 text-green-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M5 13l4 4L19 7"
|
||||
></path>
|
||||
</svg>
|
||||
<span className="font-medium">Order submitted successfully!</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 bg-white rounded-lg border border-gray-200 shadow-md p-4">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h4
|
||||
className={`font-bold text-lg ${orderType === "buy" ? "text-green-600" : "text-red-600"}`}
|
||||
>
|
||||
{orderType === "buy" ? "Buy" : "Sell"} {stockData.symbol}
|
||||
</h4>
|
||||
<button
|
||||
onClick={() => setShowOrder(false)}
|
||||
className="text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-full p-1 transition-colors"
|
||||
>
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-3">
|
||||
<div className="flex space-x-1 mb-3">
|
||||
{orderTypeOptions.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => setSelectedOrderTypeOption(option.value)}
|
||||
className={`flex-1 px-2 py-1.5 text-xs font-medium rounded-md transition-colors ${
|
||||
selectedOrderTypeOption === option.value
|
||||
? `${orderType === "buy" ? "bg-green-100 text-green-800 border-green-300" : "bg-red-100 text-red-800 border-red-300"} border`
|
||||
: "bg-gray-100 text-gray-800 border border-gray-200 hover:bg-gray-200"
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2 mb-3">
|
||||
<div className="flex-1">
|
||||
<label
|
||||
htmlFor="quantity"
|
||||
className="block text-xs font-medium text-gray-700 mb-1"
|
||||
>
|
||||
Quantity
|
||||
</label>
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
onClick={() => quantity > 1 && setQuantity(quantity - 1)}
|
||||
className="border rounded-l px-2 py-1 text-gray-600 hover:bg-gray-100"
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
id="quantity"
|
||||
min="1"
|
||||
max="1000"
|
||||
value={quantity}
|
||||
onChange={handleQuantityChange}
|
||||
className="border-t border-b w-full px-3 py-1 text-center focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
quantity < 1000 && setQuantity(quantity + 1)
|
||||
}
|
||||
className="border rounded-r px-2 py-1 text-gray-600 hover:bg-gray-100"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedOrderTypeOption !== "market" && (
|
||||
<div className="flex-1">
|
||||
<label
|
||||
htmlFor="limitPrice"
|
||||
className="block text-xs font-medium text-gray-700 mb-1"
|
||||
>
|
||||
{selectedOrderTypeOption === "limit"
|
||||
? "Limit Price"
|
||||
: "Stop Price"}
|
||||
</label>
|
||||
<div className="flex items-center border rounded overflow-hidden">
|
||||
<span className="bg-gray-100 px-2 py-1 text-gray-500 text-sm border-r">
|
||||
$
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
id="limitPrice"
|
||||
value={limitPrice}
|
||||
onChange={handleLimitPriceChange}
|
||||
className="flex-1 px-3 py-1 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedOrderTypeOption !== "market" && (
|
||||
<div className="rounded-md bg-indigo-50 p-2 mb-3">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<svg
|
||||
className="h-5 w-5 text-indigo-400"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
<p className="text-xs text-indigo-700">
|
||||
{selectedOrderTypeOption === "limit"
|
||||
? `Your ${orderType} order will execute only at ${formatCurrency(parseFloat(limitPrice))} or better.`
|
||||
: `Your ${orderType} order will execute when the price reaches ${formatCurrency(parseFloat(limitPrice))}.`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-gray-50 rounded-md p-3 mb-4">
|
||||
<div className="flex justify-between text-sm py-1">
|
||||
<span className="text-gray-600">Market Price</span>
|
||||
<span className="font-medium">
|
||||
{formatCurrency(stockData.price)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-sm py-1">
|
||||
<span className="text-gray-600">
|
||||
{orderType === "buy" ? "Cost" : "Credit"} ({quantity}{" "}
|
||||
{quantity === 1 ? "share" : "shares"})
|
||||
</span>
|
||||
<span>{formatCurrency(stockData.price * quantity)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-sm py-1">
|
||||
<span className="text-gray-600">Commission</span>
|
||||
<span>$0.00</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between text-sm py-1 border-t mt-1 pt-1">
|
||||
<span className="font-medium">Estimated Total</span>
|
||||
<span className="font-bold">
|
||||
{formatCurrency(stockData.price * quantity)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<button
|
||||
onClick={() => setShowOrder(false)}
|
||||
className="flex-1 py-2 px-4 border border-gray-300 rounded-md text-gray-700 font-medium hover:bg-gray-50 text-sm transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleOrder}
|
||||
disabled={!toolCallId}
|
||||
className={`flex-1 py-2 px-4 rounded-md font-medium text-white text-sm ${
|
||||
orderType === "buy"
|
||||
? "bg-green-600 hover:bg-green-700"
|
||||
: "bg-red-600 hover:bg-red-700"
|
||||
} ${!toolCallId ? "opacity-50 cursor-not-allowed" : ""} transition-colors`}
|
||||
>
|
||||
Review {orderType === "buy" ? "Purchase" : "Sale"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user