wip: sadhna streak
This commit is contained in:
+108
@@ -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 }),
|
||||
}),
|
||||
};
|
||||
Reference in New Issue
Block a user