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() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen text-white p-6">
|
<div className="min-h-screen bg-slate-900 flex items-center justify-center p-4">
|
||||||
<div className="max-w-7xl mx-auto">
|
<Calculator />
|
||||||
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,10 @@ const Board = () => {
|
|||||||
deleteWorkItem,
|
deleteWorkItem,
|
||||||
executeNext,
|
executeNext,
|
||||||
isExecuting,
|
isExecuting,
|
||||||
|
dragOverColumn,
|
||||||
|
handleDragOver,
|
||||||
|
handleDragLeave,
|
||||||
|
handleDrop,
|
||||||
} = useWorkItems()
|
} = useWorkItems()
|
||||||
|
|
||||||
const queuedItems = workItems.filter((item) => item.status === 'queued')
|
const queuedItems = workItems.filter((item) => item.status === 'queued')
|
||||||
@ -32,6 +36,10 @@ const Board = () => {
|
|||||||
onDelete={deleteWorkItem}
|
onDelete={deleteWorkItem}
|
||||||
onAdd={addWorkItem}
|
onAdd={addWorkItem}
|
||||||
color="blue"
|
color="blue"
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
isDragOver={dragOverColumn === 'queued'}
|
||||||
/>
|
/>
|
||||||
<Column
|
<Column
|
||||||
title="Active"
|
title="Active"
|
||||||
@ -41,6 +49,10 @@ const Board = () => {
|
|||||||
onDelete={deleteWorkItem}
|
onDelete={deleteWorkItem}
|
||||||
onAdd={addWorkItem}
|
onAdd={addWorkItem}
|
||||||
color="purple"
|
color="purple"
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
isDragOver={dragOverColumn === 'active'}
|
||||||
/>
|
/>
|
||||||
<Column
|
<Column
|
||||||
title="Complete"
|
title="Complete"
|
||||||
@ -50,6 +62,10 @@ const Board = () => {
|
|||||||
onDelete={deleteWorkItem}
|
onDelete={deleteWorkItem}
|
||||||
onAdd={addWorkItem}
|
onAdd={addWorkItem}
|
||||||
color="green"
|
color="green"
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragLeave={handleDragLeave}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
isDragOver={dragOverColumn === 'complete'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -10,6 +10,10 @@ interface ColumnProps {
|
|||||||
onDelete: (id: string) => void
|
onDelete: (id: string) => void
|
||||||
onAdd: (data: WorkItemFormData) => void
|
onAdd: (data: WorkItemFormData) => void
|
||||||
color: 'blue' | 'purple' | 'green'
|
color: 'blue' | 'purple' | 'green'
|
||||||
|
onDragOver: (status: string) => void
|
||||||
|
onDragLeave: () => void
|
||||||
|
onDrop: (status: string) => void
|
||||||
|
isDragOver: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WorkItem {
|
interface WorkItem {
|
||||||
@ -24,7 +28,7 @@ interface WorkItem {
|
|||||||
lastAttemptedAt: number | null
|
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 [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [formData, setFormData] = useState<WorkItemFormData>({
|
const [formData, setFormData] = useState<WorkItemFormData>({
|
||||||
title: '',
|
title: '',
|
||||||
@ -69,7 +73,20 @@ const Column = ({ title, status, items, onUpdate, onDelete, onAdd, color }: Colu
|
|||||||
const styles = colorStyles[color]
|
const styles = colorStyles[color]
|
||||||
|
|
||||||
return (
|
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 justify-between mb-6 animate-slide-in">
|
||||||
<div className="flex items-center gap-3">
|
<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`}>
|
<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}
|
workItem={item}
|
||||||
onUpdate={onUpdate}
|
onUpdate={onUpdate}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
|
onDragStart={() => {}}
|
||||||
|
onDragOver={() => {}}
|
||||||
|
onDragLeave={() => {}}
|
||||||
|
onDrop={() => {}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{status === 'active' && items.some(i => i.status === 'active') && (
|
{status === 'active' && items.some(i => i.status === 'active') && (
|
||||||
|
|||||||
@ -5,9 +5,13 @@ interface Props {
|
|||||||
workItem: WorkItem
|
workItem: WorkItem
|
||||||
onUpdate: (id: string, updates: Partial<WorkItem>) => void
|
onUpdate: (id: string, updates: Partial<WorkItem>) => void
|
||||||
onDelete: (id: string) => 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 [isExpanded, setIsExpanded] = useState(false)
|
||||||
const [formData, setFormData] = useState<WorkItemFormData>({
|
const [formData, setFormData] = useState<WorkItemFormData>({
|
||||||
title: workItem.title,
|
title: workItem.title,
|
||||||
@ -37,7 +41,26 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 ? (
|
{!isExpanded ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
@ -61,7 +84,10 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => onUpdate(workItem.id, { status: 'queued' })}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onUpdate(workItem.id, { status: 'queued' })
|
||||||
|
}}
|
||||||
disabled={workItem.status === 'complete' || workItem.failedAttempts >= 3}
|
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"
|
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"
|
title="Queue for AI execution"
|
||||||
@ -77,7 +103,10 @@ const WorkItemComponent = ({ workItem, onUpdate, onDelete }: Props) => {
|
|||||||
</div>
|
</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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-slate-300 mb-2">
|
<label className="block text-sm font-medium text-slate-300 mb-2">
|
||||||
Title
|
Title
|
||||||
|
|||||||
@ -26,6 +26,8 @@ const saveWorkItems = (items: WorkItem[]): void => {
|
|||||||
export const useWorkItems = () => {
|
export const useWorkItems = () => {
|
||||||
const [workItems, setWorkItems] = useState<WorkItem[]>(() => loadWorkItems())
|
const [workItems, setWorkItems] = useState<WorkItem[]>(() => loadWorkItems())
|
||||||
const [isExecuting, setIsExecuting] = useState(false)
|
const [isExecuting, setIsExecuting] = useState(false)
|
||||||
|
const [draggedItem, setDraggedItem] = useState<WorkItem | null>(null)
|
||||||
|
const [dragOverColumn, setDragOverColumn] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
saveWorkItems(workItems)
|
saveWorkItems(workItems)
|
||||||
@ -115,6 +117,35 @@ export const useWorkItems = () => {
|
|||||||
}
|
}
|
||||||
}, [isExecuting, workItems, updateWorkItem])
|
}, [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 {
|
return {
|
||||||
workItems,
|
workItems,
|
||||||
addWorkItem,
|
addWorkItem,
|
||||||
@ -122,5 +153,11 @@ export const useWorkItems = () => {
|
|||||||
deleteWorkItem,
|
deleteWorkItem,
|
||||||
executeNext,
|
executeNext,
|
||||||
isExecuting,
|
isExecuting,
|
||||||
|
draggedItem,
|
||||||
|
dragOverColumn,
|
||||||
|
handleDragStart,
|
||||||
|
handleDragOver,
|
||||||
|
handleDragLeave,
|
||||||
|
handleDrop,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user