From f830e4fccfb2f561092ec9ca557d5afe5225a012 Mon Sep 17 00:00:00 2001 From: count0 Date: Wed, 15 Oct 2025 13:50:20 +0200 Subject: [PATCH] Update for Sally Added Freeze Day --- src/components/HabitGrid.jsx | 14 ++++-- src/components/MiniGrid.jsx | 84 +++++++++++++++++++++++------------- src/lib/storage.js | 15 +++---- src/lib/utils-habit.js | 36 +++++++++++++++- 4 files changed, 106 insertions(+), 43 deletions(-) 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