feat(gamification): completionStats store
Owns the last-7-days completion series. Refreshes on task-completed / step-completed / task-uncompleted / task-deleted window events and on window focus (for day rollover). Derives todayCount from the newest entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
56
src/lib/stores/completionStats.svelte.ts
Normal file
56
src/lib/stores/completionStats.svelte.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Phase 8 store for forgiving gamification. Owns the last-7-days series
|
||||||
|
// of completion counts rendered by the Tasks-page badge + sparkline.
|
||||||
|
//
|
||||||
|
// Refresh triggers:
|
||||||
|
// - module load (first time)
|
||||||
|
// - kon:task-completed (from page.svelte.ts completeTask)
|
||||||
|
// - kon:step-completed (from MicroSteps.svelte)
|
||||||
|
// - kon:task-uncompleted (new event, emitted by uncompleteTask in Task 10)
|
||||||
|
// - kon:task-deleted (new event, emitted by deleteTask in Task 10)
|
||||||
|
// - window focus (for day rollover while the app stayed open
|
||||||
|
// past midnight)
|
||||||
|
//
|
||||||
|
// We deliberately avoid polling. Every path that changes a row's
|
||||||
|
// done state or deletes a row emits one of the events above.
|
||||||
|
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { hasTauriRuntime } from "$lib/utils/runtime.js";
|
||||||
|
import type { DailyCompletionCount } from "$lib/types/app";
|
||||||
|
|
||||||
|
const DAYS = 7;
|
||||||
|
|
||||||
|
export const recentCompletions = $state<DailyCompletionCount[]>([]);
|
||||||
|
|
||||||
|
// Reactive derived value. Svelte 5 template consumers read it as a
|
||||||
|
// plain property (no parens) and it recomputes when recentCompletions
|
||||||
|
// changes.
|
||||||
|
export const todayCount = $derived(recentCompletions.at(-1)?.count ?? 0);
|
||||||
|
|
||||||
|
export async function refresh(): Promise<void> {
|
||||||
|
if (!hasTauriRuntime()) return;
|
||||||
|
try {
|
||||||
|
const series = await invoke<DailyCompletionCount[]>(
|
||||||
|
"list_recent_completions_cmd",
|
||||||
|
{ days: DAYS },
|
||||||
|
);
|
||||||
|
recentCompletions.splice(0, recentCompletions.length, ...series);
|
||||||
|
} catch (err) {
|
||||||
|
// Do not toast. This is a background read. Log only.
|
||||||
|
console.error("completionStats.refresh failed", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const handler = () => {
|
||||||
|
refresh().catch(() => {});
|
||||||
|
};
|
||||||
|
window.addEventListener("kon:task-completed", handler);
|
||||||
|
window.addEventListener("kon:step-completed", handler);
|
||||||
|
window.addEventListener("kon:task-uncompleted", handler);
|
||||||
|
window.addEventListener("kon:task-deleted", handler);
|
||||||
|
window.addEventListener("focus", handler);
|
||||||
|
|
||||||
|
if (hasTauriRuntime()) {
|
||||||
|
refresh().catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user