upgrade navigation and route prefetch responsiveness

This commit is contained in:
2026-03-01 20:45:08 -05:00
parent d6895f185f
commit dc84f34fe9
17 changed files with 1208 additions and 142 deletions

View File

@@ -1,5 +1,6 @@
'use client';
import { useQueryClient } from '@tanstack/react-query';
import { useCallback, useEffect, useState } from 'react';
import { ArrowRight, Eye, Plus, Trash2 } from 'lucide-react';
import Link from 'next/link';
@@ -8,10 +9,13 @@ import { Panel } from '@/components/ui/panel';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { StatusPill } from '@/components/ui/status-pill';
import { useLinkPrefetch } from '@/hooks/use-link-prefetch';
import { useAuthGuard } from '@/hooks/use-auth-guard';
import { useTaskPoller } from '@/hooks/use-task-poller';
import { deleteWatchlistItem, getTask, listWatchlist, queueFilingSync, upsertWatchlistItem } from '@/lib/api';
import { deleteWatchlistItem, queueFilingSync, upsertWatchlistItem } from '@/lib/api';
import type { Task, WatchlistItem } from '@/lib/types';
import { queryKeys } from '@/lib/query/keys';
import { taskQueryOptions, watchlistQueryOptions } from '@/lib/query/options';
type FormState = {
ticker: string;
@@ -21,6 +25,8 @@ type FormState = {
export default function WatchlistPage() {
const { isPending, isAuthenticated } = useAuthGuard();
const queryClient = useQueryClient();
const { prefetchResearchTicker } = useLinkPrefetch();
const [items, setItems] = useState<WatchlistItem[]>([]);
const [loading, setLoading] = useState(true);
@@ -29,18 +35,23 @@ export default function WatchlistPage() {
const [form, setForm] = useState<FormState>({ ticker: '', companyName: '', sector: '' });
const loadWatchlist = useCallback(async () => {
setLoading(true);
const options = watchlistQueryOptions();
if (!queryClient.getQueryData(options.queryKey)) {
setLoading(true);
}
setError(null);
try {
const response = await listWatchlist();
const response = await queryClient.ensureQueryData(options);
setItems(response.items);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load watchlist');
} finally {
setLoading(false);
}
}, []);
}, [queryClient]);
useEffect(() => {
if (!isPending && isAuthenticated) {
@@ -52,6 +63,8 @@ export default function WatchlistPage() {
taskId: activeTask?.id ?? null,
onTerminalState: () => {
setActiveTask(null);
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
void queryClient.invalidateQueries({ queryKey: ['filings'] });
}
});
@@ -68,6 +81,7 @@ export default function WatchlistPage() {
});
setForm({ ticker: '', companyName: '', sector: '' });
void queryClient.invalidateQueries({ queryKey: queryKeys.watchlist() });
await loadWatchlist();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to save watchlist item');
@@ -77,8 +91,9 @@ export default function WatchlistPage() {
const queueSync = async (ticker: string) => {
try {
const { task } = await queueFilingSync({ ticker, limit: 20 });
const latest = await getTask(task.id);
const latest = await queryClient.fetchQuery(taskQueryOptions(task.id));
setActiveTask(latest.task);
void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) });
} catch (err) {
setError(err instanceof Error ? err.message : `Failed to queue sync for ${ticker}`);
}
@@ -128,6 +143,8 @@ export default function WatchlistPage() {
</Button>
<Link
href={`/filings?ticker=${item.ticker}`}
onMouseEnter={() => prefetchResearchTicker(item.ticker)}
onFocus={() => prefetchResearchTicker(item.ticker)}
className="inline-flex items-center gap-1 text-xs text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]"
>
Open stream
@@ -135,6 +152,8 @@ export default function WatchlistPage() {
</Link>
<Link
href={`/analysis?ticker=${item.ticker}`}
onMouseEnter={() => prefetchResearchTicker(item.ticker)}
onFocus={() => prefetchResearchTicker(item.ticker)}
className="inline-flex items-center gap-1 text-xs text-[color:var(--accent)] hover:text-[color:var(--accent-strong)]"
>
Analyze
@@ -146,6 +165,7 @@ export default function WatchlistPage() {
onClick={async () => {
try {
await deleteWatchlistItem(item.id);
void queryClient.invalidateQueries({ queryKey: queryKeys.watchlist() });
await loadWatchlist();
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to remove symbol');