Files
HabitGrid/src/components/HabitGrid.jsx
count0 f830e4fccf Update for Sally
Added Freeze Day
2025-10-15 13:50:20 +02:00

133 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useMemo, useEffect } from 'react';
import { motion } from 'framer-motion';
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
const todayDay = today.getDay(); // 0=Sun, 1=Mon, ...
const daysSinceMonday = (todayDay + 6) % 7; // 0=Mon, 1=Tue, ..., 6=Sun
const mondayThisWeek = new Date(today);
mondayThisWeek.setDate(today.getDate() - daysSinceMonday);
const weeksArray = [];
const totalWeeks = fullView ? 52 : 12;
for (let week = totalWeeks - 1; week >= 0; week--) {
const weekDays = [];
// For each week, calculate Monday, then add 0..6 days for each row
const monday = new Date(mondayThisWeek);
monday.setDate(mondayThisWeek.getDate() - week * 7);
for (let day = 0; day < 7; day++) {
const date = new Date(monday);
date.setDate(monday.getDate() + day);
weekDays.push(date);
}
weeksArray.push(weekDays);
}
return weeksArray;
}, [fullView]);
useEffect(() => {
// Scroll to the rightmost (most recent) week on mount
const gridScroll = document.querySelector('.grid-scroll');
if (gridScroll) {
gridScroll.scrollLeft = gridScroll.scrollWidth;
}
}, []);
const handleCellClick = (date) => {
toggleCompletion(habit.id, formatDate(date));
onUpdate();
};
return (
<div className="bg-white dark:bg-slate-800 rounded-2xl p-4 pt-0 shadow-sm border border-slate-200 dark:border-slate-700 flex flex-col items-center">
<div className="mb-2 text-center w-full">
<h2 className="text-lg font-semibold mb-1 mt-4">Activity Calendar</h2>
<p className="text-sm text-muted-foreground">
Tap any day to mark it as complete
</p>
</div>
<div className="overflow-x-auto grid-scroll mt-2 w-full flex justify-center">
<div className="inline-flex gap-1 mb-4">
{/* Grid: Monday (top) to Sunday (bottom) */}
{weeks.map((week, weekIndex) => (
<div key={weekIndex} className="flex flex-col gap-1">
{/* Month label */}
<div className="h-3 text-xs text-muted-foreground text-center">
{weekIndex % 4 === 0 && week[0].toLocaleDateString('en-US', { month: 'short' })}
</div>
{/* Days: Monday (top) to Sunday (bottom) */}
{week.map((date, dayIndex) => {
const dateStr = formatDate(date);
const isCompleted = habit.completions.includes(dateStr);
const intensity = isCompleted ? getColorIntensity(habit.completions, dateStr) : 0;
const isTodayCell = isToday(date);
const isFuture = date > new Date();
const isFrozen = frozenDays.includes(dateStr);
return (
<motion.button
key={dayIndex}
whileHover={{ scale: 1.15 }}
whileTap={{ scale: 0.9 }}
onClick={() => handleCellClick(date)}
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),
border: isTodayCell ? `2px solid ${habit.color}` : `1px solid ${habit.color}20`,
pointerEvents: isFuture ? 'none' : 'auto',
visibility: isFuture ? 'hidden' : 'visible',
}}
title={`${dateStr}${isCompleted ? ' ✓' : ''}${isFrozen ? ' (Frozen)' : ''}`}
>
{isFrozen && (
<span role="img" aria-label="Frozen" style={{ fontSize: '0.7em' }}></span>
)}
</motion.button>
);
})}
</div>
))}
{/* Weekday labels: Monday (top) to Sunday (bottom) */}
<div className="flex flex-col gap-1 ml-2">
<div className="h-1" />
{[1, 2, 3, 4, 5, 6, 0].map((day) => (
<div
key={day}
className="h-3 flex items-center justify-end text-xs text-muted-foreground pr-0"
>
{getWeekdayLabel(day)}
</div>
))}
</div>
</div>
</div>
{/* Legend */}
<div className="flex items-center gap-2 mt-4 text-xs text-muted-foreground">
<span>Less</span>
<div className="flex gap-1">
{[0, 0.25, 0.5, 0.75, 1].map((intensity, i) => (
<div
key={i}
className="w-3 h-3 rounded-sm"
style={{
backgroundColor: habit.color,
opacity: 0.3 + (intensity * 0.7),
border: `1px solid ${habit.color}20`,
}}
/>
))}
</div>
<span>More</span>
</div>
</div>
);
};
export default HabitGrid;