'use client'; import { Bell, Trash2 } from 'lucide-react'; import Link from 'next/link'; import { useCallback, useEffect, useState } from 'react'; import { EventList } from '@/components/events/event-row'; import { EventTypeBadge } from '@/components/ui/badges'; import { LiveAgo } from '@/components/ui/live'; import { Empty, Note, Section } from '@/components/ui/section'; import { SkeletonRows } from '@/components/ui/skeleton'; import { ownerApi } from '@/lib/client-api'; import { EVENT_TYPES } from '@/lib/event-styles'; import { readWatched, setOwnerToken, useOwnerToken, writeWatched } from '@/lib/owner'; import { routes } from '@/lib/site'; import type { Alert, AlertDelivery, WatchlistPayload } from '@/lib/types'; import { CompanyTable } from './company-table'; /** /watchlist: watched companies with their latest events, alert rules and recent deliveries — all keyed by the owner token. */ export function WatchlistClient() { const token = useOwnerToken(true); const [data, setData] = useState(null); const [alerts, setAlerts] = useState([]); const [deliveries, setDeliveries] = useState([]); const [error, setError] = useState(null); const [loading, setLoading] = useState(true); const [showToken, setShowToken] = useState(false); const [importDraft, setImportDraft] = useState(''); const [form, setForm] = useState<{ name: string; company: string; event_types: string[]; min_importance: string; channel: 'web' | 'webhook'; target: string }>({ name: '', company: '', event_types: [], min_importance: '', channel: 'web', target: '' }); const load = useCallback(async () => { if (!token) return; const api = ownerApi(token); setLoading(true); try { const [w, a, d] = await Promise.all([api.watchlist(), api.alerts().catch(() => ({ items: [] })), api.deliveries(30).catch(() => ({ items: [] }))]); setData(w); setAlerts(a.items ?? []); setDeliveries(d.items ?? []); setError(null); // reconcile the local mirror with the server truth writeWatched(w.items.map((c) => c.slug)); } catch (e) { setError((e as Error).message); // fall back to the local mirror for names only if (!data) setData({ items: [], events: [] }); } finally { setLoading(false); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [token]); useEffect(() => { load(); }, [load]); const remove = async (slug: string) => { if (!token) return; writeWatched(readWatched().filter((s) => s !== slug)); setData((d) => (d ? { ...d, items: d.items.filter((c) => c.slug !== slug) } : d)); try { await ownerApi(token).unwatch(slug); } catch { /* reload will reconcile */ } }; const createAlert = async (e: React.FormEvent) => { e.preventDefault(); if (!token || !form.name.trim()) return; try { const a = await ownerApi(token).createAlert({ name: form.name.trim(), company: form.company || undefined, condition: { event_types: form.event_types.length ? form.event_types : undefined, min_importance: form.min_importance ? Number(form.min_importance) : undefined }, channel: form.channel, target: form.channel === 'webhook' ? form.target : undefined }); setAlerts((l) => [...l, a]); setForm({ name: '', company: '', event_types: [], min_importance: '', channel: 'web', target: '' }); } catch (err) { setError((err as Error).message); } }; const deleteAlert = async (id: string) => { if (!token) return; setAlerts((l) => l.filter((a) => a.id !== id)); try { await ownerApi(token).deleteAlert(id); } catch { /* ignore */ } }; const localOnly = readWatched(); return (
{error && (

Watchlist service: {error}. Your locally saved list ({localOnly.length}) is kept and will sync when the API answers.

)}
{loading && !data ? ( ) : data && data.items.length ? ( <>
    {data.items.map((c) => (
  • ))}
{data.items.length >= 2 && (

c.slug))} className="link"> Compare watched companies →

)} ) : ( Use the Watch button on any company page. No account is needed — the list is tied to a token stored in this browser. )}
{data ? : }
setForm({ ...form, name: e.target.value })} placeholder="Rule name (required)" className="field" aria-label="Rule name" required /> {form.channel === 'webhook' && setForm({ ...form, target: e.target.value })} placeholder="https://your-endpoint.example/hook" className="field md:col-span-5" aria-label="Webhook URL" type="url" required />}
{EVENT_TYPES.map((t) => { const on = form.event_types.includes(t); return ( ); })} no selection = all types
{alerts.length > 0 ? (
    {alerts.map((a) => (
  • {a.name} {a.company ? `company ${a.company}` : 'any watched company'} {(a.condition.event_types ?? []).map((t) => ( ))} {a.condition.min_importance !== undefined && importance ≥ {Math.round(a.condition.min_importance * 100)}} {a.channel}
  • ))}
) : ( No rules yet. )} {deliveries.length > 0 && (

Recent deliveries

    {deliveries.map((d) => (
  • {d.alert_name ?? d.alert_id} {d.event ? ( {d.event.title} ) : ( {d.event_id} )} {d.channel} · {d.status}
  • ))}
)}

Watchlists and alerts belong to a random token generated by this browser and stored in localStorage; the server keeps only a hash. Clearing site data loses the list — copy the token to move it to another device.

{showToken && token && {token}}
{ e.preventDefault(); if (setOwnerToken(importDraft)) { setImportDraft(''); load(); } }} > setImportDraft(e.target.value)} placeholder="Paste a token from another device" className="field flex-1 text-xs" aria-label="Import token" />
); }