feat: day streaks and two-week activity history
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>
This commit is contained in:
56
src/lib/streak.ts
Normal file
56
src/lib/streak.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user