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