wip: sadhna streak

This commit is contained in:
gosangam
2026-06-07 22:32:34 +05:30
parent d5fb350652
commit ecb32d2e85
6 changed files with 1560 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
/**
* 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 }),
}),
};