mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-08-02 10:00:17 +05:30
feat(writing-studio): add goal tracker with streaks and history
Amit Haridas
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
const HISTORY_KEY = 'plugins.writing-studio.history';
|
||||
|
||||
class GoalTracker {
|
||||
/**
|
||||
* @param {object} store - { get(key), set(key, value) } settings backend
|
||||
*/
|
||||
constructor(store) {
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
_getHistory() {
|
||||
const raw = this.store.get(HISTORY_KEY);
|
||||
return raw ? JSON.parse(raw) : {};
|
||||
}
|
||||
|
||||
_setHistory(history) {
|
||||
this.store.set(HISTORY_KEY, JSON.stringify(history));
|
||||
}
|
||||
|
||||
_setHistoryDay(dateStr, data) {
|
||||
const history = this._getHistory();
|
||||
history[dateStr] = data;
|
||||
this._setHistory(history);
|
||||
}
|
||||
|
||||
addWords(count) {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const history = this._getHistory();
|
||||
if (!history[today]) {
|
||||
history[today] = { words: 0, sessions: 0 };
|
||||
}
|
||||
history[today].words += count;
|
||||
history[today].sessions += 1;
|
||||
this._setHistory(history);
|
||||
}
|
||||
|
||||
getDailyProgress(goal) {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const history = this._getHistory();
|
||||
const written = history[today]?.words || 0;
|
||||
return { written, goal, pct: goal > 0 ? Math.min(100, Math.round((written / goal) * 100)) : 0 };
|
||||
}
|
||||
|
||||
getStreak(goal) {
|
||||
const history = this._getHistory();
|
||||
let streak = 0;
|
||||
const d = new Date();
|
||||
for (let i = 0; i < 365; i++) {
|
||||
const key = d.toISOString().split('T')[0];
|
||||
const day = history[key];
|
||||
if (day && day.words >= goal) {
|
||||
streak++;
|
||||
d.setDate(d.getDate() - 1);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return streak;
|
||||
}
|
||||
|
||||
getLast30Days() {
|
||||
const history = this._getHistory();
|
||||
const days = [];
|
||||
const d = new Date();
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const key = d.toISOString().split('T')[0];
|
||||
const day = history[key];
|
||||
days.push({ date: key, words: day?.words || 0 });
|
||||
d.setDate(d.getDate() - 1);
|
||||
}
|
||||
return days.reverse();
|
||||
}
|
||||
|
||||
getWeeklyTotal() {
|
||||
const history = this._getHistory();
|
||||
let total = 0;
|
||||
const d = new Date();
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const key = d.toISOString().split('T')[0];
|
||||
if (history[key]) total += history[key].words || 0;
|
||||
d.setDate(d.getDate() - 1);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { GoalTracker };
|
||||
@@ -0,0 +1,82 @@
|
||||
const { GoalTracker } = require('../src/plugins/built-in/writing-studio/goal-tracker');
|
||||
|
||||
describe('GoalTracker', () => {
|
||||
let tracker;
|
||||
let store;
|
||||
|
||||
beforeEach(() => {
|
||||
store = {};
|
||||
tracker = new GoalTracker({
|
||||
get: (key) => store[key],
|
||||
set: (key, value) => { store[key] = value; }
|
||||
});
|
||||
});
|
||||
|
||||
test('addWords records words for today', () => {
|
||||
tracker.addWords(500);
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
expect(store['plugins.writing-studio.history']).toBeDefined();
|
||||
const history = JSON.parse(store['plugins.writing-studio.history']);
|
||||
expect(history[today].words).toBe(500);
|
||||
});
|
||||
|
||||
test('addWords accumulates across multiple calls', () => {
|
||||
tracker.addWords(300);
|
||||
tracker.addWords(200);
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const history = JSON.parse(store['plugins.writing-studio.history']);
|
||||
expect(history[today].words).toBe(500);
|
||||
});
|
||||
|
||||
test('getDailyProgress returns 0 when no history', () => {
|
||||
expect(tracker.getDailyProgress(1000)).toEqual({ written: 0, goal: 1000, pct: 0 });
|
||||
});
|
||||
|
||||
test('getDailyProgress returns percentage', () => {
|
||||
tracker.addWords(500);
|
||||
const progress = tracker.getDailyProgress(1000);
|
||||
expect(progress.written).toBe(500);
|
||||
expect(progress.pct).toBe(50);
|
||||
});
|
||||
|
||||
test('getStreak counts consecutive days meeting goal', () => {
|
||||
const today = new Date();
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
const key = d.toISOString().split('T')[0];
|
||||
tracker._setHistoryDay(key, { words: 1200 });
|
||||
}
|
||||
const streak = tracker.getStreak(1000);
|
||||
expect(streak).toBe(3);
|
||||
});
|
||||
|
||||
test('getStreak breaks on missed day', () => {
|
||||
const today = new Date();
|
||||
tracker._setHistoryDay(today.toISOString().split('T')[0], { words: 1200 });
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
tracker._setHistoryDay(yesterday.toISOString().split('T')[0], { words: 500 });
|
||||
const dayBefore = new Date(today);
|
||||
dayBefore.setDate(dayBefore.getDate() - 2);
|
||||
tracker._setHistoryDay(dayBefore.toISOString().split('T')[0], { words: 1200 });
|
||||
expect(tracker.getStreak(1000)).toBe(1);
|
||||
});
|
||||
|
||||
test('getLast30Days returns array of 30 entries', () => {
|
||||
tracker.addWords(100);
|
||||
const days = tracker.getLast30Days();
|
||||
expect(days.length).toBe(30);
|
||||
expect(days[29].words).toBe(100);
|
||||
});
|
||||
|
||||
test('getWeeklyTotal sums last 7 days', () => {
|
||||
const today = new Date();
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(today);
|
||||
d.setDate(d.getDate() - i);
|
||||
tracker._setHistoryDay(d.toISOString().split('T')[0], { words: 200 });
|
||||
}
|
||||
expect(tracker.getWeeklyTotal()).toBe(1400);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user