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'; // Helpers to get custom icons from localStorage or fallback function getStreakIcon() { if (typeof window === 'undefined') return ( ); const icon = localStorage.getItem('streakIcon'); if (!icon || icon === 'flame') return ( ); return ( {icon} ); } function getFreezeIcon() { if (typeof window === 'undefined') return '❄️'; const icon = localStorage.getItem('freezeIcon'); return icon || '❄️'; } import { motion } from 'framer-motion'; import { getColorIntensity, isToday, formatDate } from '../lib/utils-habit'; import { getFrozenDays } from '../lib/utils-habit'; import { toggleCompletion, getAuthUser } from '../lib/datastore'; import { toast } from './ui/use-toast'; const MiniGrid = ({ habit, onUpdate }) => { const today = new Date(); // Dynamically calculate number of days that fit based on window width and cell size, max 28 const CELL_SIZE = 42; // px, matches w-8 h-8 const PADDING = 16; // px, for grid padding/margin const numDays = Math.min(28, Math.max(5, Math.floor((window.innerWidth - PADDING) / CELL_SIZE))); const days = []; const scrollRef = React.useRef(null); React.useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollLeft = scrollRef.current.scrollWidth; } }, [numDays, habit.completions]); for (let i = numDays - 1; i >= 0; i--) { const date = new Date(today); date.setDate(date.getDate() - i); days.push(date); } const handleCellClick = async (e, date) => { e.stopPropagation(); const dateStr = formatDate(date); const isTodayCell = isToday(date); const wasCompleted = habit.completions.includes(dateStr); const user = await getAuthUser(); if (user) { // Optimistically update completions for instant UI const habits = JSON.parse(localStorage.getItem('habitgrid_data') || '[]'); const idx = habits.findIndex(h => h.id === habit.id); if (idx !== -1) { const completions = Array.isArray(habits[idx].completions) ? [...habits[idx].completions] : []; const cidx = completions.indexOf(dateStr); if (cidx > -1) completions.splice(cidx, 1); else completions.push(dateStr); habits[idx].completions = completions; localStorage.setItem('habitgrid_data', JSON.stringify(habits)); } onUpdate(); // Sync in background toggleCompletion(habit.id, dateStr); } else { // Local-only: just call toggleCompletion, then update UI await toggleCompletion(habit.id, dateStr); onUpdate(); } // Only show encouragement toast if validating (adding) today's dot if (isTodayCell && !wasCompleted) { try { const res = await fetch('/encouragements.json'); const messages = await res.json(); const msg = messages[Math.floor(Math.random() * messages.length)]; toast({ title: '🎉 Keep Going!', description: msg, duration: 2500, }); } catch (err) { // fallback message toast({ title: '🎉 Keep Going!', description: 'Great job! Keep up the streak!', duration: 2500, }); } } }; return (
{(() => { 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]; // Check if previous day was completed and next day is today let isFrozen = frozenDays.includes(dateStr); if (!isCompleted && !isTodayCell && index < days.length - 1 && index > 0) { const prevDateStr = formatDate(days[index - 1]); const nextDateStr = formatDate(days[index + 1]); const prevCompleted = habit.completions.includes(prevDateStr); const nextIsToday = isToday(days[index + 1]); if (prevCompleted && nextIsToday) { isFrozen = true; } } 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 && ( {getFreezeIcon()} )} {/* Flame icon for full streak days */} {isCompleted && intensity >= 1 && ( {}, } }} whileHover={{ scale: 1.5, rotate: 10 }} whileTap={{ scale: 1.2, rotate: 0 }} > {getStreakIcon()} )} {dayLetter}
); }); })()}
); }; export default MiniGrid;