mirror of
https://github.com/nagaoo0/HabbitGrid.git
synced 2026-01-11 15:34:54 +00:00
CountersUpdate
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, { useMemo, useEffect } from 'react';
|
||||
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';
|
||||
|
||||
const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
|
||||
@@ -50,6 +50,11 @@ const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
|
||||
const cidx = completions.indexOf(dateStr);
|
||||
if (cidx > -1) completions.splice(cidx, 1); else completions.push(dateStr);
|
||||
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));
|
||||
}
|
||||
onUpdate();
|
||||
|
||||
@@ -38,7 +38,7 @@ function getFreezeIcon() {
|
||||
return icon || '❄️';
|
||||
}
|
||||
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 { toggleCompletion, getAuthUser } from '../lib/datastore';
|
||||
import { toast } from './ui/use-toast';
|
||||
@@ -79,6 +79,11 @@ const MiniGrid = ({ habit, onUpdate }) => {
|
||||
const cidx = completions.indexOf(dateStr);
|
||||
if (cidx > -1) completions.splice(cidx, 1); else completions.push(dateStr);
|
||||
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));
|
||||
}
|
||||
onUpdate();
|
||||
|
||||
@@ -23,6 +23,7 @@ function ensureUUIDs(habits) {
|
||||
});
|
||||
}
|
||||
import { supabase, isSupabaseConfigured } from './supabase';
|
||||
import { calculateStreaks } from './utils-habit';
|
||||
import * as local from './storage';
|
||||
|
||||
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 idx = completions.indexOf(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';
|
||||
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 };
|
||||
}
|
||||
import { calculateStreaks } from './utils-habit.js';
|
||||
|
||||
export const exportData = () => {
|
||||
const habits = getHabits();
|
||||
|
||||
@@ -40,6 +40,71 @@ export const getWeekdayLabel = (dayIndex) => {
|
||||
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
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
|
||||
export function getFrozenDays(completions) {
|
||||
// Map: month string -> frozen day string
|
||||
|
||||
@@ -9,6 +9,7 @@ import HabitCard from '../components/HabitCard';
|
||||
import AnimatedCounter from '../components/AnimatedCounter';
|
||||
import GitActivityGrid from '../components/GitActivityGrid';
|
||||
import { getGitEnabled } from '../lib/git';
|
||||
import { calculateStreaks } from '../lib/utils-habit';
|
||||
import { getHabits, updateHabit, syncLocalToRemoteIfNeeded, syncRemoteToLocal, getAuthUser, hasEverLoggedIn } from '../lib/datastore';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -70,13 +71,27 @@ const HomePage = () => {
|
||||
const loadHabits = async () => {
|
||||
// Always read from local for instant UI
|
||||
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) return -1;
|
||||
if (b.sortOrder !== undefined) return 1;
|
||||
return new Date(a.createdAt || 0) - new Date(b.createdAt || 0);
|
||||
});
|
||||
setHabits(loadedHabits);
|
||||
setHabits(updated);
|
||||
// Initialize collapsed state for new categories
|
||||
const categories = Array.from(new Set(loadedHabits.map(h => h.category || 'Uncategorized')));
|
||||
setCollapsedGroups(prev => {
|
||||
|
||||
Reference in New Issue
Block a user