Add drag and drop for Kanban board cards

This commit is contained in:
Jarian Cottingham 2026-02-24 22:39:05 -06:00
parent 7044ffa751
commit 4e2ef81f97
5 changed files with 112 additions and 26 deletions

View File

@ -1,26 +1,9 @@
import Board from './components/Board'
import Calculator from './components/Calculator'
function App() {
return (
<div className="min-h-screen text-white p-6">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-8 animate-fade-in">
<div>
<h1 className="text-5xl font-bold bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 bg-clip-text text-transparent bg-gradient-to-r bg-[length:200%_auto] animate-gradient-text">
WorkBoard
</h1>
<p className="text-slate-400 mt-2 text-lg">AI-powered task management</p>
</div>
<div className="flex items-center gap-3 animate-slide-in">
<div className="relative">
<div className="w-3 h-3 rounded-full bg-emerald-500 animate-pulse"></div>
<span className="absolute inset-0 rounded-full bg-emerald-500 animate-ping opacity-75"></span>
</div>
<span className="text-sm font-medium text-slate-300">AI Agent Ready</span>
</div>
</div>
<Board />
</div>
<div className="min-h-screen bg-slate-900 flex items-center justify-center p-4">
<Calculator />
</div>
)
}

View File

@ -10,6 +10,10 @@ const Board = () => {
deleteWorkItem,
executeNext,
isExecuting,
dragOverColumn,
handleDragOver,
handleDragLeave,
handleDrop,
} = useWorkItems()
const queuedItems = workItems.filter((item) => item.status === 'queued')
@ -32,6 +36,10 @@ const Board = () => {
onDelete={deleteWorkItem}
onAdd={addWorkItem}
color="blue"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
isDragOver={dragOverColumn === 'queued'}
/>
<Column
title="Active"
@ -41,6 +49,10 @@ const Board = () => {
onDelete={deleteWorkItem}
onAdd={addWorkItem}
color="purple"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
isDragOver={dragOverColumn === 'active'}
/>
<Column
title="Complete"
@ -50,6 +62,10 @@ const Board = () => {
onDelete={deleteWorkItem}
onAdd={addWorkItem}
color="green"
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
isDragOver={dragOverColumn === 'complete'}
/>
</div>
)

View File

@ -10,6 +10,10 @@ interface ColumnProps {
onDelete: (id: string) => void
onAdd: (data: WorkItemFormData) => void
color: 'blue' | 'purple' | 'green'
onDragOver: (status: string) => void
onDragLeave: () => void
onDrop: (status: string) => void
isDragOver: boolean
}
interface WorkItem {
@ -24,7 +28,7 @@ interface WorkItem {
lastAttemptedAt: number | null
}
const Column = ({ title, status, items, onUpdate, onDelete, onAdd, color }: ColumnProps) => {
const Column = ({ title, status, items, onUpdate, onDelete, onAdd, color, onDragOver, onDragLeave, onDrop, isDragOver }: ColumnProps) => {
const [isModalOpen, setIsModalOpen] = useState(false)
const [formData, setFormData] = useState<WorkItemFormData>({
title: '',
@ -69,7 +73,20 @@ const Column = ({ title, status, items, onUpdate, onDelete, onAdd, color }: Colu
const styles = colorStyles[color]
return (
<div className={`flex-1 rounded-2xl p-5 ${styles.bg} border ${styles.border} backdrop-blur-sm transition-all duration-300 hover:shadow-2xl hover:${styles.glow}`}>
<div
onDragOver={(e) => {
e.preventDefault()
onDragOver(status)
}}
onDragLeave={onDragLeave}
onDrop={(e) => {
e.preventDefault()
onDrop(status)
}}
className={`flex-1 rounded-2xl p-5 transition-all duration-300 ${
isDragOver ? 'ring-4 ring-blue-500/50 bg-blue-500/10' : `${styles.bg} border ${styles.border}`
} backdrop-blur-sm hover:shadow-2xl hover:${styles.glow}`}
>
<div className="flex items-center justify-between mb-6 animate-slide-in">
<div className="flex items-center gap-3">
<div className={`relative w-3 h-3 rounded-full ${color === 'blue' ? 'bg-blue-400' : color === 'purple' ? 'bg-purple-400' : 'bg-emerald-400'} animate-pulse`}>
@ -88,6 +105,10 @@ const Column = ({ title, status, items, onUpdate, onDelete, onAdd, color }: Colu
workItem={item}
onUpdate={onUpdate}
onDelete={onDelete}
onDragStart={() => {}}
onDragOver={() => {}}
onDragLeave={() => {}}
onDrop={() => {}}
/>
))}
{status === 'active' && items.some(i => i.status === 'active') && (

View File

@ -5,9 +5,13 @@ interface Props {
workItem: WorkItem
onUpdate: (id: string, updates: Partial<WorkItem>) => void
onDelete: (id: string) => void
onDragStart: (item: WorkItem) => void
onDragOver: () => void
onDragLeave: () => void
onDrop: () => void
}
const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
const WorkItemComponent = ({ workItem, onUpdate, onDelete, onDragStart, onDragOver, onDragLeave, onDrop }: Props) => {
const [isExpanded, setIsExpanded] = useState(false)
const [formData, setFormData] = useState<WorkItemFormData>({
title: workItem.title,
@ -37,7 +41,26 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
}
return (
<div className={`glass-card p-5 transition-all duration-300 ${isExpanded ? 'ring-2 ring-blue-500/50' : ''} ${workItem.failedAttempts >= 3 ? 'border-red-500/50' : ''}`}>
<div
draggable
onDragStart={(e) => {
e.preventDefault()
e.stopPropagation()
onDragStart(workItem)
}}
onDragOver={(e) => {
e.preventDefault()
e.stopPropagation()
onDragOver()
}}
onDragLeave={onDragLeave}
onDrop={(e) => {
e.preventDefault()
e.stopPropagation()
onDrop()
}}
className={`glass-card p-5 transition-all duration-300 cursor-move ${isExpanded ? 'ring-2 ring-blue-500/50' : ''} ${workItem.failedAttempts >= 3 ? 'border-red-500/50' : ''}`}
>
{!isExpanded ? (
<>
<div className="flex items-start justify-between">
@ -61,7 +84,10 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
)}
</div>
<button
onClick={() => onUpdate(workItem.id, { status: 'queued' })}
onClick={(e) => {
e.stopPropagation()
onUpdate(workItem.id, { status: 'queued' })
}}
disabled={workItem.status === 'complete' || workItem.failedAttempts >= 3}
className="ml-4 bg-gradient-to-r from-blue-600 via-blue-500 to-cyan-500 hover:from-blue-700 hover:via-blue-600 hover:to-cyan-600 text-white rounded-full w-10 h-10 flex items-center justify-center disabled:opacity-30 disabled:cursor-not-allowed transition-all duration-200 shadow-lg shadow-blue-500/20 hover:shadow-blue-500/40 hover:scale-110 active:scale-95"
title="Queue for AI execution"
@ -77,7 +103,10 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
</div>
</>
) : (
<form onSubmit={handleSubmit} className="space-y-4 animate-fade-in">
<form onSubmit={(e) => {
e.preventDefault()
handleSubmit(e)
}} className="space-y-4 animate-fade-in">
<div>
<label className="block text-sm font-medium text-slate-300 mb-2">
Title

View File

@ -26,6 +26,8 @@ const saveWorkItems = (items: WorkItem[]): void => {
export const useWorkItems = () => {
const [workItems, setWorkItems] = useState<WorkItem[]>(() => loadWorkItems())
const [isExecuting, setIsExecuting] = useState(false)
const [draggedItem, setDraggedItem] = useState<WorkItem | null>(null)
const [dragOverColumn, setDragOverColumn] = useState<string | null>(null)
useEffect(() => {
saveWorkItems(workItems)
@ -115,6 +117,35 @@ export const useWorkItems = () => {
}
}, [isExecuting, workItems, updateWorkItem])
const moveWorkItem = useCallback(async (itemId: string, toStatus: 'queued' | 'active' | 'complete'): Promise<void> => {
setWorkItems((prev) =>
prev.map((item) => (item.id === itemId ? { ...item, status: toStatus } : item)),
)
}, [])
const handleDragStart = useCallback((item: WorkItem): void => {
setDraggedItem(item)
}, [])
const handleDragOver = useCallback((status: string): void => {
setDragOverColumn(status)
}, [])
const handleDragLeave = useCallback((): void => {
setDragOverColumn(null)
}, [])
const handleDrop = useCallback((status: string): void => {
if (draggedItem) {
const newStatus = status as 'queued' | 'active' | 'complete'
if (draggedItem.status !== newStatus) {
moveWorkItem(draggedItem.id, newStatus)
}
setDraggedItem(null)
setDragOverColumn(null)
}
}, [draggedItem, moveWorkItem])
return {
workItems,
addWorkItem,
@ -122,5 +153,11 @@ export const useWorkItems = () => {
deleteWorkItem,
executeNext,
isExecuting,
draggedItem,
dragOverColumn,
handleDragStart,
handleDragOver,
handleDragLeave,
handleDrop,
}
}