'use client';
import { useSession } from 'next-auth/react';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell, Legend } from 'recharts';
import { format } from 'date-fns';
import Link from 'next/link';
export default function PortfolioPage() {
const { data: session, status } = useSession();
const router = useRouter();
const [portfolio, setPortfolio] = useState([]);
const [summary, setSummary] = useState({ total_value: 0, total_gain_loss: 0, cost_basis: 0 });
const [loading, setLoading] = useState(true);
const [showAddModal, setShowAddModal] = useState(false);
const [newHolding, setNewHolding] = useState({ ticker: '', shares: '', avg_cost: '' });
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/auth/signin');
return;
}
if (session?.user) {
fetchPortfolio(session.user.id);
}
}, [session, status, router]);
const fetchPortfolio = async (userId: string) => {
try {
const [portfolioRes, summaryRes] = await Promise.all([
fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/portfolio/${userId}`),
fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/portfolio/${userId}/summary`)
]);
const portfolioData = await portfolioRes.json();
const summaryData = await summaryRes.json();
setPortfolio(portfolioData);
setSummary(summaryData);
} catch (error) {
console.error('Error fetching portfolio:', error);
} finally {
setLoading(false);
}
};
const handleAddHolding = async (e: React.FormEvent) => {
e.preventDefault();
try {
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/portfolio`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
user_id: session?.user?.id,
ticker: newHolding.ticker.toUpperCase(),
shares: parseFloat(newHolding.shares),
avg_cost: parseFloat(newHolding.avg_cost)
})
});
setShowAddModal(false);
setNewHolding({ ticker: '', shares: '', avg_cost: '' });
fetchPortfolio(session?.user?.id);
} catch (error) {
console.error('Error adding holding:', error);
}
};
const handleDeleteHolding = async (id: number) => {
if (!confirm('Are you sure you want to delete this holding?')) return;
try {
await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/portfolio/${id}`, {
method: 'DELETE'
});
fetchPortfolio(session?.user?.id);
} catch (error) {
console.error('Error deleting holding:', error);
}
};
if (loading) {
return
Loading...
;
}
const pieData = portfolio.length > 0 ? portfolio.map((p: any) => ({
name: p.ticker,
value: p.current_value || (p.shares * p.avg_cost)
})) : [];
const COLORS = ['#3b82f6', '#8b5cf6', '#10b981', '#f59e0b', '#ef4444', '#ec4899'];
return (
Portfolio
Total Value
${summary.total_value?.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '$0.00'}
Total Gain/Loss
= 0 ? 'text-green-400' : 'text-red-400'}`}>
{summary.total_gain_loss >= 0 ? '+' : ''}${summary.total_gain_loss?.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }) || '$0.00'}
Positions
{portfolio.length}
Portfolio Allocation
{pieData.length > 0 ? (
`${entry.name} ($${(entry.value / 1000).toFixed(1)}k)`}
>
{pieData.map((entry, index) => (
|
))}
) : (
No holdings yet
)}
Performance
{portfolio.length > 0 ? (
({ name: p.ticker, value: p.gain_loss_pct || 0 }))}>
) : (
No performance data yet
)}
| Ticker |
Shares |
Avg Cost |
Current Price |
Value |
Gain/Loss |
% |
Actions |
{portfolio.map((holding: any) => (
| {holding.ticker} |
{holding.shares.toLocaleString()} |
${holding.avg_cost.toFixed(2)} |
${holding.current_price?.toFixed(2) || 'N/A'} |
${holding.current_value?.toFixed(2) || 'N/A'} |
= 0 ? 'text-green-400' : 'text-red-400'}`}>
{holding.gain_loss >= 0 ? '+' : ''}${holding.gain_loss?.toFixed(2) || '0.00'}
|
= 0 ? 'text-green-400' : 'text-red-400'}`}>
{holding.gain_loss_pct >= 0 ? '+' : ''}{holding.gain_loss_pct?.toFixed(2) || '0.00'}%
|
|
))}
{showAddModal && (
)}
);
}