/** * Browser-side BFF client for the sādhana streak (/sadhna-streak). * * Mirrors lib/social.ts: same `/api/v1` base, same `credentials: "include"` * so the auth cookie rides along, same envelope unwrapping. Kept in its * own module so the streak feature doesn't bloat the social client. */ const BFF_BASE = process.env.NEXT_PUBLIC_BFF_URL || "http://localhost:8080"; export class SadhnaError extends Error { constructor( public status: number, message: string, ) { super(message); this.name = "SadhnaError"; } } type Envelope = { success: boolean; data?: T; error?: { code: number; message: string }; }; async function call(path: string, init?: RequestInit): Promise { const res = await fetch(`${BFF_BASE}/api/v1${path}`, { credentials: "include", headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) }, ...init, }); if (res.status === 204) return undefined as T; const body = (await res.json().catch(() => ({}))) as Envelope; if (!res.ok || body.success === false) { throw new SadhnaError(res.status, body.error?.message || res.statusText); } return (body.data ?? (undefined as unknown as T)) as T; } // ─── Types ─────────────────────────────────────────────────────────────── /** Plant growth stage, mirrors the BFF's stageFor(). */ export type SadhnaStage = | "dead" | "sprout" | "seedling" | "sapling" | "young" | "mature"; /** One cell of the 7-day streak strip. */ export type SadhnaDay = { date: string; // YYYY-MM-DD (UTC) chant_done: boolean; quote_done: boolean; completed: boolean; }; /** * One fold of the sādhana creeper — a single day the vine has wound around * its pole. The vine grows a fresh fold for each day of the current streak * (oldest first); the final fold may be today, still in progress (the * growing tip). `rounds`/`posts` annotate what was offered that day. */ export type VineDay = { date: string; // YYYY-MM-DD (UTC) rounds: number; posts: number; completed: boolean; in_progress: boolean; }; export type SadhnaDashboard = { // Today's two disciplines. target_rounds: number; rounds_completed: number; chant_done: boolean; quote_done: boolean; today_completed: boolean; // Streak counters. current_streak: number; longest_streak: number; // Plant state (growth 0–100). growth: number; stage: SadhnaStage; alive: boolean; // Trailing 7 days, oldest first. week: SadhnaDay[]; // The creeper: one fold per day of the current streak (oldest first), // plus an in-progress fold for an unfinished today. Capped server-side; // current_streak holds the true length. vine_days: VineDay[]; }; // ─── API ─────────────────────────────────────────────────────────────── export const sadhna = { /** Today's progress + streak + plant state. */ dashboard: () => call("/sadhna/dashboard"), /** Save today's chanting round count; returns the recomputed dashboard. */ saveChant: (roundsCompleted: number) => call("/sadhna/chant", { method: "POST", body: JSON.stringify({ rounds_completed: roundsCompleted }), }), };