upgrade navigation and route prefetch responsiveness
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Bar } from 'recharts';
|
||||
import { PieChart, Pie, Cell, Tooltip, Legend, ResponsiveContainer, BarChart, CartesianGrid, XAxis, YAxis, Bar } from 'recharts';
|
||||
import { BrainCircuit, Plus, RefreshCcw, Trash2 } from 'lucide-react';
|
||||
import { AppShell } from '@/components/shell/app-shell';
|
||||
import { Panel } from '@/components/ui/panel';
|
||||
@@ -12,16 +13,19 @@ import { useAuthGuard } from '@/hooks/use-auth-guard';
|
||||
import { useTaskPoller } from '@/hooks/use-task-poller';
|
||||
import {
|
||||
deleteHolding,
|
||||
getLatestPortfolioInsight,
|
||||
getTask,
|
||||
getPortfolioSummary,
|
||||
listHoldings,
|
||||
queuePortfolioInsights,
|
||||
queuePriceRefresh,
|
||||
upsertHolding
|
||||
} from '@/lib/api';
|
||||
import type { Holding, PortfolioInsight, PortfolioSummary, Task } from '@/lib/types';
|
||||
import { asNumber, formatCurrency, formatPercent } from '@/lib/format';
|
||||
import { queryKeys } from '@/lib/query/keys';
|
||||
import {
|
||||
holdingsQueryOptions,
|
||||
latestPortfolioInsightQueryOptions,
|
||||
portfolioSummaryQueryOptions,
|
||||
taskQueryOptions
|
||||
} from '@/lib/query/options';
|
||||
|
||||
type FormState = {
|
||||
ticker: string;
|
||||
@@ -31,6 +35,11 @@ type FormState = {
|
||||
};
|
||||
|
||||
const CHART_COLORS = ['#6effd8', '#5fd3ff', '#66ffa1', '#8dbbff', '#f4f88f', '#ff9c9c'];
|
||||
const CHART_TEXT = '#e8fff8';
|
||||
const CHART_MUTED = '#b4ced9';
|
||||
const CHART_GRID = 'rgba(126, 217, 255, 0.24)';
|
||||
const CHART_TOOLTIP_BG = 'rgba(6, 17, 24, 0.95)';
|
||||
const CHART_TOOLTIP_BORDER = 'rgba(123, 255, 217, 0.45)';
|
||||
|
||||
const EMPTY_SUMMARY: PortfolioSummary = {
|
||||
positions: 0,
|
||||
@@ -42,6 +51,7 @@ const EMPTY_SUMMARY: PortfolioSummary = {
|
||||
|
||||
export default function PortfolioPage() {
|
||||
const { isPending, isAuthenticated } = useAuthGuard();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [holdings, setHoldings] = useState<Holding[]>([]);
|
||||
const [summary, setSummary] = useState<PortfolioSummary>(EMPTY_SUMMARY);
|
||||
@@ -52,14 +62,21 @@ export default function PortfolioPage() {
|
||||
const [form, setForm] = useState<FormState>({ ticker: '', shares: '', avgCost: '', currentPrice: '' });
|
||||
|
||||
const loadPortfolio = useCallback(async () => {
|
||||
setLoading(true);
|
||||
const holdingsOptions = holdingsQueryOptions();
|
||||
const summaryOptions = portfolioSummaryQueryOptions();
|
||||
const insightOptions = latestPortfolioInsightQueryOptions();
|
||||
|
||||
if (!queryClient.getQueryData(summaryOptions.queryKey)) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const [holdingsRes, summaryRes, insightRes] = await Promise.all([
|
||||
listHoldings(),
|
||||
getPortfolioSummary(),
|
||||
getLatestPortfolioInsight()
|
||||
queryClient.ensureQueryData(holdingsOptions),
|
||||
queryClient.ensureQueryData(summaryOptions),
|
||||
queryClient.ensureQueryData(insightOptions)
|
||||
]);
|
||||
|
||||
setHoldings(holdingsRes.holdings);
|
||||
@@ -70,7 +87,7 @@ export default function PortfolioPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPending && isAuthenticated) {
|
||||
@@ -82,6 +99,10 @@ export default function PortfolioPage() {
|
||||
taskId: activeTask?.id ?? null,
|
||||
onTerminalState: async () => {
|
||||
setActiveTask(null);
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.holdings() });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.portfolioSummary() });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.latestPortfolioInsight() });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
|
||||
await loadPortfolio();
|
||||
}
|
||||
});
|
||||
@@ -116,6 +137,8 @@ export default function PortfolioPage() {
|
||||
});
|
||||
|
||||
setForm({ ticker: '', shares: '', avgCost: '', currentPrice: '' });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.holdings() });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.portfolioSummary() });
|
||||
await loadPortfolio();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to save holding');
|
||||
@@ -125,8 +148,10 @@ export default function PortfolioPage() {
|
||||
const queueRefresh = async () => {
|
||||
try {
|
||||
const { task } = await queuePriceRefresh();
|
||||
const latest = await getTask(task.id);
|
||||
const latest = await queryClient.fetchQuery(taskQueryOptions(task.id));
|
||||
setActiveTask(latest.task);
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.portfolioSummary() });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unable to queue price refresh');
|
||||
}
|
||||
@@ -135,8 +160,10 @@ export default function PortfolioPage() {
|
||||
const queueInsights = async () => {
|
||||
try {
|
||||
const { task } = await queuePortfolioInsights();
|
||||
const latest = await getTask(task.id);
|
||||
const latest = await queryClient.fetchQuery(taskQueryOptions(task.id));
|
||||
setActiveTask(latest.task);
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.latestPortfolioInsight() });
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unable to queue portfolio insights');
|
||||
}
|
||||
@@ -148,7 +175,7 @@ export default function PortfolioPage() {
|
||||
|
||||
return (
|
||||
<AppShell
|
||||
title="Portfolio Matrix"
|
||||
title="Portfolio"
|
||||
subtitle="Position management, market valuation, and AI generated portfolio commentary."
|
||||
actions={(
|
||||
<>
|
||||
@@ -209,7 +236,22 @@ export default function PortfolioPage() {
|
||||
<Cell key={`${entry.name}-${index}`} fill={CHART_COLORS[index % CHART_COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip formatter={(value: number | string | undefined) => formatCurrency(value)} />
|
||||
<Tooltip
|
||||
formatter={(value: number | string | undefined) => formatCurrency(value)}
|
||||
contentStyle={{
|
||||
backgroundColor: CHART_TOOLTIP_BG,
|
||||
border: `1px solid ${CHART_TOOLTIP_BORDER}`,
|
||||
borderRadius: '0.75rem'
|
||||
}}
|
||||
labelStyle={{ color: CHART_TEXT }}
|
||||
itemStyle={{ color: CHART_TEXT }}
|
||||
/>
|
||||
<Legend
|
||||
verticalAlign="bottom"
|
||||
iconType="circle"
|
||||
wrapperStyle={{ paddingTop: '0.75rem' }}
|
||||
formatter={(value) => <span style={{ color: CHART_TEXT }}>{value}</span>}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
@@ -225,10 +267,33 @@ export default function PortfolioPage() {
|
||||
<div className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={performanceData}>
|
||||
<CartesianGrid strokeDasharray="2 2" stroke="rgba(126, 217, 255, 0.2)" />
|
||||
<XAxis dataKey="name" stroke="#8cb6c5" fontSize={12} />
|
||||
<YAxis stroke="#8cb6c5" fontSize={12} />
|
||||
<Tooltip formatter={(value: number | string | undefined) => `${asNumber(value).toFixed(2)}%`} />
|
||||
<CartesianGrid strokeDasharray="2 2" stroke={CHART_GRID} />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
stroke={CHART_MUTED}
|
||||
fontSize={12}
|
||||
axisLine={{ stroke: CHART_MUTED }}
|
||||
tickLine={{ stroke: CHART_MUTED }}
|
||||
tick={{ fill: CHART_MUTED }}
|
||||
/>
|
||||
<YAxis
|
||||
stroke={CHART_MUTED}
|
||||
fontSize={12}
|
||||
axisLine={{ stroke: CHART_MUTED }}
|
||||
tickLine={{ stroke: CHART_MUTED }}
|
||||
tick={{ fill: CHART_MUTED }}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value: number | string | undefined) => `${asNumber(value).toFixed(2)}%`}
|
||||
contentStyle={{
|
||||
backgroundColor: CHART_TOOLTIP_BG,
|
||||
border: `1px solid ${CHART_TOOLTIP_BORDER}`,
|
||||
borderRadius: '0.75rem'
|
||||
}}
|
||||
labelStyle={{ color: CHART_TEXT }}
|
||||
itemStyle={{ color: CHART_TEXT }}
|
||||
cursor={{ fill: 'rgba(104, 255, 213, 0.08)' }}
|
||||
/>
|
||||
<Bar dataKey="value" fill="#68ffd5" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
@@ -277,6 +342,8 @@ export default function PortfolioPage() {
|
||||
onClick={async () => {
|
||||
try {
|
||||
await deleteHolding(holding.id);
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.holdings() });
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.portfolioSummary() });
|
||||
await loadPortfolio();
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to delete holding');
|
||||
|
||||
Reference in New Issue
Block a user