Add drag and drop for Kanban board cards
This commit is contained in:
parent
7044ffa751
commit
4e2ef81f97
23
src/App.tsx
23
src/App.tsx
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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') && (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user