slice 1g: Next.js M1 overview panel + Primary-Rule lint (ADR-0007)
Symbol command bar (Zustand active-symbol) -> OverviewPanel (price + day-change shape/color/label per P7 G3 + Recharts sparkline + sector) + signup/login + ADR-0007 footer. next.config rewrites proxy /api -> backend:3001 (same-origin cookies). Vanilla fetch tRPC client (decoupled from server Node types). Primary-Rule lint 2/2 green. Slice 1 tracer bullet COMPLETE: 38 tests green, live-verified end-to-end.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// Thin typed client over the tRPC backend (DESIGN.md §2). Slice 1 uses a fetch wrapper
|
||||
// rather than @trpc/client to keep the Next app decoupled from the server's Node-native
|
||||
// type graph; the server is a real tRPC server and full end-to-end client types can be
|
||||
// layered later without changing this surface.
|
||||
export interface Quote {
|
||||
symbol: string;
|
||||
price: number;
|
||||
bid?: number | null;
|
||||
ask?: number | null;
|
||||
change?: number | null;
|
||||
changePercent?: number | null;
|
||||
iv?: number | null;
|
||||
}
|
||||
export interface PriceCandle {
|
||||
ts: string;
|
||||
o: number;
|
||||
h: number;
|
||||
l: number;
|
||||
c: number;
|
||||
v: number;
|
||||
adjClose?: number | null;
|
||||
}
|
||||
export interface SymbolMeta {
|
||||
symbol: string;
|
||||
name?: string | null;
|
||||
sector?: string | null;
|
||||
industry?: string | null;
|
||||
exchange?: string | null;
|
||||
tickerKind: string;
|
||||
peers?: string[] | null;
|
||||
}
|
||||
export interface Snapshot {
|
||||
symbol: string;
|
||||
quote: Quote | null;
|
||||
candles: PriceCandle[] | null;
|
||||
sector: SymbolMeta | null;
|
||||
stale: { quote: boolean; candles: boolean; sector: boolean };
|
||||
}
|
||||
export interface AuthUser {
|
||||
userId: string;
|
||||
email?: string;
|
||||
complexity?: string;
|
||||
riskTolerance?: string;
|
||||
convexityPosture?: string;
|
||||
}
|
||||
|
||||
async function parse<T>(res: Response): Promise<T> {
|
||||
const json = await res.json();
|
||||
if (json?.error) {
|
||||
const msg = json.error.message ?? `tRPC error ${json.error.data?.code ?? ""}`;
|
||||
throw new Error(typeof msg === "string" ? msg : "tRPC error");
|
||||
}
|
||||
return json.result.data as T;
|
||||
}
|
||||
|
||||
async function trpcQuery<T>(procedure: string, input?: unknown): Promise<T> {
|
||||
const search = input ? `?input=${encodeURIComponent(JSON.stringify(input))}` : "";
|
||||
const res = await fetch(`/api/trpc/${procedure}${search}`, {
|
||||
headers: { "content-type": "application/json" },
|
||||
credentials: "include",
|
||||
});
|
||||
return parse<T>(res);
|
||||
}
|
||||
|
||||
async function trpcMutate<T>(procedure: string, input: unknown): Promise<T> {
|
||||
const res = await fetch(`/api/trpc/${procedure}`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
credentials: "include",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
return parse<T>(res);
|
||||
}
|
||||
|
||||
export const api = {
|
||||
market: { snapshot: (symbol: string) => trpcQuery<Snapshot>("market.snapshot", { symbol }) },
|
||||
auth: {
|
||||
signup: (email: string, password: string) => trpcMutate<{ userId: string }>("auth.signup", { email, password }),
|
||||
login: (email: string, password: string) => trpcMutate<{ userId: string }>("auth.login", { email, password }),
|
||||
logout: () => trpcMutate<{ ok: boolean }>("auth.logout", {}),
|
||||
me: () => trpcQuery<AuthUser | null>("auth.me"),
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user