summaryrefslogtreecommitdiff
path: root/frontend/lib
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/lib')
-rw-r--r--frontend/lib/api.ts52
-rw-r--r--frontend/lib/format.ts31
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";
+}