167 lines
5.9 KiB
TypeScript
167 lines
5.9 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 { 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 { 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) => {
|
|
const options = companyAnalysisQueryOptions(symbol);
|
|
|
|
if (!queryClient.getQueryData(options.queryKey)) {
|
|
setLoading(true);
|
|
}
|
|
|
|
setError(null);
|
|
|
|
try {
|
|
const response = await queryClient.fetchQuery(options);
|
|
setAnalysis(response.analysis);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Unable to load company overview');
|
|
setAnalysis(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;
|
|
}
|
|
|
|
setTicker(normalized);
|
|
}}
|
|
onRefresh={() => {
|
|
const normalizedTicker = activeTicker.trim().toUpperCase();
|
|
void queryClient.invalidateQueries({ queryKey: queryKeys.companyAnalysis(normalizedTicker) });
|
|
void loadAnalysis(normalizedTicker);
|
|
}}
|
|
quickLinks={quickLinks}
|
|
onLinkPrefetch={() => prefetchResearchTicker(activeTicker)}
|
|
/>
|
|
|
|
{error ? (
|
|
<Panel>
|
|
<p className="text-sm text-[#ffb5b5]">{error}</p>
|
|
</Panel>
|
|
) : null}
|
|
|
|
{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>
|
|
);
|
|
}
|