Files
spsays-training/lib/sadhna.ts
T
2026-06-07 22:32:34 +05:30

109 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<T> = {
success: boolean;
data?: T;
error?: { code: number; message: string };
};
async function call<T>(path: string, init?: RequestInit): Promise<T> {
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<T>;
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<SadhnaDashboard>("/sadhna/dashboard"),
/** Save today's chanting round count; returns the recomputed dashboard. */
saveChant: (roundsCompleted: number) =>
call<SadhnaDashboard>("/sadhna/chant", {
method: "POST",
body: JSON.stringify({ rounds_completed: roundsCompleted }),
}),
};