135 lines
4.0 KiB
TypeScript
135 lines
4.0 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { cn } from '@/lib/utils'
|
|
import { TaskCard } from './task-card'
|
|
import { Plus } from 'lucide-react'
|
|
import type { VikunjaTask, VikunjaBucket } from '@/lib/vikunja-client'
|
|
|
|
interface KanbanColumnProps {
|
|
bucket: VikunjaBucket
|
|
tasks: VikunjaTask[]
|
|
onEditTask?: (task: VikunjaTask) => void
|
|
onToggleDone?: (task: VikunjaTask) => void
|
|
onDragStart?: (e: React.DragEvent, task: VikunjaTask) => void
|
|
onDrop?: (bucketId: number) => void
|
|
onQuickAdd?: (title: string, bucketId: number) => void
|
|
}
|
|
|
|
export function KanbanColumn({
|
|
bucket,
|
|
tasks,
|
|
onEditTask,
|
|
onToggleDone,
|
|
onDragStart,
|
|
onDrop,
|
|
onQuickAdd,
|
|
}: KanbanColumnProps) {
|
|
const [isDragOver, setIsDragOver] = useState(false)
|
|
const [isAdding, setIsAdding] = useState(false)
|
|
const [quickTitle, setQuickTitle] = useState('')
|
|
|
|
function handleDragOver(e: React.DragEvent) {
|
|
e.preventDefault()
|
|
setIsDragOver(true)
|
|
}
|
|
|
|
function handleDragLeave() {
|
|
setIsDragOver(false)
|
|
}
|
|
|
|
function handleDrop(e: React.DragEvent) {
|
|
e.preventDefault()
|
|
setIsDragOver(false)
|
|
onDrop?.(bucket.id)
|
|
}
|
|
|
|
function handleQuickAdd() {
|
|
const title = quickTitle.trim()
|
|
if (title) {
|
|
onQuickAdd?.(title, bucket.id)
|
|
setQuickTitle('')
|
|
setIsAdding(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
'flex w-72 shrink-0 flex-col rounded-lg bg-muted/50',
|
|
isDragOver && 'ring-2 ring-primary/50'
|
|
)}
|
|
onDragOver={handleDragOver}
|
|
onDragLeave={handleDragLeave}
|
|
onDrop={handleDrop}
|
|
>
|
|
<div className="flex items-center justify-between px-3 py-2.5">
|
|
<div className="flex items-center gap-2">
|
|
<h3 className="text-sm font-semibold">{bucket.title}</h3>
|
|
<span className="flex h-5 min-w-[20px] items-center justify-center rounded-full bg-muted px-1.5 text-xs font-medium text-muted-foreground">
|
|
{tasks.length}
|
|
</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsAdding(true)}
|
|
className="rounded p-1 text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 space-y-2 overflow-y-auto px-2 pb-2" style={{ maxHeight: 'calc(100vh - 280px)' }}>
|
|
{isAdding && (
|
|
<div className="rounded-lg border bg-card p-2">
|
|
<input
|
|
type="text"
|
|
value={quickTitle}
|
|
onChange={(e) => setQuickTitle(e.target.value)}
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') handleQuickAdd()
|
|
if (e.key === 'Escape') { setIsAdding(false); setQuickTitle('') }
|
|
}}
|
|
placeholder="Task title..."
|
|
className="w-full rounded bg-transparent px-1 py-0.5 text-sm outline-none placeholder:text-muted-foreground"
|
|
autoFocus
|
|
/>
|
|
<div className="mt-1.5 flex justify-end gap-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => { setIsAdding(false); setQuickTitle('') }}
|
|
className="rounded px-2 py-1 text-xs text-muted-foreground hover:bg-accent"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleQuickAdd}
|
|
className="rounded bg-primary px-2 py-1 text-xs text-primary-foreground hover:bg-primary/90"
|
|
>
|
|
Add
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{tasks.map((task) => (
|
|
<TaskCard
|
|
key={task.id}
|
|
task={task}
|
|
onEdit={onEditTask}
|
|
onToggleDone={onToggleDone}
|
|
onDragStart={onDragStart}
|
|
/>
|
|
))}
|
|
|
|
{tasks.length === 0 && !isAdding && (
|
|
<div className="py-8 text-center text-xs text-muted-foreground">
|
|
Drop tasks here
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|