Files
Neon-Desk/app/analysis/page.tsx

182 lines
6.5 KiB
TypeScript

'use client';
import { useQueryClient } from '@tanstack/react-query';
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { AppShell } from '@/components/shell/app-shell';
import { CompanyAnalysisSkeleton } from '@/components/analysis/company-analysis-skeleton';
import { AnalysisToolbar } from '@/components/analysis/analysis-toolbar';
import { BullBearPanel } from '@/components/analysis/bull-bear-panel';
import { CompanyOverviewCard } from '@/components/analysis/company-overview-card';
import { CompanyProfileFactsTable } from '@/components/analysis/company-profile-facts-table';
import { PriceHistoryCard } from '@/components/analysis/price-history-card';
import { RecentDevelopmentsSection } from '@/components/analysis/recent-developments-section';
import { ValuationFactsTable } from '@/components/analysis/valuation-facts-table';
import { Panel } from '@/components/ui/panel';
import { useAuthGuard } from '@/hooks/use-auth-guard';
import { useLinkPrefetch } from '@/hooks/use-link-prefetch';
import { ensureTickerAutomation } from '@/lib/api';
import { buildGraphingHref } from '@/lib/graphing/catalog';
import { queryKeys } from '@/lib/query/keys';
import { companyAnalysisQueryOptions } from '@/lib/query/options';
import type { CompanyAnalysis } from '@/lib/types';
function normalizeTickerInput(value: string | null) {
const normalized = value?.trim().toUpperCase() ?? '';
return normalized || null;
}
export default function AnalysisPage() {
return (
<Suspense fallback={<div className="flex min-h-screen items-center justify-center text-sm text-[color:var(--terminal-muted)]">Loading overview...</div>}>
<AnalysisPageContent />
</Suspense>
);
}
function AnalysisPageContent() {
const { isPending, isAuthenticated } = useAuthGuard();
const searchParams = useSearchParams();
const queryClient = useQueryClient();
const { prefetchResearchTicker } = useLinkPrefetch();
const initialTicker = normalizeTickerInput(searchParams.get('ticker')) ?? 'MSFT';
const [tickerInput, setTickerInput] = useState(initialTicker);
const [ticker, setTicker] = useState(initialTicker);
const [analysis, setAnalysis] = useState<CompanyAnalysis | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const normalized = normalizeTickerInput(searchParams.get('ticker'));
if (!normalized) {
return;
}
setTickerInput(normalized);
setTicker(normalized);
}, [searchParams]);
const loadAnalysis = useCallback(async (symbol: string, options?: { refresh?: boolean }) => {
const queryOptions = companyAnalysisQueryOptions(symbol, options);
if (!queryClient.getQueryData(queryOptions.queryKey)) {
setLoading(true);
}
setError(null);
try {
const response = await queryClient.fetchQuery(queryOptions);
setAnalysis(response.analysis);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unable to load company overview');
setAnalysis((current) => {
const normalizedTicker = symbol.trim().toUpperCase();
if (options?.refresh && current?.company.ticker === normalizedTicker) {
return current;
}
return null;
});
} finally {
setLoading(false);
}
}, [queryClient]);
useEffect(() => {
if (!isPending && isAuthenticated) {
void loadAnalysis(ticker);
}
}, [isPending, isAuthenticated, loadAnalysis, ticker]);
const activeTicker = analysis?.company.ticker ?? ticker;
const quickLinks = useMemo(() => ({
research: `/research?ticker=${encodeURIComponent(activeTicker)}`,
filings: `/filings?ticker=${encodeURIComponent(activeTicker)}`,
financials: `/financials?ticker=${encodeURIComponent(activeTicker)}`,
graphing: buildGraphingHref(activeTicker)
}), [activeTicker]);
if (isPending || !isAuthenticated) {
return <div className="flex min-h-screen items-center justify-center text-sm text-[color:var(--terminal-muted)]">Loading overview...</div>;
}
return (
<AppShell
title="Company Overview"
subtitle="A summary-first view of price, business context, valuation, recent developments, and key debate points."
activeTicker={activeTicker}
actions={null}
>
<AnalysisToolbar
tickerInput={tickerInput}
currentTicker={activeTicker}
onTickerInputChange={setTickerInput}
onSubmit={(event) => {
event.preventDefault();
const normalized = tickerInput.trim().toUpperCase();
if (!normalized) {
return;
}
void ensureTickerAutomation({
ticker: normalized,
source: 'analysis'
});
setTicker(normalized);
}}
onRefresh={() => {
const normalizedTicker = activeTicker.trim().toUpperCase();
void queryClient.invalidateQueries({ queryKey: queryKeys.companyAnalysis(normalizedTicker) });
void loadAnalysis(normalizedTicker, { refresh: true });
}}
quickLinks={quickLinks}
onLinkPrefetch={() => prefetchResearchTicker(activeTicker)}
/>
{error ? (
<Panel>
<p className="text-sm text-[#ffb5b5]">{error}</p>
</Panel>
) : null}
{!analysis && loading ? (
<CompanyAnalysisSkeleton />
) : analysis ? (
<>
<section className="grid gap-6 xl:grid-cols-[minmax(320px,1fr)_minmax(0,2fr)]">
<CompanyOverviewCard
analysis={analysis}
/>
<PriceHistoryCard
loading={loading}
priceHistory={analysis.priceHistory}
benchmarkHistory={analysis.benchmarkHistory}
quote={analysis.quote}
position={analysis.position}
/>
</section>
<section className="grid gap-6 xl:grid-cols-2">
<CompanyProfileFactsTable analysis={analysis} />
<ValuationFactsTable analysis={analysis} />
</section>
<BullBearPanel
bullBear={analysis.bullBear}
researchHref={quickLinks.research}
onLinkPrefetch={() => prefetchResearchTicker(activeTicker)}
/>
<RecentDevelopmentsSection recentDevelopments={analysis.recentDevelopments} />
</>
) : (
<Panel>
<p className="text-sm text-[color:var(--terminal-muted)]">No overview is available for the selected ticker.</p>
</Panel>
)}
</AppShell>
);
}