Add src/components/CalendarView.tsx
This commit is contained in:
160
src/components/CalendarView.tsx
Normal file
160
src/components/CalendarView.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user