mirror of
https://github.com/nagaoo0/HabbitGrid.git
synced 2026-01-11 23:44:55 +00:00
CountersUpdate
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import React, { useMemo, useEffect } from 'react';
|
import React, { useMemo, useEffect } from 'react';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { getColorIntensity, isToday, formatDate, getWeekdayLabel, getFrozenDays } from '../lib/utils-habit';
|
import { getColorIntensity, isToday, formatDate, getWeekdayLabel, getFrozenDays, calculateStreaks } from '../lib/utils-habit';
|
||||||
import { toggleCompletion, getAuthUser } from '../lib/datastore';
|
import { toggleCompletion, getAuthUser } from '../lib/datastore';
|
||||||
|
|
||||||
const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
|
const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
|
||||||
@@ -50,6 +50,11 @@ const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
|
|||||||
const cidx = completions.indexOf(dateStr);
|
const cidx = completions.indexOf(dateStr);
|
||||||
if (cidx > -1) completions.splice(cidx, 1); else completions.push(dateStr);
|
if (cidx > -1) completions.splice(cidx, 1); else completions.push(dateStr);
|
||||||
habits[idx].completions = completions;
|
habits[idx].completions = completions;
|
||||||
|
// Recalculate streaks so counters reflect immediately
|
||||||
|
const { currentStreak, longestStreak } = calculateStreaks(completions);
|
||||||
|
const prevRecord = habits[idx].longestStreak || 0;
|
||||||
|
habits[idx].currentStreak = currentStreak;
|
||||||
|
habits[idx].longestStreak = Math.max(longestStreak, prevRecord);
|
||||||
localStorage.setItem('habitgrid_data', JSON.stringify(habits));
|
localStorage.setItem('habitgrid_data', JSON.stringify(habits));
|
||||||
}
|
}
|
||||||
onUpdate();
|
onUpdate();
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ function getFreezeIcon() {
|
|||||||
return icon || '❄️';
|
return icon || '❄️';
|
||||||
}
|
}
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
import { getColorIntensity, isToday, formatDate } from '../lib/utils-habit';
|
import { getColorIntensity, isToday, formatDate, calculateStreaks } from '../lib/utils-habit';
|
||||||
import { getFrozenDays } from '../lib/utils-habit';
|
import { getFrozenDays } from '../lib/utils-habit';
|
||||||
import { toggleCompletion, getAuthUser } from '../lib/datastore';
|
import { toggleCompletion, getAuthUser } from '../lib/datastore';
|
||||||
import { toast } from './ui/use-toast';
|
import { toast } from './ui/use-toast';
|
||||||
@@ -79,6 +79,11 @@ const MiniGrid = ({ habit, onUpdate }) => {
|
|||||||
const cidx = completions.indexOf(dateStr);
|
const cidx = completions.indexOf(dateStr);
|
||||||
if (cidx > -1) completions.splice(cidx, 1); else completions.push(dateStr);
|
if (cidx > -1) completions.splice(cidx, 1); else completions.push(dateStr);
|
||||||
habits[idx].completions = completions;
|
habits[idx].completions = completions;
|
||||||
|
// Recalculate streaks locally so counters update immediately
|
||||||
|
const { currentStreak, longestStreak } = calculateStreaks(completions);
|
||||||
|
const prevRecord = habits[idx].longestStreak || 0;
|
||||||
|
habits[idx].currentStreak = currentStreak;
|
||||||
|
habits[idx].longestStreak = Math.max(longestStreak, prevRecord);
|
||||||
localStorage.setItem('habitgrid_data', JSON.stringify(habits));
|
localStorage.setItem('habitgrid_data', JSON.stringify(habits));
|
||||||
}
|
}
|
||||||
onUpdate();
|
onUpdate();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ function ensureUUIDs(habits) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
import { supabase, isSupabaseConfigured } from './supabase';
|
import { supabase, isSupabaseConfigured } from './supabase';
|
||||||
|
import { calculateStreaks } from './utils-habit';
|
||||||
import * as local from './storage';
|
import * as local from './storage';
|
||||||
|
|
||||||
const SYNC_FLAG = 'habitgrid_remote_synced_at';
|
const SYNC_FLAG = 'habitgrid_remote_synced_at';
|
||||||
@@ -172,7 +173,10 @@ export async function toggleCompletion(habitId, dateStr) {
|
|||||||
const completions = Array.isArray(target.completions) ? [...target.completions] : [];
|
const completions = Array.isArray(target.completions) ? [...target.completions] : [];
|
||||||
const idx = completions.indexOf(dateStr);
|
const idx = completions.indexOf(dateStr);
|
||||||
if (idx > -1) completions.splice(idx, 1); else completions.push(dateStr);
|
if (idx > -1) completions.splice(idx, 1); else completions.push(dateStr);
|
||||||
return updateHabit(habitId, { completions });
|
// Calculate streaks and preserve personal record (do not decrease longest)
|
||||||
|
const { currentStreak, longestStreak } = calculateStreaks(completions);
|
||||||
|
const nextLongest = Math.max(longestStreak, target.longestStreak || 0);
|
||||||
|
return updateHabit(habitId, { completions, currentStreak, longestStreak: nextLongest });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -126,65 +126,7 @@ export const toggleCompletion = (habitId, dateStr) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
import { getFrozenDays } from './utils-habit.js';
|
import { calculateStreaks } from './utils-habit.js';
|
||||||
const calculateStreaks = (completions) => {
|
|
||||||
if (completions.length === 0) {
|
|
||||||
return { currentStreak: 0, longestStreak: 0 };
|
|
||||||
}
|
|
||||||
// Only use frozen days for streak calculation
|
|
||||||
const frozenDays = getFrozenDays(completions);
|
|
||||||
const allValid = Array.from(new Set([...completions, ...frozenDays]));
|
|
||||||
const sortedDates = allValid
|
|
||||||
.map(d => new Date(d))
|
|
||||||
.sort((a, b) => b - a);
|
|
||||||
|
|
||||||
let currentStreak = 0;
|
|
||||||
let longestStreak = 0;
|
|
||||||
let tempStreak = 1;
|
|
||||||
|
|
||||||
const today = new Date();
|
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
const yesterday = new Date(today);
|
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
|
||||||
|
|
||||||
const mostRecent = sortedDates[0];
|
|
||||||
mostRecent.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
if (mostRecent.getTime() === today.getTime() || mostRecent.getTime() === yesterday.getTime()) {
|
|
||||||
currentStreak = 1;
|
|
||||||
for (let i = 1; i < sortedDates.length; i++) {
|
|
||||||
const current = new Date(sortedDates[i]);
|
|
||||||
current.setHours(0, 0, 0, 0);
|
|
||||||
const previous = new Date(sortedDates[i - 1]);
|
|
||||||
previous.setHours(0, 0, 0, 0);
|
|
||||||
const diffDays = Math.floor((previous - current) / (1000 * 60 * 60 * 24));
|
|
||||||
if (diffDays === 1) {
|
|
||||||
currentStreak++;
|
|
||||||
tempStreak++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tempStreak = 1;
|
|
||||||
for (let i = 1; i < sortedDates.length; i++) {
|
|
||||||
const current = new Date(sortedDates[i]);
|
|
||||||
current.setHours(0, 0, 0, 0);
|
|
||||||
const previous = new Date(sortedDates[i - 1]);
|
|
||||||
previous.setHours(0, 0, 0, 0);
|
|
||||||
const diffDays = Math.floor((previous - current) / (1000 * 60 * 60 * 24));
|
|
||||||
if (diffDays === 1) {
|
|
||||||
tempStreak++;
|
|
||||||
longestStreak = Math.max(longestStreak, tempStreak);
|
|
||||||
} else {
|
|
||||||
tempStreak = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
longestStreak = Math.max(longestStreak, currentStreak, 1);
|
|
||||||
return { currentStreak, longestStreak };
|
|
||||||
}
|
|
||||||
|
|
||||||
export const exportData = () => {
|
export const exportData = () => {
|
||||||
const habits = getHabits();
|
const habits = getHabits();
|
||||||
|
|||||||
@@ -40,6 +40,71 @@ export const getWeekdayLabel = (dayIndex) => {
|
|||||||
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
return labels[dayIndex];
|
return labels[dayIndex];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate current and longest streaks from a list of completion date strings (YYYY-MM-DD)
|
||||||
|
// Rules:
|
||||||
|
// - Streaks count consecutive days
|
||||||
|
// - Today or yesterday must be present to have a non-zero current streak
|
||||||
|
// - We also include "frozen" days (one missed day per month sandwiched by completions)
|
||||||
|
// - Longest streak is at least 1 if there is at least one completion
|
||||||
|
export function calculateStreaks(completions) {
|
||||||
|
if (!Array.isArray(completions) || completions.length === 0) {
|
||||||
|
return { currentStreak: 0, longestStreak: 0 };
|
||||||
|
}
|
||||||
|
// Only use frozen days for streak calculation
|
||||||
|
const frozenDays = getFrozenDays(completions);
|
||||||
|
const allValid = Array.from(new Set([...(completions || []), ...frozenDays]));
|
||||||
|
const sortedDates = allValid
|
||||||
|
.map(d => new Date(d))
|
||||||
|
.sort((a, b) => b - a);
|
||||||
|
|
||||||
|
let currentStreak = 0;
|
||||||
|
let longestStreak = 0;
|
||||||
|
let tempStreak = 1;
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
const yesterday = new Date(today);
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
|
||||||
|
const mostRecent = sortedDates[0];
|
||||||
|
mostRecent.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
if (mostRecent.getTime() === today.getTime() || mostRecent.getTime() === yesterday.getTime()) {
|
||||||
|
currentStreak = 1;
|
||||||
|
for (let i = 1; i < sortedDates.length; i++) {
|
||||||
|
const current = new Date(sortedDates[i]);
|
||||||
|
current.setHours(0, 0, 0, 0);
|
||||||
|
const previous = new Date(sortedDates[i - 1]);
|
||||||
|
previous.setHours(0, 0, 0, 0);
|
||||||
|
const diffDays = Math.floor((previous - current) / (1000 * 60 * 60 * 24));
|
||||||
|
if (diffDays === 1) {
|
||||||
|
currentStreak++;
|
||||||
|
tempStreak++;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tempStreak = 1;
|
||||||
|
for (let i = 1; i < sortedDates.length; i++) {
|
||||||
|
const current = new Date(sortedDates[i]);
|
||||||
|
current.setHours(0, 0, 0, 0);
|
||||||
|
const previous = new Date(sortedDates[i - 1]);
|
||||||
|
previous.setHours(0, 0, 0, 0);
|
||||||
|
const diffDays = Math.floor((previous - current) / (1000 * 60 * 60 * 24));
|
||||||
|
if (diffDays === 1) {
|
||||||
|
tempStreak++;
|
||||||
|
longestStreak = Math.max(longestStreak, tempStreak);
|
||||||
|
} else {
|
||||||
|
tempStreak = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
longestStreak = Math.max(longestStreak, currentStreak, 1);
|
||||||
|
return { currentStreak, longestStreak };
|
||||||
|
}
|
||||||
// Returns array of frozen days (date strings) for a given completions array
|
// Returns array of frozen days (date strings) for a given completions array
|
||||||
export function getFrozenDays(completions) {
|
export function getFrozenDays(completions) {
|
||||||
// Map: month string -> frozen day string
|
// Map: month string -> frozen day string
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import HabitCard from '../components/HabitCard';
|
|||||||
import AnimatedCounter from '../components/AnimatedCounter';
|
import AnimatedCounter from '../components/AnimatedCounter';
|
||||||
import GitActivityGrid from '../components/GitActivityGrid';
|
import GitActivityGrid from '../components/GitActivityGrid';
|
||||||
import { getGitEnabled } from '../lib/git';
|
import { getGitEnabled } from '../lib/git';
|
||||||
|
import { calculateStreaks } from '../lib/utils-habit';
|
||||||
import { getHabits, updateHabit, syncLocalToRemoteIfNeeded, syncRemoteToLocal, getAuthUser, hasEverLoggedIn } from '../lib/datastore';
|
import { getHabits, updateHabit, syncLocalToRemoteIfNeeded, syncRemoteToLocal, getAuthUser, hasEverLoggedIn } from '../lib/datastore';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -70,13 +71,27 @@ const HomePage = () => {
|
|||||||
const loadHabits = async () => {
|
const loadHabits = async () => {
|
||||||
// Always read from local for instant UI
|
// Always read from local for instant UI
|
||||||
const loadedHabits = JSON.parse(localStorage.getItem('habitgrid_data') || '[]');
|
const loadedHabits = JSON.parse(localStorage.getItem('habitgrid_data') || '[]');
|
||||||
loadedHabits.sort((a, b) => {
|
// One-time consistency pass: recompute streaks from completions if missing or outdated
|
||||||
|
let changed = false;
|
||||||
|
const updated = loadedHabits.map(h => {
|
||||||
|
const { currentStreak, longestStreak } = calculateStreaks(h.completions || []);
|
||||||
|
const nextLongest = Math.max(longestStreak, h.longestStreak || 0);
|
||||||
|
if ((h.currentStreak ?? 0) !== currentStreak || (h.longestStreak ?? 0) !== nextLongest) {
|
||||||
|
changed = true;
|
||||||
|
return { ...h, currentStreak, longestStreak: nextLongest };
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
});
|
||||||
|
if (changed) {
|
||||||
|
localStorage.setItem('habitgrid_data', JSON.stringify(updated));
|
||||||
|
}
|
||||||
|
updated.sort((a, b) => {
|
||||||
if (a.sortOrder !== undefined && b.sortOrder !== undefined) return a.sortOrder - b.sortOrder;
|
if (a.sortOrder !== undefined && b.sortOrder !== undefined) return a.sortOrder - b.sortOrder;
|
||||||
if (a.sortOrder !== undefined) return -1;
|
if (a.sortOrder !== undefined) return -1;
|
||||||
if (b.sortOrder !== undefined) return 1;
|
if (b.sortOrder !== undefined) return 1;
|
||||||
return new Date(a.createdAt || 0) - new Date(b.createdAt || 0);
|
return new Date(a.createdAt || 0) - new Date(b.createdAt || 0);
|
||||||
});
|
});
|
||||||
setHabits(loadedHabits);
|
setHabits(updated);
|
||||||
// Initialize collapsed state for new categories
|
// Initialize collapsed state for new categories
|
||||||
const categories = Array.from(new Set(loadedHabits.map(h => h.category || 'Uncategorized')));
|
const categories = Array.from(new Set(loadedHabits.map(h => h.category || 'Uncategorized')));
|
||||||
setCollapsedGroups(prev => {
|
setCollapsedGroups(prev => {
|
||||||
|
|||||||
Reference in New Issue
Block a user