phase-1: optimize code

This commit is contained in:
saikiranvella
2026-06-04 01:36:28 -04:00
parent 225b88ea4f
commit 5a4b4aa6d1
89 changed files with 11189 additions and 845 deletions
+8
View File
@@ -0,0 +1,8 @@
export async function load({ fetch }) {
const [callsRes, calRes] = await Promise.all([fetch('/api/calls'), fetch('/api/calls/calendar')]);
const { calls } = callsRes.ok ? await callsRes.json() : { calls: [] };
const { events } = calRes.ok ? await calRes.json() : { events: [] };
return { calls, events };
}
+420
View File
@@ -0,0 +1,420 @@
<script>
import { createCall, deleteCall } from '$lib/api.js';
import SignalBadge from '$lib/SignalBadge.svelte';
import Spinner from '$lib/Spinner.svelte';
import { invalidateAll } from '$app/navigation';
let { data } = $props();
// New call form state
let showForm = $state(false);
let saving = $state(false);
let formError = $state(null);
let form = $state({
title: '',
quarter: currentQuarter(),
date: today(),
thesis: '',
tickers: '',
});
function currentQuarter() {
const d = new Date();
const q = Math.ceil((d.getMonth() + 1) / 3);
return `Q${q} ${d.getFullYear()}`;
}
function today() {
return new Date().toISOString().slice(0, 10);
}
async function submit() {
formError = null;
saving = true;
try {
await createCall({
title: form.title.trim(),
quarter: form.quarter.trim(),
date: form.date,
thesis: form.thesis.trim(),
tickers: form.tickers.split(/[\s,]+/).map(t => t.trim().toUpperCase()).filter(Boolean),
});
showForm = false;
form = { title: '', quarter: currentQuarter(), date: today(), thesis: '', tickers: '' };
await invalidateAll(); // re-run load() to refresh the list
} catch (e) {
formError = e.message;
} finally {
saving = false;
}
}
async function remove(id) {
if (!confirm('Delete this market call?')) return;
await deleteCall(id);
await invalidateAll();
}
const signalColor = s => {
if (s?.includes('Strong')) return '#4ade80';
if (s?.includes('Momentum')) return '#60a5fa';
if (s?.includes('Neutral')) return '#94a3b8';
if (s?.includes('Speculation')) return '#fb923c';
return '#f87171';
};
const eventIcon = type => ({ earnings: '📊', exdividend: '💰', dividend: '💵' })[type] ?? '📅';
const eventColor = type => ({ earnings: '#60a5fa', exdividend: '#facc15', dividend: '#4ade80' })[type] ?? '#94a3b8';
const upcoming = $derived((data.events ?? []).filter(e => !e.isPast).slice(0, 20));
const past = $derived((data.events ?? []).filter(e => e.isPast).slice(0, 10));
const fmtMoney = n => n == null ? null :
n >= 1e9 ? '$' + (n / 1e9).toFixed(1) + 'B' :
n >= 1e6 ? '$' + (n / 1e6).toFixed(1) + 'M' : '$' + n.toFixed(2);
</script>
<div class="page">
<div class="page-header">
<div>
<h1>Market Calls</h1>
<p class="subtitle">Quarterly investment theses tracked from the day you made the call</p>
</div>
<button class="btn-primary" onclick={() => showForm = !showForm}>
{showForm ? 'Cancel' : ' New Call'}
</button>
</div>
<!-- ── New Call Form ──────────────────────────────────────────────── -->
{#if showForm}
<section class="section form-section">
<div class="section-header"><h2>New Market Call</h2></div>
<form class="call-form" onsubmit={e => { e.preventDefault(); submit(); }}>
<div class="form-row">
<label>
<span>Title</span>
<input bind:value={form.title} placeholder="Q3 2025 Rate pivot & tech rotation" required />
</label>
<label class="narrow">
<span>Quarter</span>
<input bind:value={form.quarter} placeholder="Q3 2025" required />
</label>
<label class="narrow">
<span>Date</span>
<input type="date" bind:value={form.date} required />
</label>
</div>
<label>
<span>Thesis</span>
<textarea
bind:value={form.thesis}
rows="4"
placeholder="Describe the macro thesis behind this call — why these tickers, why now..."
required
></textarea>
</label>
<label>
<span>Tickers to track</span>
<input
bind:value={form.tickers}
placeholder="AAPL, MSFT, TLT, GLD …"
required
/>
<span class="hint">Comma or space separated. Current prices will be snapshot automatically.</span>
</label>
{#if formError}
<div class="form-error">{formError}</div>
{/if}
<button type="submit" class="btn-primary" disabled={saving}>
{#if saving}
<Spinner size="sm" />
<span>Snapshotting prices…</span>
{:else}
Save Call
{/if}
</button>
</form>
</section>
{/if}
<!-- ── Calendar ──────────────────────────────────────────────────── -->
{#if (data.events ?? []).length > 0}
<section class="section">
<div class="section-header">
<h2>📅 Upcoming Events</h2>
<span class="count">{upcoming.length} upcoming</span>
{#if past.length > 0}
<span class="count" style="margin-left:4px">{past.length} recent</span>
{/if}
</div>
<div class="cal-grid">
{#each upcoming as ev}
<div class="cal-event">
<div class="cal-date">{ev.date}</div>
<div class="cal-content">
<span class="cal-ticker">{ev.ticker}</span>
<span class="cal-type" style="color:{eventColor(ev.type)}">
{eventIcon(ev.type)} {ev.label}
{#if ev.detail}<span class="cal-detail">· {ev.detail}</span>{/if}
</span>
{#if ev.epsEstimate != null}
<span class="cal-est">EPS est. ${ev.epsEstimate?.toFixed(2)} · Rev est. {fmtMoney(ev.revEstimate)}</span>
{/if}
</div>
</div>
{/each}
{#if past.length > 0}
<div class="cal-divider">— Past —</div>
{#each past as ev}
<div class="cal-event past">
<div class="cal-date">{ev.date}</div>
<div class="cal-content">
<span class="cal-ticker">{ev.ticker}</span>
<span class="cal-type past-type">
{eventIcon(ev.type)} {ev.label}
</span>
</div>
</div>
{/each}
{/if}
</div>
</section>
{/if}
<!-- ── Calls List ────────────────────────────────────────────────── -->
{#if data.error}
<div class="error-banner">{data.error}</div>
{:else if data.calls.length === 0}
<div class="empty">No market calls yet. Create your first one to start tracking.</div>
{:else}
{#each data.calls as call}
<section class="section call-card">
<div class="section-header">
<div class="call-meta">
<a href="/calls/{call.id}" class="call-title">{call.title}</a>
<div class="call-badges">
<span class="tag">{call.quarter}</span>
<span class="date-badge">{call.date}</span>
<span class="count">{call.tickers.length} tickers</span>
</div>
</div>
<button class="btn-delete" onclick={() => remove(call.id)}>✕</button>
</div>
<div class="call-body">
<p class="thesis">{call.thesis}</p>
{#if Object.keys(call.snapshot ?? {}).length}
<div class="snapshot-grid">
{#each call.tickers as ticker}
{@const snap = call.snapshot[ticker]}
{#if snap}
<a href="/calls/{call.id}" class="snap-card">
<div class="snap-ticker">{ticker}</div>
<div class="snap-price">${snap.price?.toFixed(2) ?? '—'}</div>
<div class="snap-signal" style="color:{signalColor(snap.signal)}">
{snap.signal?.replace(/[✅⚡⚠️🔄❌]/u, '').trim() ?? '—'}
</div>
</a>
{/if}
{/each}
</div>
<a href="/calls/{call.id}" class="view-link">View performance → </a>
{/if}
</div>
</section>
{/each}
{/if}
</div>
<style>
.page { max-width: 1100px; padding-bottom: 60px; }
.page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 24px;
}
h1 { font-size: 20px; font-weight: 700; color: #f1f5f9; margin-bottom: 4px; }
.subtitle { font-size: 12px; color: #475569; }
/* ── Buttons ─────────────────────────────────────────────────────── */
button {
padding: 9px 18px;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: none;
transition: background 0.15s;
}
.btn-primary { background: #2563eb; color: #fff; display: inline-flex; align-items: center; gap: 8px; }
.btn-primary:hover:not(:disabled) { background: #1d4ed8; }
.btn-primary:disabled { opacity: 0.5; cursor: default; }
.btn-delete { background: transparent; color: #475569; padding: 4px 8px; font-size: 14px; }
.btn-delete:hover { color: #f87171; }
/* ── Section ─────────────────────────────────────────────────────── */
.section {
background: #0d1117;
border: 1px solid #1e293b;
border-radius: 10px;
margin-bottom: 16px;
overflow: hidden;
}
.section-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 18px;
border-bottom: 1px solid #1e293b;
background: #111827;
}
h2 { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; margin: 0; }
/* ── Form ────────────────────────────────────────────────────────── */
.call-form { padding: 20px; display: flex; flex-direction: column; gap: 16px; }
.form-row { display: grid; grid-template-columns: 1fr auto auto; gap: 12px; align-items: start; }
.form-row .narrow { min-width: 120px; }
label { display: flex; flex-direction: column; gap: 5px; }
label > span { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b; }
input, textarea {
background: #1e293b;
border: 1px solid #2d3f55;
border-radius: 8px;
color: #e2e8f0;
padding: 9px 12px;
font-size: 13px;
outline: none;
transition: border-color 0.15s;
font-family: inherit;
}
input:focus, textarea:focus { border-color: #3b82f6; }
textarea { resize: vertical; }
.hint { font-size: 11px; color: #475569; }
.form-error { color: #f87171; font-size: 12px; background: #450a0a33; padding: 8px 12px; border-radius: 6px; }
/* ── Call card ───────────────────────────────────────────────────── */
.call-meta { display: flex; flex-direction: column; gap: 6px; flex: 1; min-width: 0; }
.call-title {
font-size: 14px;
font-weight: 700;
color: #f1f5f9;
text-decoration: none;
}
.call-title:hover { color: #60a5fa; }
.call-badges { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.tag {
display: inline-block;
background: #1e293b;
color: #64748b;
padding: 2px 8px;
border-radius: 4px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.date-badge { font-size: 11px; color: #475569; }
.count { font-size: 10px; color: #334155; background: #1e293b; padding: 2px 7px; border-radius: 20px; }
.call-body { padding: 18px; display: flex; flex-direction: column; gap: 16px; }
.thesis {
font-size: 13px;
color: #94a3b8;
line-height: 1.6;
border-left: 3px solid #1e3a5f;
padding-left: 14px;
margin: 0;
}
/* ── Snapshot grid ───────────────────────────────────────────────── */
.snapshot-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 8px;
}
.snap-card {
background: #111827;
border: 1px solid #1e293b;
border-radius: 8px;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 3px;
text-decoration: none;
transition: border-color 0.15s;
}
.snap-card:hover { border-color: #334155; }
.snap-ticker { font-size: 12px; font-weight: 700; color: #f1f5f9; }
.snap-price { font-size: 11px; color: #64748b; font-variant-numeric: tabular-nums; }
.snap-signal { font-size: 10px; font-weight: 600; }
.view-link { font-size: 12px; color: #60a5fa; text-decoration: none; }
.view-link:hover { text-decoration: underline; }
.empty { color: #475569; font-size: 13px; padding: 40px 0; text-align: center; }
.error-banner { background: #450a0a55; border: 1px solid #7f1d1d; border-radius: 8px; color: #f87171; padding: 10px 14px; margin-bottom: 16px; font-size: 13px; }
/* ── Calendar ───────────────────────────────────────────────────── */
.cal-grid {
padding: 8px 18px 14px;
display: flex;
flex-direction: column;
gap: 2px;
}
.cal-event {
display: grid;
grid-template-columns: 96px 1fr;
gap: 14px;
align-items: start;
padding: 8px 6px;
border-radius: 6px;
transition: background 0.1s;
}
.cal-event:hover { background: #111827; }
.cal-event.past { opacity: 0.45; }
.cal-date {
font-size: 11px;
font-variant-numeric: tabular-nums;
color: #475569;
padding-top: 1px;
white-space: nowrap;
}
.cal-content { display: flex; flex-direction: column; gap: 2px; }
.cal-ticker { font-size: 12px; font-weight: 700; color: #f1f5f9; }
.cal-type { font-size: 11px; font-weight: 600; }
.cal-detail { font-weight: 400; color: #64748b; }
.past-type { color: #475569 !important; }
.cal-est { font-size: 10px; color: #475569; }
.cal-divider {
font-size: 10px;
color: #334155;
text-align: center;
padding: 8px 0 4px;
letter-spacing: 0.06em;
}
</style>
+5
View File
@@ -0,0 +1,5 @@
export async function load({ fetch, params }) {
const res = await fetch(`/api/calls/${params.id}`);
if (!res.ok) return { error: await res.text() };
return res.json();
}
+202
View File
@@ -0,0 +1,202 @@
<script>
let { data } = $props();
const fmt = v => v != null ? '$' + v.toFixed(2) : '—';
const pctChange = (then, now) => {
if (then == null || now == null || then === 0) return null;
return ((now - then) / then) * 100;
};
const pctClass = v => v == null ? '' : v >= 0 ? 'pos' : 'neg';
const fmtPct = v => v == null ? '—' : (v >= 0 ? '+' : '') + v.toFixed(1) + '%';
const verdictColor = label => {
if (!label) return '#64748b';
if (label.startsWith('🟢')) return '#4ade80';
if (label.startsWith('🟡')) return '#facc15';
return '#f87171';
};
const daysSince = dateStr => {
const diff = Date.now() - new Date(dateStr).getTime();
return Math.floor(diff / 86400000);
};
const tickers = $derived(data?.tickers ?? []);
const snapshot = $derived(data?.snapshot ?? {});
const current = $derived(data?.current ?? {});
</script>
<div class="page">
{#if data?.error}
<div class="error-banner">{data.error}</div>
{:else if data}
<div class="breadcrumb"><a href="/calls">← Market Calls</a></div>
<div class="call-hero">
<div class="hero-meta">
<span class="tag">{data.quarter}</span>
<span class="date">{data.date}</span>
<span class="days">({daysSince(data.date)} days ago)</span>
</div>
<h1>{data.title}</h1>
<p class="thesis">{data.thesis}</p>
</div>
<!-- ── Performance Table ─────────────────────────────────────────── -->
<section class="section">
<div class="section-header">
<h2>Performance since call date</h2>
<span class="count">{tickers.length} tickers</span>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Ticker</th>
<th>Call Price</th>
<th>Now</th>
<th>Return</th>
<th>Call Signal</th>
<th>Now Signal</th>
<th>Call Verdict</th>
<th>Now Verdict</th>
</tr>
</thead>
<tbody>
{#each tickers as ticker}
{@const snap = snapshot[ticker]}
{@const cur = current[ticker]}
{@const ret = pctChange(snap?.price, cur?.price)}
<tr class:best={ret != null && ret >= 10} class:worst={ret != null && ret <= -10}>
<td class="ticker">{ticker}</td>
<td class="num">{fmt(snap?.price)}</td>
<td class="num">{fmt(cur?.price)}</td>
<td class="num {pctClass(ret)}">{fmtPct(ret)}</td>
<td>
{#if snap?.signal}
<span class="signal-text">{snap.signal}</span>
{:else}
<span class="muted"></span>
{/if}
</td>
<td>
{#if cur?.signal}
<span class="signal-text">{cur.signal}</span>
{:else}
<span class="muted"></span>
{/if}
</td>
<td>
{#if snap?.inflatedVerdict}
<span class="verdict-pill" style="color:{verdictColor(snap.inflatedVerdict)}">
{snap.inflatedVerdict.replace(/[🟢🟡🔴]/u, '').trim()}
</span>
{:else}
<span class="muted"></span>
{/if}
</td>
<td>
{#if cur?.inflatedVerdict}
<span class="verdict-pill" style="color:{verdictColor(cur.inflatedVerdict)}">
{cur.inflatedVerdict.replace(/[🟢🟡🔴]/u, '').trim()}
</span>
{:else}
<span class="muted"></span>
{/if}
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</section>
{/if}
</div>
<style>
.page { max-width: 1100px; padding-bottom: 60px; }
.breadcrumb { margin-bottom: 20px; }
.breadcrumb a { font-size: 12px; color: #475569; text-decoration: none; }
.breadcrumb a:hover { color: #94a3b8; }
.call-hero { margin-bottom: 24px; }
.hero-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.tag { background: #1e293b; color: #64748b; padding: 2px 8px; border-radius: 4px; font-size: 10px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; }
.date { font-size: 12px; color: #475569; }
.days { font-size: 12px; color: #334155; }
h1 { font-size: 20px; font-weight: 700; color: #f1f5f9; margin-bottom: 10px; }
.thesis {
font-size: 13px;
color: #94a3b8;
line-height: 1.7;
border-left: 3px solid #1e3a5f;
padding-left: 14px;
max-width: 800px;
}
/* ── Section ─────────────────────────────────────────────────────── */
.section { background: #0d1117; border: 1px solid #1e293b; border-radius: 10px; overflow: hidden; }
.section-header {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 18px;
border-bottom: 1px solid #1e293b;
background: #111827;
}
h2 { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; margin: 0; }
.count { font-size: 10px; color: #334155; background: #1e293b; padding: 2px 7px; border-radius: 20px; }
/* ── Table ───────────────────────────────────────────────────────── */
.table-wrap { overflow-x: auto; }
table { width: max-content; min-width: 100%; border-collapse: collapse; }
thead th {
text-align: left;
padding: 8px 14px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: #334155;
border-bottom: 1px solid #1e293b;
white-space: nowrap;
background: #111827;
}
tbody tr { border-bottom: 1px solid #161f2e; }
tbody tr:hover { background: #131c2b; }
tbody tr.best td { background: #14532d11; }
tbody tr.worst td { background: #450a0a11; }
tbody td { padding: 10px 14px; vertical-align: middle; white-space: nowrap; font-size: 13px; }
.ticker { font-weight: 700; color: #f1f5f9; }
.num { font-variant-numeric: tabular-nums; color: #64748b; }
.pos { color: #4ade80; font-weight: 600; }
.neg { color: #f87171; font-weight: 600; }
.muted { color: #334155; }
.signal-text { font-size: 12px; color: #94a3b8; }
.verdict-pill {
display: inline-block;
padding: 3px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
background: #1e293b;
}
.error-banner { background: #450a0a55; border: 1px solid #7f1d1d; border-radius: 8px; color: #f87171; padding: 10px 14px; font-size: 13px; }
</style>