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