Kanban shenanigans
This commit is contained in:
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
Reference in New Issue
Block a user