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">
 | 
			
		||||
        {columns.map((status) => (
 | 
			
		||||
    <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]} />
 | 
			
		||||
          ))}
 | 
			
		||||
        </DndContext>
 | 
			
		||||
      ) : (
 | 
			
		||||
        columns.map((status) => (
 | 
			
		||||
          <KanbanColumn key={status} status={status} items={cards[status]} />
 | 
			
		||||
        ))}
 | 
			
		||||
      </div>
 | 
			
		||||
    </DndContext>
 | 
			
		||||
        ))
 | 
			
		||||
      )}
 | 
			
		||||
    </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 && (
 | 
			
		||||
        <div className="text-xs text-muted-foreground">
 | 
			
		||||
          {done} / {total} tasks done
 | 
			
		||||
        </div>
 | 
			
		||||
      )}
 | 
			
		||||
      <h3 className="font-medium text-sm mb-1">{card.title}</h3>
 | 
			
		||||
      <div className="text-xs text-muted-foreground">
 | 
			
		||||
        {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