Add src/components/CalendarView.tsx

This commit is contained in:
2026-05-02 18:03:57 -04:00
parent cd55bc5fda
commit 63d580ac4f

View File

@@ -0,0 +1,160 @@
'use client';
import { useApp, type Task } from './AppProvider';
import { useState, useMemo } from 'react';
import {
startOfMonth, endOfMonth, startOfWeek, endOfWeek, addMonths, subMonths,
format, isSameDay, parseISO, isToday, isPast, isFuture,
eachDayOfInterval,
} from 'date-fns';
export default function CalendarView() {
const { tasks, projects, setSelectedTask, setShowEditTaskModal, setEditingTask } = useApp();
const [currentDate, setCurrentDate] = useState(new Date());
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
const monthStart = startOfMonth(currentDate);
const monthEnd = endOfMonth(currentDate);
const calendarStart = startOfWeek(monthStart);
const calendarEnd = endOfWeek(monthEnd);
const days = eachDayOfInterval({ start: calendarStart, end: calendarEnd });
const daysInMonth = days.filter(d => d.getMonth() === month);
// Group tasks by date
const tasksByDate = useMemo(() => {
const map = new Map<string, Task[]>();
tasks.forEach(task => {
if (!task.dueDate) return;
const date = format(parseISO(task.dueDate), 'yyyy-MM-dd');
if (!map.has(date)) map.set(date, []);
map.get(date)!.push(task);
});
return map;
}, [tasks]);
const priorityColors = {
urgent: 'bg-[var(--priority-urgent)]',
high: 'bg-[var(--priority-high)]',
medium: 'bg-[var(--priority-medium)]',
low: 'bg-[var(--priority-low)]',
};
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const monthName = format(currentDate, 'MMMM yyyy');
const handleDayClick = (date: Date) => {
// Could open a "new task" modal pre-filled with this date
console.log('Day clicked:', format(date, 'yyyy-MM-dd'));
};
const handleTaskClick = (task: Task) => {
setSelectedTask(task);
setEditingTask(task);
setShowEditTaskModal(true);
};
return (
<div className="flex-1 overflow-y-auto p-4">
<div className="max-w-4xl mx-auto">
{/* Month navigation */}
<div className="flex items-center justify-between mb-4">
<button
onClick={() => setCurrentDate(subMonths(currentDate, 1))}
className="p-2 hover:bg-content rounded-lg transition-colors"
>
</button>
<h2 className="text-xl font-bold">{monthName}</h2>
<button
onClick={() => setCurrentDate(addMonths(currentDate, 1))}
className="p-2 hover:bg-content rounded-lg transition-colors"
>
</button>
</div>
{/* Calendar grid */}
<div className="bg-card border border-border rounded-xl overflow-hidden">
{/* Week day headers */}
<div className="grid grid-cols-7 border-b border-border">
{weekDays.map(day => (
<div key={day} className="p-2 text-center text-xs font-semibold text-text-secondary bg-content">
{day}
</div>
))}
</div>
{/* Days */}
<div className="grid grid-cols-7">
{days.map((day, index) => {
const dateStr = format(day, 'yyyy-MM-dd');
const isCurrentMonth = day.getMonth() === month;
const isTodayDate = isToday(day);
const dayTasks = tasksByDate.get(dateStr) || [];
const incompleteTasks = dayTasks.filter(t => !t.completed);
return (
<div
key={dateStr}
onClick={() => handleDayClick(day)}
className={`min-h-[80px] p-1.5 border-b border-r border-border cursor-pointer hover:bg-content/50 transition-colors ${
!isCurrentMonth ? 'opacity-40' : ''
}`}
>
{/* Date number */}
<div className="flex items-center justify-between mb-1">
<span className={`text-xs font-medium ${
isTodayDate
? 'bg-accent text-white w-6 h-6 rounded-full flex items-center justify-center'
: isPast(day) && !isTodayDate
? 'text-text-secondary'
: 'text-text-primary'
}`}>
{format(day, 'd')}
</span>
{incompleteTasks.length > 0 && (
<span className="text-xs text-accent font-medium">{incompleteTasks.length}</span>
)}
</div>
{/* Task indicators */}
<div className="space-y-0.5">
{dayTasks.slice(0, 3).map(task => (
<button
key={task.id}
onClick={(e) => { e.stopPropagation(); handleTaskClick(task); }}
className={`w-full text-left text-xs px-1 py-0.5 rounded truncate ${priorityColors[task.priority]} bg-opacity-20 text-text-primary hover:bg-opacity-30 transition-colors`}
title={task.title}
>
{task.title}
</button>
))}
{dayTasks.length > 3 && (
<span className="text-xs text-text-secondary pl-1">
+{dayTasks.length - 3} more
</span>
)}
</div>
</div>
);
})}
</div>
</div>
{/* Legend */}
<div className="mt-4 flex items-center gap-4 text-xs text-text-secondary">
<span className="font-medium">Priority:</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-[var(--priority-urgent)]" /> Urgent</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-[var(--priority-high)]" /> High</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-[var(--priority-medium)]" /> Medium</span>
<span className="flex items-center gap-1"><span className="w-2 h-2 rounded-full bg-[var(--priority-low)]" /> Low</span>
</div>
</div>
</div>
);
}