import React from 'react';
import { useTasks, Task } from './TasksProvider';
import { useDrag, useDrop } from 'react-dnd';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import TaskDialog from './TaskDialog';
const dayInMs = 24 * 60 * 60 * 1000;
function getNextDueDate(task: Task): Date {
const start = new Date(task.start_date);
const localStart = new Date(
start.getFullYear(),
start.getMonth(),
start.getDate()
);
const today = new Date();
const localToday = new Date(
today.getFullYear(),
today.getMonth(),
today.getDate()
);
if (localStart >= localToday) return localStart;
const diffDays = Math.ceil(
(localToday.getTime() - localStart.getTime()) / dayInMs
);
const cycles = Math.ceil(diffDays / task.interval);
return new Date(localStart.getTime() + cycles * task.interval * dayInMs);
}
// Move TaskItem out as a separate component to fix the rules-of-hooks issue
type TaskItemProps = {
task: Task;
isToday: boolean;
getTaskStatus: (task: Task, isToday: boolean) => 'past' | 'imminent' | 'upcoming';
setSelectedTask: (task: Task) => void;
setIsDialogOpen: (isOpen: boolean) => void;
dividerIndex?: number;
index?: number;
};
const TaskItem: React.FC<TaskItemProps> = ({
task,
isToday,
getTaskStatus,
setSelectedTask,
setIsDialogOpen,
dividerIndex,
index
}) => {
const [{ opacity }, dragRef] = useDrag({
type: 'TASK',
item: { id: task.id, currentStartDate: task.start_date },
collect: (monitor) => ({ opacity: monitor.isDragging() ? 0.5 : 1 })
});
const status = getTaskStatus(task, isToday);
const showDivider = isToday && typeof index === 'number' && typeof dividerIndex === 'number' && index === dividerIndex;
if (showDivider) {
return (
<React.Fragment>
<div className="divider text-sm text-gray-500">Now</div>
<div
// @ts-ignore
ref={dragRef}
onClick={() => {
setSelectedTask(task);
setIsDialogOpen(true);
}}
className={`card p-2 shadow transition-all ${
status === 'imminent' ? 'bg-gray-100 animate-pulse' : ''
} hover:bg-orange-50 cursor-pointer`}
style={{
opacity: status === 'past' ? 0.5 : (typeof opacity === 'number' ? opacity : 1)
}}
>
<p className="text-sm font-medium">{task.name}</p>
<p className="text-xs text-gray-500">
{task.time} • {task.interval} days •{' '}
{`${task.difficulty}🔥`}
</p>
</div>
</React.Fragment>
);
}
return (
<div
// @ts-ignore
ref={dragRef}
onClick={() => {
setSelectedTask(task);
setIsDialogOpen(true);
}}
className={`relative p-2 rounded shadow-sm transition-all hover:shadow-lg
${status === 'imminent' ? 'bg-amber-50 border-amber-200' : 'bg-white'}
hover:bg-orange-50 border-gray-200 cursor-pointer`}
style={{
opacity: status === 'past' ? 0.5 : (typeof opacity === 'number' ? opacity : 1)
}}
>
{/* Badge for difficulty */}
<div className="absolute top-1 right-1 inline-flex items-center gap-1 text-xs font-medium text-gray-700 bg-white/80 rounded-full px-1.5 py-0.5 border shadow-sm">
🔥 {task.difficulty}
</div>
{/* Main content */}
<div className="px-1">
<div className="font-medium text-sm truncate">{task.name}</div>
<div className="flex items-center gap-1 text-xs text-gray-600">
<span className="inline-block w-3 h-3">⏳</span>
<span className="truncate">
{task.time} • {task.interval} days
</span>
</div>
</div>
</div>
);
};
export default function UpcomingTasksKanban() {
const { tasks, updateTask, deleteTask, setIsDialogOpen, setSelectedTask, isDialogOpen, selectedTask } = useTasks();
const today = new Date();
today.setHours(0, 0, 0, 0);
// Simply create an array of the next 7 days starting from today
const columns = Array.from(
{ length: 7 },
(_, i) => new Date(today.getTime() + i * dayInMs)
);
// Initialize columns by due date key (YYYY-MM-DD)
const tasksByDate: Record<string, Task[]> = {};
columns.forEach((date) => {
const key = date.toISOString().slice(0, 10);
tasksByDate[key] = [];
});
// Group tasks by all upcoming due dates in the displayed range
tasks.forEach((task) => {
let due = getNextDueDate(task);
const lastDate = columns[columns.length - 1];
while (due <= lastDate) {
const key = due.toISOString().slice(0, 10);
if (tasksByDate[key]) tasksByDate[key].push(task);
due = new Date(due.getTime() + task.interval * dayInMs);
}
});
// Update the sorting to consider time only
const sortTasks = (a: Task, b: Task) => {
return a.time.localeCompare(b.time);
};
// Add this helper function before the Column component
const getTaskStatus = (task: Task, isToday: boolean) => {
if (!isToday) return 'upcoming';
const now = new Date();
const [hours, minutes] = task.time.split(':').map(Number);
const taskTime = new Date();
taskTime.setHours(hours, minutes, 0, 0);
if (taskTime < now) return 'past';
// Check if task is within the next hour
const nextHour = new Date(now.getTime() + 60 * 60 * 1000);
if (taskTime <= nextHour) return 'imminent';
return 'upcoming';
};
const handleSave = (updatedFields: Partial<Task>) => {
if (selectedTask?.id) {
updateTask(selectedTask.id, updatedFields);
}
};
// Modify the Column component
const Column = ({
date,
tasksForDate
}: {
date: Date;
tasksForDate: Task[];
}) => {
const [, drop] = useDrop(() => ({
accept: 'TASK',
drop: (item: { id: string; currentStartDate: string }) => {
updateTask(item.id, { start_date: date.toISOString() });
}
}));
const isToday = date.toDateString() === new Date().toDateString();
const now = new Date();
const currentTime = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`;
// Sort tasks and find the insertion point for the "Now" divider
const sortedTasks = [...tasksForDate].sort((a, b) =>
a.time.localeCompare(b.time)
);
const dividerIndex = isToday
? sortedTasks.findIndex((task) => task.time > currentTime)
: -1;
return (
// @ts-ignore
<div ref={drop} className="flex-1 max-w-[300px]">
<div className="text-center font-bold mb-2">
<div className="flex flex-col items-center">
<span className="text-gray-600 text-xs">{date.toLocaleDateString('bg-BG', { weekday: 'short' }).toLowerCase()}</span>
<span className="text-gray-900 text-sm">
{date.toLocaleDateString('bg-BG', { day: '2-digit', month: '2-digit' }).replace('/', '.')}
</span>
</div>
</div>
<div className="space-y-2">
{sortedTasks.map((task, index) => {
// Create a component to use the hook correctly
return <TaskItem key={task.id} task={task} isToday={isToday} getTaskStatus={getTaskStatus} setSelectedTask={setSelectedTask} setIsDialogOpen={setIsDialogOpen} dividerIndex={dividerIndex} index={index} />;
})}
</div>
</div>
);
};
return (
<DndProvider backend={HTML5Backend}>
<div className="overflow-x-auto w-full">
<div className="flex space-x-4 mb-4 min-w-max">
{columns.map((date) => {
const key = date.toISOString().slice(0, 10);
let tasksForDate = tasksByDate[key] || [];
tasksForDate = tasksForDate.sort(sortTasks);
return <Column key={key} date={date} tasksForDate={tasksForDate} />;
})}
</div>
</div>
<TaskDialog
isOpen={isDialogOpen}
onClose={() => {
setIsDialogOpen(false);
setSelectedTask(undefined);
}}
onSave={handleSave}
onDelete={deleteTask}
task={selectedTask}
/>
</DndProvider>
);
}