Jaspers Hub

biseroff / massive

pluginReviewed★ 0↓ 3

Massive Market Data (Polygon.io): Market data plugin wrapping the Polygon.io/Massive REST API: bars, snapshots, options, news, financials, indicators, and four chart/watchlist views.

3 KB
1import { useBridge, useData, usePublish, type PanelRef } from '@jaspers-ai/sdk'
2import './styles.css'
3
4interface Article {
5 id: string
6 published: string | null
7 title: string | null
8 publisher: string | null
9 author: string | null
10 url: string | null
11 summary: string | null
12 tickers: string | null
13 insights: string | null
14 sentiment: string | null
15 sentiment_ticker: string | null
16 sentiment_reasoning: string | null
17 [field: string]: unknown
18}
19
20interface State {
21 tickers?: string[]
22 limit?: number
23}
24
25const when = (s: string | null) => (s ? new Date(s).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' }) : '')
26const tone = (s: string | null) => (s === 'positive' ? 'mv-up' : s === 'negative' ? 'mv-down' : '')
27
28export function NewsView({ panel }: { panel: PanelRef }) {
29 const state = useData(`workspaces/${panel.workspaceId}/panels/${panel.id}/state`) as State | undefined
30 if (state === undefined) return <p className="mv-note">Loading…</p>
31 const tickers = (state.tickers ?? ['AAPL']).map((t) => t.trim().toUpperCase()).filter(Boolean)
32 return <List panel={panel} tickers={tickers} limit={state.limit ?? 50} />
33}
34
35function List({ panel, tickers, limit }: { panel: PanelRef; tickers: string[]; limit: number }) {
36 const bridge = useBridge()
37 const { data, error, loading, refetch } = useData('massive/news', { tickers, limit })
38 const rows = (data ?? []) as Article[]
39 usePublish(panel, { tickers, count: rows.length, latest: rows[0]?.title ?? null, error: error ?? null })
40
41 return (
42 <div className="mv-wrap">
43 <div className="mv-bar">
44 <span className="mv-title">News · {tickers.join(', ') || 'all'}</span>
45 <button className="mv-button" onClick={() => refetch()} disabled={loading}>
46 {loading ? 'Loading…' : 'Refresh'}
47 </button>
48 </div>
49 {error ? <p className="mv-note mv-error">{error}</p> : null}
50 {!data && !error ? <p className="mv-note">Loading…</p> : null}
51 {data && !rows.length ? <p className="mv-note">No articles.</p> : null}
52 <ul className="mv-news">
53 {rows.map((a) => (
54 <li key={a.id ?? a.url ?? a.title}>
55 <a
56 className="mv-headline"
57 href={a.url ?? undefined}
58 onClick={(e) => {
59 e.preventDefault()
60 if (a.url) bridge.openLink(a.url)
61 }}
62 >
63 {a.title}
64 </a>
65 <div className="mv-meta">
66 {[a.publisher, a.author, when(a.published)].filter(Boolean).join(' · ')}
67 {a.tickers ? ` · ${a.tickers}` : ''}
68 </div>
69 {a.summary ? <p className="mv-summary">{a.summary}</p> : null}
70 {a.sentiment ? (
71 <div>
72 <span className={`mv-tag ${tone(a.sentiment)}`}>
73 {a.sentiment_ticker}: {a.sentiment}
74 </span>
75 {a.sentiment_reasoning ? <span className="mv-meta"> {a.sentiment_reasoning}</span> : null}
76 </div>
77 ) : null}
78 </li>
79 ))}
80 </ul>
81 </div>
82 )
83}