upgrade navigation and route prefetch responsiveness
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import Link from 'next/link';
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
@@ -25,12 +26,14 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Panel } from '@/components/ui/panel';
|
||||
import { useAuthGuard } from '@/hooks/use-auth-guard';
|
||||
import { getCompanyAnalysis } from '@/lib/api';
|
||||
import { useLinkPrefetch } from '@/hooks/use-link-prefetch';
|
||||
import {
|
||||
formatCurrencyByScale,
|
||||
formatPercent,
|
||||
type NumberScaleUnit
|
||||
} from '@/lib/format';
|
||||
import { queryKeys } from '@/lib/query/keys';
|
||||
import { companyAnalysisQueryOptions } from '@/lib/query/options';
|
||||
import type { CompanyAnalysis } from '@/lib/types';
|
||||
|
||||
type StatementPeriodPoint = {
|
||||
@@ -76,6 +79,16 @@ const FINANCIAL_VALUE_SCALE_OPTIONS: Array<{ value: NumberScaleUnit; label: stri
|
||||
{ value: 'billions', label: 'Billions (B)' }
|
||||
];
|
||||
|
||||
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)';
|
||||
|
||||
function renderLegendLabel(value: string) {
|
||||
return <span style={{ color: CHART_TEXT }}>{value}</span>;
|
||||
}
|
||||
|
||||
function formatShortDate(value: string) {
|
||||
const parsed = new Date(value);
|
||||
if (Number.isNaN(parsed.getTime())) {
|
||||
@@ -380,6 +393,8 @@ export default function FinancialsPage() {
|
||||
function FinancialsPageContent() {
|
||||
const { isPending, isAuthenticated } = useAuthGuard();
|
||||
const searchParams = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
const { prefetchResearchTicker } = useLinkPrefetch();
|
||||
|
||||
const [tickerInput, setTickerInput] = useState('MSFT');
|
||||
const [ticker, setTicker] = useState('MSFT');
|
||||
@@ -405,11 +420,16 @@ function FinancialsPageContent() {
|
||||
}, [searchParams]);
|
||||
|
||||
const loadFinancials = useCallback(async (symbol: string) => {
|
||||
setLoading(true);
|
||||
const options = companyAnalysisQueryOptions(symbol);
|
||||
|
||||
if (!queryClient.getQueryData(options.queryKey)) {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await getCompanyAnalysis(symbol);
|
||||
const response = await queryClient.ensureQueryData(options);
|
||||
setAnalysis(response.analysis);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unable to load financial history');
|
||||
@@ -417,7 +437,7 @@ function FinancialsPageContent() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
}, [queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPending && isAuthenticated) {
|
||||
@@ -535,8 +555,15 @@ function FinancialsPageContent() {
|
||||
<AppShell
|
||||
title="Financials"
|
||||
subtitle="Explore 10-K and 10-Q fundamentals, profitability, and balance sheet dynamics by ticker."
|
||||
activeTicker={analysis?.company.ticker ?? ticker}
|
||||
actions={(
|
||||
<Button variant="secondary" onClick={() => void loadFinancials(ticker)}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
void queryClient.invalidateQueries({ queryKey: queryKeys.companyAnalysis(ticker.trim().toUpperCase()) });
|
||||
void loadFinancials(ticker);
|
||||
}}
|
||||
>
|
||||
<RefreshCcw className="size-4" />
|
||||
Refresh
|
||||
</Button>
|
||||
@@ -566,10 +593,20 @@ function FinancialsPageContent() {
|
||||
</Button>
|
||||
{analysis ? (
|
||||
<>
|
||||
<Link href={`/analysis?ticker=${analysis.company.ticker}`} className="text-sm text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]">
|
||||
<Link
|
||||
href={`/analysis?ticker=${analysis.company.ticker}`}
|
||||
onMouseEnter={() => prefetchResearchTicker(analysis.company.ticker)}
|
||||
onFocus={() => prefetchResearchTicker(analysis.company.ticker)}
|
||||
className="text-sm text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]"
|
||||
>
|
||||
Open full analysis
|
||||
</Link>
|
||||
<Link href={`/filings?ticker=${analysis.company.ticker}`} className="text-sm text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]">
|
||||
<Link
|
||||
href={`/filings?ticker=${analysis.company.ticker}`}
|
||||
onMouseEnter={() => prefetchResearchTicker(analysis.company.ticker)}
|
||||
onFocus={() => prefetchResearchTicker(analysis.company.ticker)}
|
||||
className="text-sm text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]"
|
||||
>
|
||||
Open filings stream
|
||||
</Link>
|
||||
</>
|
||||
@@ -648,11 +685,36 @@ function FinancialsPageContent() {
|
||||
<div className="h-[330px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartSeries}>
|
||||
<CartesianGrid strokeDasharray="2 2" stroke="rgba(126, 217, 255, 0.2)" />
|
||||
<XAxis dataKey="label" minTickGap={20} stroke="#8cb6c5" fontSize={12} />
|
||||
<YAxis stroke="#8cb6c5" fontSize={12} tickFormatter={(value: number) => asAxisCurrencyTick(value, financialValueScale)} />
|
||||
<Tooltip formatter={(value) => asTooltipCurrency(value, financialValueScale)} />
|
||||
<Legend />
|
||||
<CartesianGrid strokeDasharray="2 2" stroke={CHART_GRID} />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
minTickGap={20}
|
||||
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 }}
|
||||
tickFormatter={(value: number) => asAxisCurrencyTick(value, financialValueScale)}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value) => asTooltipCurrency(value, financialValueScale)}
|
||||
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)' }}
|
||||
/>
|
||||
<Legend wrapperStyle={{ paddingTop: '0.75rem' }} formatter={renderLegendLabel} />
|
||||
<Bar dataKey="revenue" name="Revenue" fill="#68ffd5" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="netIncome" name="Net Income" fill="#5fd3ff" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
@@ -676,11 +738,36 @@ function FinancialsPageContent() {
|
||||
<stop offset="95%" stopColor="#68ffd5" stopOpacity={0} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="2 2" stroke="rgba(126, 217, 255, 0.2)" />
|
||||
<XAxis dataKey="label" minTickGap={20} stroke="#8cb6c5" fontSize={12} />
|
||||
<YAxis stroke="#8cb6c5" fontSize={12} tickFormatter={(value: number) => asAxisCurrencyTick(value, financialValueScale)} />
|
||||
<Tooltip formatter={(value) => asTooltipCurrency(value, financialValueScale)} />
|
||||
<Legend />
|
||||
<CartesianGrid strokeDasharray="2 2" stroke={CHART_GRID} />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
minTickGap={20}
|
||||
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 }}
|
||||
tickFormatter={(value: number) => asAxisCurrencyTick(value, financialValueScale)}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value) => asTooltipCurrency(value, financialValueScale)}
|
||||
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)' }}
|
||||
/>
|
||||
<Legend wrapperStyle={{ paddingTop: '0.75rem' }} formatter={renderLegendLabel} />
|
||||
<Area type="monotone" dataKey="totalAssets" name="Total Assets" stroke="#68ffd5" fill="url(#assetsGradient)" strokeWidth={2} />
|
||||
<Line type="monotone" dataKey="cash" name="Cash" stroke="#8bd3ff" strokeWidth={2} dot={false} />
|
||||
<Line type="monotone" dataKey="debt" name="Debt" stroke="#ffd08a" strokeWidth={2} dot={false} />
|
||||
@@ -701,9 +788,24 @@ function FinancialsPageContent() {
|
||||
<div className="h-[300px]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={chartSeries}>
|
||||
<CartesianGrid strokeDasharray="2 2" stroke="rgba(126, 217, 255, 0.2)" />
|
||||
<XAxis dataKey="label" minTickGap={20} stroke="#8cb6c5" fontSize={12} />
|
||||
<YAxis stroke="#8cb6c5" fontSize={12} tickFormatter={(value: number) => `${value.toFixed(0)}%`} />
|
||||
<CartesianGrid strokeDasharray="2 2" stroke={CHART_GRID} />
|
||||
<XAxis
|
||||
dataKey="label"
|
||||
minTickGap={20}
|
||||
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 }}
|
||||
tickFormatter={(value: number) => `${value.toFixed(0)}%`}
|
||||
/>
|
||||
<Tooltip
|
||||
formatter={(value) => {
|
||||
const normalized = normalizeTooltipValue(value);
|
||||
@@ -714,8 +816,16 @@ function FinancialsPageContent() {
|
||||
const numeric = Number(normalized);
|
||||
return Number.isFinite(numeric) ? `${numeric.toFixed(2)}%` : 'n/a';
|
||||
}}
|
||||
contentStyle={{
|
||||
backgroundColor: CHART_TOOLTIP_BG,
|
||||
border: `1px solid ${CHART_TOOLTIP_BORDER}`,
|
||||
borderRadius: '0.75rem'
|
||||
}}
|
||||
labelStyle={{ color: CHART_TEXT }}
|
||||
itemStyle={{ color: CHART_TEXT }}
|
||||
cursor={{ stroke: 'rgba(104, 255, 213, 0.35)', strokeWidth: 1 }}
|
||||
/>
|
||||
<Legend />
|
||||
<Legend wrapperStyle={{ paddingTop: '0.75rem' }} formatter={renderLegendLabel} />
|
||||
<Line type="monotone" dataKey="netMargin" name="Net Margin" stroke="#68ffd5" strokeWidth={2} dot={false} connectNulls />
|
||||
<Line type="monotone" dataKey="debtToAssets" name="Debt / Assets" stroke="#ffb980" strokeWidth={2} dot={false} connectNulls />
|
||||
</LineChart>
|
||||
|
||||
Reference in New Issue
Block a user