Kanban shenanigans

This commit is contained in:
2025-07-05 10:49:28 +02:00
parent 75623f37bc
commit babc1d95ba
4 changed files with 79 additions and 39 deletions

View File

@ -3,7 +3,7 @@
import { mockCards } from "@/data/mockCards" import { mockCards } from "@/data/mockCards"
import type { KanbanCardData } from "@/types/kanban" import type { KanbanCardData } from "@/types/kanban"
import { DndContext, DragEndEvent, closestCenter } from "@dnd-kit/core" import { DndContext, DragEndEvent, closestCenter } from "@dnd-kit/core"
import { useState } from "react" import { useEffect, useState } from "react"
import { KanbanColumn } from "./KanbanColumn" import { KanbanColumn } from "./KanbanColumn"
export type Status = "PENDING" | "ACCEPTED" | "IN_PROGRESS" | "DONE" | "REJECTED" export type Status = "PENDING" | "ACCEPTED" | "IN_PROGRESS" | "DONE" | "REJECTED"
@ -13,6 +13,11 @@ const columns: Status[] = ["PENDING", "ACCEPTED", "IN_PROGRESS", "DONE", "REJECT
export function KanbanBoard() { export function KanbanBoard() {
const [cards, setCards] = useState<Record<Status, KanbanCardData[]>>(mockCards) const [cards, setCards] = useState<Record<Status, KanbanCardData[]>>(mockCards)
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
function findColumnByCardId(cardId: string): Status | null { function findColumnByCardId(cardId: string): Status | null {
for (const status of columns) { for (const status of columns) {
if (cards[status].some((c) => c.id === cardId)) return status if (cards[status].some((c) => c.id === cardId)) return status
@ -41,12 +46,18 @@ export function KanbanBoard() {
} }
return ( return (
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-5 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-5 gap-4"> {mounted ? (
{columns.map((status) => ( <DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
{columns.map((status) => (
<KanbanColumn key={status} status={status} items={cards[status]} />
))}
</DndContext>
) : (
columns.map((status) => (
<KanbanColumn key={status} status={status} items={cards[status]} /> <KanbanColumn key={status} status={status} items={cards[status]} />
))} ))
</div> )}
</DndContext> </div>
) )
} }

View File

@ -1,8 +1,8 @@
"use client" "use client"
import { Dialog, DialogContent } from "@/components/ui/dialog" import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
import type { KanbanCardData } from "@/types/kanban" import type { KanbanCardData } from "@/types/kanban"
import { VisuallyHidden } from "@radix-ui/react-visually-hidden"
export interface KanbanCardModalProps { export interface KanbanCardModalProps {
card: KanbanCardData | null card: KanbanCardData | null
onClose: () => void onClose: () => void
@ -12,6 +12,11 @@ export function KanbanCardModal({ card, onClose }: KanbanCardModalProps) {
return ( return (
<Dialog open={!!card} onOpenChange={onClose}> <Dialog open={!!card} onOpenChange={onClose}>
<DialogContent> <DialogContent>
<VisuallyHidden>
<DialogTitle className="text-lg font-semibold mb-2">
{card?.title ?? "Card Details"}
</DialogTitle>
</VisuallyHidden>
{card && ( {card && (
<div className="space-y-4"> <div className="space-y-4">
<h3 className="text-xl font-semibold">{card.title}</h3> <h3 className="text-xl font-semibold">{card.title}</h3>

View File

@ -1,43 +1,71 @@
// src/components/kanban/KanbanCardPreview.tsx
"use client" "use client"
import type { KanbanCardData } from "@/types/kanban" import { cn } from "@/lib/utils"
import { KanbanCardData } from "@/types/kanban"
import { useDraggable } from "@dnd-kit/core" import { useDraggable } from "@dnd-kit/core"
import { PointerEvent, useRef } from "react"
interface KanbanCardPreviewProps extends KanbanCardData { interface KanbanCardPreviewProps {
card: KanbanCardData
onClick: () => void onClick: () => void
} }
export function KanbanCardPreview({ export function KanbanCardPreview({ card, onClick }: KanbanCardPreviewProps) {
id, const {
title, attributes,
labels = [], listeners = {},
todos = [], setNodeRef,
onClick, transform,
}: KanbanCardPreviewProps) { isDragging,
const { setNodeRef, attributes, listeners, transform, isDragging } = useDraggable({ } = useDraggable({
id, id: card.id,
data: { card },
}) })
const done = todos.filter((t) => t.done).length const dragThreshold = 5
const total = todos.length const pointerStart = useRef<{ x: number; y: number } | null>(null)
const handlePointerDown = (e: PointerEvent) => {
pointerStart.current = { x: e.clientX, y: e.clientY }
listeners.onPointerDown?.(e)
}
const handlePointerUp = (e: PointerEvent) => {
if (!pointerStart.current) return
const dx = Math.abs(e.clientX - pointerStart.current.x)
const dy = Math.abs(e.clientY - pointerStart.current.y)
const isDrag = dx > dragThreshold || dy > dragThreshold
if (!isDrag) {
onClick()
}
pointerStart.current = null
}
const todoCount = card.todos?.length ?? 0
const doneCount = card.todos?.filter((t) => t.done).length ?? 0
return ( return (
<div <div
ref={setNodeRef} ref={setNodeRef}
{...listeners}
{...attributes} {...attributes}
onClick={onClick} {...listeners}
className={`rounded-md bg-white dark:bg-zinc-900 border border-border p-3 shadow-sm cursor-pointer hover:ring-2 hover:ring-primary/40 transition ${isDragging ? "opacity-50" : "" onPointerDown={handlePointerDown}
}`} onPointerUp={handlePointerUp}
style={{ style={{
transform: transform transform: transform
? `translate(${transform.x}px, ${transform.y}px)` ? `translate3d(${transform.x}px, ${transform.y}px, 0)`
: undefined, : undefined,
opacity: isDragging ? 0.5 : 1,
}} }}
className={cn(
"rounded-md bg-card p-3 shadow transition-all cursor-pointer select-none",
isDragging && "ring-2 ring-ring"
)}
> >
<div className="flex gap-1 mb-2 flex-wrap"> <div className="flex gap-1 mb-1 flex-wrap">
{labels.map((label) => ( {card.labels?.map((label) => (
<span <div
key={label.id} key={label.id}
className="w-3 h-3 rounded-full" className="w-3 h-3 rounded-full"
style={{ backgroundColor: label.color }} style={{ backgroundColor: label.color }}
@ -45,14 +73,10 @@ export function KanbanCardPreview({
/> />
))} ))}
</div> </div>
<h3 className="font-medium text-sm mb-1">{card.title}</h3>
<div className="font-medium text-sm mb-1">{title}</div> <div className="text-xs text-muted-foreground">
{doneCount}/{todoCount} ToDos
{total > 0 && ( </div>
<div className="text-xs text-muted-foreground">
{done} / {total} tasks done
</div>
)}
</div> </div>
) )
} }

View File

@ -27,7 +27,7 @@ export function KanbanColumn({ status, items }: KanbanColumnProps) {
{items.map((item) => ( {items.map((item) => (
<KanbanCardPreview <KanbanCardPreview
key={item.id} key={item.id}
{...item} card={item}
onClick={() => setActiveCard(item)} onClick={() => setActiveCard(item)}
/> />
))} ))}