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;