Files
kanban/src/App.tsx
2026-02-16 23:23:53 +01:00

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;