diff options
| author | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-17 12:46:13 -0700 |
|---|---|---|
| committer | Tyler Hoang <tyler@tylerhoang.xyz> | 2026-05-17 12:46:13 -0700 |
| commit | 1482422f2f5b236cdcdff4429ae06bb55dca4083 (patch) | |
| tree | 4653cb4986a8a138f84dbec934effb0d011751d3 /frontend/lib | |
Add stack start and stop scripts
Diffstat (limited to 'frontend/lib')
| -rw-r--r-- | frontend/lib/api.ts | 52 | ||||
| -rw-r--r-- | frontend/lib/format.ts | 31 |
2 files changed, 83 insertions, 0 deletions
diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts new file mode 100644 index 0000000..b2f0dea --- /dev/null +++ b/frontend/lib/api.ts @@ -0,0 +1,52 @@ +import type { HistoryPoint, MarketIndex, SearchResult, TickerOverview, WatchlistResponse } from "@/types/api"; + +const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"; + +export class ApiError extends Error { + status: number; + + constructor(message: string, status: number) { + super(message); + this.name = "ApiError"; + this.status = status; + } +} + +async function request<T>(path: string, init?: RequestInit): Promise<T> { + const res = await fetch(`${API_BASE}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...(init?.headers || {}) + } + }); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new ApiError(body.detail || `Request failed: ${res.status}`, res.status); + } + return res.json() as Promise<T>; +} + +export const api = { + search(query: string) { + return request<SearchResult[]>(`/api/search?q=${encodeURIComponent(query)}`); + }, + marketIndices() { + return request<MarketIndex[]>("/api/market/indices"); + }, + overview(symbol: string) { + return request<TickerOverview>(`/api/tickers/${encodeURIComponent(symbol)}/overview`); + }, + history(symbol: string, period: string) { + return request<HistoryPoint[]>(`/api/tickers/${encodeURIComponent(symbol)}/history?period=${encodeURIComponent(period)}`); + }, + watchlist() { + return request<WatchlistResponse>("/api/watchlist"); + }, + addWatchlist(symbol: string) { + return request<WatchlistResponse>(`/api/watchlist/${encodeURIComponent(symbol)}`, { method: "POST" }); + }, + removeWatchlist(symbol: string) { + return request<WatchlistResponse>(`/api/watchlist/${encodeURIComponent(symbol)}`, { method: "DELETE" }); + } +}; diff --git a/frontend/lib/format.ts b/frontend/lib/format.ts new file mode 100644 index 0000000..34bffd8 --- /dev/null +++ b/frontend/lib/format.ts @@ -0,0 +1,31 @@ +export function fmtCurrency(value?: number | null, decimals = 2): string { + if (value === null || value === undefined || Number.isNaN(value)) return "-"; + return `$${value.toLocaleString(undefined, { maximumFractionDigits: decimals, minimumFractionDigits: decimals })}`; +} + +export function fmtNumber(value?: number | null, decimals = 2): string { + if (value === null || value === undefined || Number.isNaN(value)) return "-"; + return value.toLocaleString(undefined, { maximumFractionDigits: decimals, minimumFractionDigits: decimals }); +} + +export function fmtLarge(value?: number | null): string { + if (value === null || value === undefined || Number.isNaN(value)) return "-"; + const abs = Math.abs(value); + const sign = value < 0 ? "-" : ""; + if (abs >= 1e12) return `${sign}$${(abs / 1e12).toFixed(2)}T`; + if (abs >= 1e9) return `${sign}$${(abs / 1e9).toFixed(2)}B`; + if (abs >= 1e6) return `${sign}$${(abs / 1e6).toFixed(1)}M`; + return `${sign}$${abs.toLocaleString(undefined, { maximumFractionDigits: 0 })}`; +} + +export function fmtPct(value?: number | null, decimals = 2, signed = false): string { + if (value === null || value === undefined || Number.isNaN(value)) return "-"; + const pct = value * 100; + const sign = signed && pct > 0 ? "+" : ""; + return `${sign}${pct.toFixed(decimals)}%`; +} + +export function deltaClass(value?: number | null): string { + if (value === null || value === undefined) return "neutral"; + return value >= 0 ? "positive" : "negative"; +} |
