'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'; import { AppShell } from '@/components/shell/app-shell'; import { Panel } from '@/components/ui/panel'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { useLinkPrefetch } from '@/hooks/use-link-prefetch'; import { useAuthGuard } from '@/hooks/use-auth-guard'; import { deleteWatchlistItem, queueFilingSync, upsertWatchlistItem } from '@/lib/api'; import type { WatchlistItem } from '@/lib/types'; import { queryKeys } from '@/lib/query/keys'; import { watchlistQueryOptions } from '@/lib/query/options'; type FormState = { ticker: string; companyName: string; sector: string; category: string; tags: string; }; function parseTagsInput(input: string) { const unique = new Set(); for (const segment of input.split(',')) { const tag = segment.trim(); if (!tag) { continue; } unique.add(tag); } return [...unique]; } export default function WatchlistPage() { const { isPending, isAuthenticated } = useAuthGuard(); const queryClient = useQueryClient(); const { prefetchResearchTicker } = useLinkPrefetch(); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [form, setForm] = useState({ ticker: '', companyName: '', sector: '', category: '', tags: '' }); const loadWatchlist = useCallback(async () => { const options = watchlistQueryOptions(); if (!queryClient.getQueryData(options.queryKey)) { setLoading(true); } setError(null); try { 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) { void loadWatchlist(); } }, [isPending, isAuthenticated, loadWatchlist]); const submit = async (event: React.FormEvent) => { event.preventDefault(); try { await upsertWatchlistItem({ ticker: form.ticker.toUpperCase(), companyName: form.companyName, sector: form.sector || undefined, category: form.category || undefined, tags: parseTagsInput(form.tags) }); setForm({ ticker: '', companyName: '', sector: '', category: '', tags: '' }); void queryClient.invalidateQueries({ queryKey: queryKeys.watchlist() }); await loadWatchlist(); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save watchlist item'); } }; const queueSync = async (item: WatchlistItem) => { try { await queueFilingSync({ ticker: item.ticker, limit: 20, category: item.category ?? undefined, tags: item.tags.length > 0 ? item.tags : undefined }); void queryClient.invalidateQueries({ queryKey: queryKeys.recentTasks(20) }); void queryClient.invalidateQueries({ queryKey: ['filings'] }); } catch (err) { setError(err instanceof Error ? err.message : `Failed to queue sync for ${item.ticker}`); } }; if (isPending || !isAuthenticated) { return
Loading watchlist terminal...
; } return (
{error ?

{error}

: null} {loading ? (

Loading watchlist...

) : items.length === 0 ? (

No symbols yet. Add one from the right panel.

) : (
{items.map((item) => (

{item.sector ?? 'Unclassified'} {item.category ? ` ยท ${item.category}` : ''}

{item.ticker}

{item.company_name}

{item.tags.length > 0 ? (
{item.tags.map((tag) => ( {tag} ))}
) : null}
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 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
))}
)}
setForm((prev) => ({ ...prev, ticker: event.target.value.toUpperCase() }))} required />
setForm((prev) => ({ ...prev, companyName: event.target.value }))} required />
setForm((prev) => ({ ...prev, sector: event.target.value }))} />
setForm((prev) => ({ ...prev, category: event.target.value }))} placeholder="e.g. Core, Speculative, Watch only" />
setForm((prev) => ({ ...prev, tags: event.target.value }))} placeholder="Comma-separated tags" />
); }