diff --git a/src/components/HabitGrid.jsx b/src/components/HabitGrid.jsx
index 2e65d15..64ebc63 100644
--- a/src/components/HabitGrid.jsx
+++ b/src/components/HabitGrid.jsx
@@ -1,9 +1,10 @@
import React, { useMemo, useEffect } from 'react';
import { motion } from 'framer-motion';
-import { getColorIntensity, isToday, formatDate, getWeekdayLabel } from '../lib/utils-habit';
+import { getColorIntensity, isToday, formatDate, getWeekdayLabel, getFrozenDays } from '../lib/utils-habit';
import { toggleCompletion } from '../lib/storage';
const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
+ const frozenDays = getFrozenDays(habit.completions);
const weeks = useMemo(() => {
const today = new Date();
// Find the Monday of the current week
@@ -67,13 +68,14 @@ const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
const intensity = isCompleted ? getColorIntensity(habit.completions, dateStr) : 0;
const isTodayCell = isToday(date);
const isFuture = date > new Date();
+ const isFrozen = frozenDays.includes(dateStr);
return (
handleCellClick(date)}
- className="habit-cell w-3 h-3 rounded-sm"
+ className="habit-cell w-3 h-3 rounded-sm flex items-center justify-center"
style={{
backgroundColor: isCompleted ? habit.color : 'transparent',
opacity: isFuture ? 0 : (isCompleted ? 0.3 + (intensity * 0.7) : 1),
@@ -81,8 +83,12 @@ const HabitGrid = ({ habit, onUpdate, fullView = false }) => {
pointerEvents: isFuture ? 'none' : 'auto',
visibility: isFuture ? 'hidden' : 'visible',
}}
- title={`${dateStr}${isCompleted ? ' ✓' : ''}`}
- />
+ title={`${dateStr}${isCompleted ? ' ✓' : ''}${isFrozen ? ' (Frozen)' : ''}`}
+ >
+ {isFrozen && (
+ ❄️
+ )}
+
);
})}
diff --git a/src/components/MiniGrid.jsx b/src/components/MiniGrid.jsx
index 0335423..40d80e9 100644
--- a/src/components/MiniGrid.jsx
+++ b/src/components/MiniGrid.jsx
@@ -1,6 +1,21 @@
import React from 'react';
+// Utility to lighten a hex color
+function lightenColor(hex, percent) {
+ hex = hex.replace(/^#/, '');
+ if (hex.length === 3) hex = hex.split('').map(x => x + x).join('');
+ const num = parseInt(hex, 16);
+ let r = (num >> 16) + Math.round(255 * percent);
+ let g = ((num >> 8) & 0x00FF) + Math.round(255 * percent);
+ let b = (num & 0x0000FF) + Math.round(255 * percent);
+ r = Math.min(255, r);
+ g = Math.min(255, g);
+ b = Math.min(255, b);
+ return `rgb(${r},${g},${b})`;
+}
+import { Flame } from 'lucide-react';
import { motion } from 'framer-motion';
import { getColorIntensity, isToday, formatDate } from '../lib/utils-habit';
+import { getFrozenDays } from '../lib/utils-habit';
import { toggleCompletion } from '../lib/storage';
const MiniGrid = ({ habit, onUpdate }) => {
@@ -31,35 +46,46 @@ const MiniGrid = ({ habit, onUpdate }) => {
return (
- {days.map((date, index) => {
- const dateStr = formatDate(date);
- const isCompleted = habit.completions.includes(dateStr);
- const intensity = isCompleted ? getColorIntensity(habit.completions, dateStr) : 0;
- const isTodayCell = isToday(date);
- const dayLetter = date.toLocaleDateString('en-US', { weekday: 'short' })[0];
-
- return (
-
- handleCellClick(e, date)}
- className={`habit-cell flex w-8 h-8 transition-all ${isTodayCell ? 'rounded-md' : 'rounded-2xl'}`}
- style={{
- backgroundColor: isCompleted
- ? habit.color
- : 'transparent',
- opacity: isCompleted ? 0.3 + (intensity * 0.7) : 1,
- border: isTodayCell
- ? `2px solid ${habit.color}`
- : `1px solid ${habit.color}20`,
- }}
- title={dateStr}
- />
- {dayLetter}
-
- );
- })}
+ {(() => {
+ const frozenDays = getFrozenDays(habit.completions);
+ return days.map((date, index) => {
+ const dateStr = formatDate(date);
+ const isCompleted = habit.completions.includes(dateStr);
+ const intensity = isCompleted ? getColorIntensity(habit.completions, dateStr) : 0;
+ const isTodayCell = isToday(date);
+ const dayLetter = date.toLocaleDateString('en-US', { weekday: 'short' })[0];
+ const isFrozen = frozenDays.includes(dateStr);
+ return (
+
+ handleCellClick(e, date)}
+ className={`habit-cell flex w-8 h-8 transition-all items-center justify-center ${isTodayCell ? 'rounded-md' : 'rounded-2xl'}`}
+ style={{
+ backgroundColor: isCompleted
+ ? habit.color
+ : 'transparent',
+ opacity: isCompleted ? 0.3 + (intensity * 0.7) : 1,
+ border: isTodayCell
+ ? `2px solid ${habit.color}`
+ : `1px solid ${habit.color}20`,
+ }}
+ title={dateStr}
+ >
+ {isFrozen && (
+ ❄️
+ )}
+ {/* Flame icon for full streak days */}
+ {isCompleted && intensity >= 1 && (
+
+ )}
+
+ {dayLetter}
+
+ );
+ });
+ })()}
);
};
diff --git a/src/lib/storage.js b/src/lib/storage.js
index a79bc77..2c5889c 100644
--- a/src/lib/storage.js
+++ b/src/lib/storage.js
@@ -65,12 +65,15 @@ export const toggleCompletion = (habitId, dateStr) => {
});
};
+import { getFrozenDays } from './utils-habit.js';
const calculateStreaks = (completions) => {
if (completions.length === 0) {
return { currentStreak: 0, longestStreak: 0 };
}
-
- const sortedDates = completions
+ // 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);
@@ -88,15 +91,12 @@ const calculateStreaks = (completions) => {
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++;
@@ -112,9 +112,7 @@ const calculateStreaks = (completions) => {
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);
@@ -124,9 +122,8 @@ const calculateStreaks = (completions) => {
}
longestStreak = Math.max(longestStreak, currentStreak, 1);
-
return { currentStreak, longestStreak };
-};
+}
export const exportData = () => {
const habits = getHabits();
diff --git a/src/lib/utils-habit.js b/src/lib/utils-habit.js
index b4c97f6..db48561 100644
--- a/src/lib/utils-habit.js
+++ b/src/lib/utils-habit.js
@@ -39,4 +39,38 @@ export const getColorIntensity = (completions, dateStr) => {
export const getWeekdayLabel = (dayIndex) => {
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return labels[dayIndex];
-};
\ No newline at end of file
+};
+ // Returns array of frozen days (date strings) for a given completions array
+ export function getFrozenDays(completions) {
+ // Map: month string -> frozen day string
+ const frozenDays = [];
+ const completedSet = new Set(completions);
+ // Sort completions for easier lookup
+ const sorted = [...completions].sort();
+ // Track frozen per month
+ const frozenPerMonth = {};
+ // To find missed days, scan a range of dates
+ if (completions.length === 0) return [];
+ const minDate = new Date(sorted[0]);
+ const maxDate = new Date(sorted[sorted.length - 1]);
+ for (let d = new Date(minDate); d <= maxDate; d.setDate(d.getDate() + 1)) {
+ const dateStr = formatDate(d);
+ if (completedSet.has(dateStr)) continue; // skip completed days
+ // Check neighbors
+ const prevDate = new Date(d); prevDate.setDate(prevDate.getDate() - 1);
+ const nextDate = new Date(d); nextDate.setDate(nextDate.getDate() + 1);
+ const prevDateStr = formatDate(prevDate);
+ const nextDateStr = formatDate(nextDate);
+ // Only freeze if both neighbors are completed
+ const monthKey = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;
+ if (
+ completedSet.has(prevDateStr) &&
+ completedSet.has(nextDateStr) &&
+ !frozenPerMonth[monthKey]
+ ) {
+ frozenDays.push(dateStr);
+ frozenPerMonth[monthKey] = true;
+ }
+ }
+ return frozenDays;
+ }
\ No newline at end of file