Session count alone says nothing about consistency, which is what makes pelvic floor training work. Record every completion timestamp and derive streaks from local calendar days: current streak (survives until a full day is missed), best streak, and a 14-day dot strip on the home screen. The done screen now shows the streak so finishing is rewarded. - src/lib/streak.ts: pure day-key / streak logic, covered by node --test - store: new `sessions` array, defaulted for pre-existing installs - home: StreakCard, screen now scrolls for smaller phones - tsconfig: TS 6 no longer auto-includes @types, so declare node types Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
57 lines
1.9 KiB
TypeScript
57 lines
1.9 KiB
TypeScript
// Pure streak logic over local calendar days — no React, no storage. Days are
|
|
// "YYYY-MM-DD" keys in the device's local time zone, so a session at 23:50
|
|
// counts for the day it was actually done, not the UTC one.
|
|
|
|
export type DayKey = string;
|
|
|
|
export function dayKey(date: Date): DayKey {
|
|
const y = date.getFullYear();
|
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
const d = String(date.getDate()).padStart(2, "0");
|
|
return `${y}-${m}-${d}`;
|
|
}
|
|
|
|
/** Local-midnight Date for a day key. */
|
|
export function fromKey(key: DayKey): Date {
|
|
const [y, m, d] = key.split("-").map(Number);
|
|
return new Date(y, m - 1, d);
|
|
}
|
|
|
|
function shiftKey(key: DayKey, days: number): DayKey {
|
|
const date = fromKey(key);
|
|
date.setDate(date.getDate() + days);
|
|
return dayKey(date);
|
|
}
|
|
|
|
/** Unique, ascending day keys from a list of ISO session timestamps. */
|
|
export function sessionDays(sessionsIso: string[]): DayKey[] {
|
|
return Array.from(new Set(sessionsIso.map((iso) => dayKey(new Date(iso))))).sort();
|
|
}
|
|
|
|
/** The last `n` day keys ending on `today`, oldest first. */
|
|
export function lastNDays(n: number, today: DayKey): DayKey[] {
|
|
const out: DayKey[] = [];
|
|
for (let i = n - 1; i >= 0; i--) out.push(shiftKey(today, -i));
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Current streak = consecutive trained days ending today or yesterday (a
|
|
* streak isn't broken until a full day is missed). Best = longest run ever.
|
|
*/
|
|
export function computeStreaks(days: DayKey[], today: DayKey): { current: number; best: number } {
|
|
const sorted = Array.from(new Set(days)).sort();
|
|
if (sorted.length === 0) return { current: 0, best: 0 };
|
|
|
|
let best = 1;
|
|
let run = 1;
|
|
for (let i = 1; i < sorted.length; i++) {
|
|
run = shiftKey(sorted[i - 1], 1) === sorted[i] ? run + 1 : 1;
|
|
if (run > best) best = run;
|
|
}
|
|
|
|
const last = sorted[sorted.length - 1];
|
|
const alive = last === today || last === shiftKey(today, -1);
|
|
return { current: alive ? run : 0, best };
|
|
}
|