Kanban shenanigans
This commit is contained in:
@ -3,7 +3,7 @@
|
||||
import { mockCards } from "@/data/mockCards"
|
||||
import type { KanbanCardData } from "@/types/kanban"
|
||||
import { DndContext, DragEndEvent, closestCenter } from "@dnd-kit/core"
|
||||
import { useState } from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import { KanbanColumn } from "./KanbanColumn"
|
||||
|
||||
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() {
|
||||
const [cards, setCards] = useState<Record<Status, KanbanCardData[]>>(mockCards)
|
||||
|
||||
const [mounted, setMounted] = useState(false)
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
function findColumnByCardId(cardId: string): Status | null {
|
||||
for (const status of columns) {
|
||||
if (cards[status].some((c) => c.id === cardId)) return status
|
||||
@ -41,12 +46,18 @@ export function KanbanBoard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-5 gap-4">
|
||||
{mounted ? (
|
||||
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
{columns.map((status) => (
|
||||
<KanbanColumn key={status} status={status} items={cards[status]} />
|
||||
))}
|
||||
</div>
|
||||
</DndContext>
|
||||
) : (
|
||||
columns.map((status) => (
|
||||
<KanbanColumn key={status} status={status} items={cards[status]} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { Dialog, DialogContent } from "@/components/ui/dialog"
|
||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
|
||||
import type { KanbanCardData } from "@/types/kanban"
|
||||
|
||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden"
|
||||
export interface KanbanCardModalProps {
|
||||
card: KanbanCardData | null
|
||||
onClose: () => void
|
||||
@ -12,6 +12,11 @@ export function KanbanCardModal({ card, onClose }: KanbanCardModalProps) {
|
||||
return (
|
||||
<Dialog open={!!card} onOpenChange={onClose}>
|
||||
<DialogContent>
|
||||
<VisuallyHidden>
|
||||
<DialogTitle className="text-lg font-semibold mb-2">
|
||||
{card?.title ?? "Card Details"}
|
||||
</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
{card && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-semibold">{card.title}</h3>
|
||||
|
@ -1,43 +1,71 @@
|
||||
// src/components/kanban/KanbanCardPreview.tsx
|
||||
"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 { PointerEvent, useRef } from "react"
|
||||
|
||||
interface KanbanCardPreviewProps extends KanbanCardData {
|
||||
interface KanbanCardPreviewProps {
|
||||
card: KanbanCardData
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export function KanbanCardPreview({
|
||||
id,
|
||||
title,
|
||||
labels = [],
|
||||
todos = [],
|
||||
onClick,
|
||||
}: KanbanCardPreviewProps) {
|
||||
const { setNodeRef, attributes, listeners, transform, isDragging } = useDraggable({
|
||||
id,
|
||||
export function KanbanCardPreview({ card, onClick }: KanbanCardPreviewProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners = {},
|
||||
setNodeRef,
|
||||
transform,
|
||||
isDragging,
|
||||
} = useDraggable({
|
||||
id: card.id,
|
||||
data: { card },
|
||||
})
|
||||
|
||||
const done = todos.filter((t) => t.done).length
|
||||
const total = todos.length
|
||||
const dragThreshold = 5
|
||||
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 (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
{...listeners}
|
||||
{...attributes}
|
||||
onClick={onClick}
|
||||
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" : ""
|
||||
}`}
|
||||
{...listeners}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerUp={handlePointerUp}
|
||||
style={{
|
||||
transform: transform
|
||||
? `translate(${transform.x}px, ${transform.y}px)`
|
||||
? `translate3d(${transform.x}px, ${transform.y}px, 0)`
|
||||
: 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">
|
||||
{labels.map((label) => (
|
||||
<span
|
||||
<div className="flex gap-1 mb-1 flex-wrap">
|
||||
{card.labels?.map((label) => (
|
||||
<div
|
||||
key={label.id}
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: label.color }}
|
||||
@ -45,14 +73,10 @@ export function KanbanCardPreview({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="font-medium text-sm mb-1">{title}</div>
|
||||
|
||||
{total > 0 && (
|
||||
<h3 className="font-medium text-sm mb-1">{card.title}</h3>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{done} / {total} tasks done
|
||||
{doneCount}/{todoCount} ToDos
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ export function KanbanColumn({ status, items }: KanbanColumnProps) {
|
||||
{items.map((item) => (
|
||||
<KanbanCardPreview
|
||||
key={item.id}
|
||||
{...item}
|
||||
card={item}
|
||||
onClick={() => setActiveCard(item)}
|
||||
/>
|
||||
))}
|
||||
|
Reference in New Issue
Block a user