147 lines
4.0 KiB
TypeScript
147 lines
4.0 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import {
|
|
DndContext,
|
|
DragOverlay,
|
|
PointerSensor,
|
|
useSensor,
|
|
useSensors,
|
|
} from '@dnd-kit/core';
|
|
import type { DragEndEvent, DragOverEvent, DragStartEvent } from '@dnd-kit/core';
|
|
import { Column } from './components/Column';
|
|
import { TaskCard } from './components/TaskCard';
|
|
import type { Task, Column as ColumnType, ColumnId } from './types';
|
|
import './App.css';
|
|
|
|
const defaultColumns: ColumnType[] = [
|
|
{ id: 'backlog', title: '📋 Backlog', tasks: [] },
|
|
{ id: 'todo', title: '📝 To Do', tasks: [] },
|
|
{ id: 'doing', title: '🔨 Doing', tasks: [] },
|
|
{ id: 'done', title: '✅ Done', tasks: [] },
|
|
];
|
|
|
|
function App() {
|
|
const [columns, setColumns] = useState<ColumnType[]>(() => {
|
|
const saved = localStorage.getItem('kanban-data');
|
|
return saved ? JSON.parse(saved) : defaultColumns;
|
|
});
|
|
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
|
|
|
const sensors = useSensors(
|
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } })
|
|
);
|
|
|
|
useEffect(() => {
|
|
localStorage.setItem('kanban-data', JSON.stringify(columns));
|
|
}, [columns]);
|
|
|
|
const findTask = (id: string): { task: Task; columnId: string } | null => {
|
|
for (const column of columns) {
|
|
const task = column.tasks.find((t) => t.id === id);
|
|
if (task) return { task, columnId: column.id };
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const handleDragStart = (event: DragStartEvent) => {
|
|
const found = findTask(event.active.id as string);
|
|
if (found) setActiveTask(found.task);
|
|
};
|
|
|
|
const handleDragOver = (event: DragOverEvent) => {
|
|
const { active, over } = event;
|
|
if (!over) return;
|
|
|
|
const activeId = active.id as string;
|
|
const overId = over.id as string;
|
|
|
|
const activeData = findTask(activeId);
|
|
if (!activeData) return;
|
|
|
|
const overColumn = columns.find((c) => c.id === overId);
|
|
const overTask = findTask(overId);
|
|
|
|
const targetColumnId = overColumn?.id || overTask?.columnId;
|
|
if (!targetColumnId || targetColumnId === activeData.columnId) return;
|
|
|
|
setColumns((cols) => {
|
|
const newCols = cols.map((c) => ({ ...c, tasks: [...c.tasks] }));
|
|
const sourceCol = newCols.find((c) => c.id === activeData.columnId)!;
|
|
const targetCol = newCols.find((c) => c.id === targetColumnId)!;
|
|
|
|
const taskIndex = sourceCol.tasks.findIndex((t) => t.id === activeId);
|
|
const [task] = sourceCol.tasks.splice(taskIndex, 1);
|
|
targetCol.tasks.push(task);
|
|
|
|
return newCols;
|
|
});
|
|
};
|
|
|
|
const handleDragEnd = (_event: DragEndEvent) => {
|
|
setActiveTask(null);
|
|
};
|
|
|
|
const addTask = (columnId: ColumnId, title: string) => {
|
|
const newTask: Task = {
|
|
id: crypto.randomUUID(),
|
|
title,
|
|
createdAt: Date.now(),
|
|
};
|
|
setColumns((cols) =>
|
|
cols.map((c) =>
|
|
c.id === columnId ? { ...c, tasks: [...c.tasks, newTask] } : c
|
|
)
|
|
);
|
|
};
|
|
|
|
const deleteTask = (taskId: string) => {
|
|
setColumns((cols) =>
|
|
cols.map((c) => ({
|
|
...c,
|
|
tasks: c.tasks.filter((t) => t.id !== taskId),
|
|
}))
|
|
);
|
|
};
|
|
|
|
const updateTask = (taskId: string, updates: Partial<Task>) => {
|
|
setColumns((cols) =>
|
|
cols.map((c) => ({
|
|
...c,
|
|
tasks: c.tasks.map((t) =>
|
|
t.id === taskId ? { ...t, ...updates } : t
|
|
),
|
|
}))
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div className="app">
|
|
<header>
|
|
<h1>🐾 Clawd's Kanban</h1>
|
|
</header>
|
|
<DndContext
|
|
sensors={sensors}
|
|
onDragStart={handleDragStart}
|
|
onDragOver={handleDragOver}
|
|
onDragEnd={handleDragEnd}
|
|
>
|
|
<div className="board">
|
|
{columns.map((column) => (
|
|
<Column
|
|
key={column.id}
|
|
column={column}
|
|
onAddTask={(title) => addTask(column.id as ColumnId, title)}
|
|
onDeleteTask={deleteTask}
|
|
onUpdateTask={updateTask}
|
|
/>
|
|
))}
|
|
</div>
|
|
<DragOverlay>
|
|
{activeTask && <TaskCard task={activeTask} isDragging />}
|
|
</DragOverlay>
|
|
</DndContext>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App;
|