Jaspers Hub

jaspers / earningscall

pluginOfficialReviewed★ 0↓ 6

EarningsCall Transcripts & Calendar: Shows a company's earnings call transcripts and the daily earnings calendar from EarningsCall.biz in a single panel view.

3.3 KB
1import type { ReactElement } from 'react'
2import { useData, type PanelRef } from '@jaspers-ai/sdk'
3import { answers, calendarOutput, readCalendarRows, shiftDay, today, type CallsState } from './calls'
4import { cleanError, localTime, longDate, Publish, TickerField } from './shared'
5
6// The calls held on one day. A row opens its call in this panel and keeps the day, so the way back
7// lands on the same calendar.
8
9export function Calendar({ panel, state, write }: { panel: PanelRef; state: CallsState; write: (next: CallsState) => void }): ReactElement {
10 const now = today(new Date())
11 const day = state.date ?? now
12 const run = useData('earningscall/calendar', { date: day })
13 // Until the new day is in, the run still holds the last day's calls.
14 const fresh = run.data !== undefined && answers(run.meta, { date: day })
15 const rows = fresh ? readCalendarRows(run.data) : []
16 const error = run.error ? cleanError(run.error) : null
17 const go = (date: string | null): void => write({ ...state, date })
18 return (
19 <div className="ec-root">
20 {(fresh || error) && <Publish panel={panel} output={{ ...calendarOutput(day, rows, error) }} text={null} />}
21 <div className="ec-bar">
22 <button type="button" className="ec-button" title="Day before" onClick={() => go(shiftDay(day, -1))}>
23 ‹
24 </button>
25 <span className="ec-title">{longDate(day)}</span>
26 <button type="button" className="ec-button" title="Day after" onClick={() => go(shiftDay(day, 1))}>
27 ›
28 </button>
29 {day !== now && (
30 <button type="button" className="ec-button" onClick={() => go(null)}>
31 Today
32 </button>
33 )}
34 {run.loading && <span className="ec-muted">loading…</span>}
35 <span className="ec-grow" />
36 <TickerField symbol="" onSymbol={(symbol) => write({ ...state, symbol, exchange: null, year: null, quarter: null })} />
37 </div>
38 <div className="ec-body">
39 {error ? (
40 <p className="ec-error">{error}</p>
41 ) : !fresh ? (
42 <p className="ec-note">Loading…</p>
43 ) : rows.length === 0 ? (
44 <p className="ec-note">No earnings calls on this day.</p>
45 ) : (
46 <table className="ec-table">
47 <thead>
48 <tr>
49 <th>Time</th>
50 <th>Ticker</th>
51 <th>Company</th>
52 <th>Call</th>
53 <th>Transcript</th>
54 </tr>
55 </thead>
56 <tbody>
57 {rows.map((row) => (
58 <tr
59 key={`${row.exchange}:${row.symbol}:${row.year}:${row.quarter}`}
60 className="ec-row"
61 title={`Open the Q${row.quarter} ${row.year} call`}
62 onClick={() => write({ ...state, symbol: row.symbol, exchange: row.exchange, year: row.year, quarter: row.quarter, date: day })}
63 >
64 <td className="ec-num">{localTime(row.date)}</td>
65 <td>{row.symbol}</td>
66 <td className="ec-company">{row.company}</td>
67 <td>
68 Q{row.quarter} {row.year}
69 </td>
70 <td className={row.ready ? undefined : 'ec-muted'}>{row.ready ? 'Ready' : 'Not yet'}</td>
71 </tr>
72 ))}
73 </tbody>
74 </table>
75 )}
76 </div>
77 </div>
78 )
79}