Skip to content

Commit 72965b0

Browse files
committed
feat(components): KanbanBoard 5 colonne drag & drop nativo per stati candidatura con card job e hover effects
2 parents 98f9e59 + 6898c20 commit 72965b0

1 file changed

Lines changed: 128 additions & 0 deletions

File tree

web/app/components/KanbanBoard.tsx

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
'use client'
2+
3+
import { useState, useRef } from 'react'
4+
5+
export type KanbanCard = {
6+
id: string
7+
title: string
8+
company: string
9+
meta?: string
10+
tag?: string
11+
}
12+
13+
export type KanbanColumn = {
14+
id: string
15+
label: string
16+
color?: string
17+
cards: KanbanCard[]
18+
}
19+
20+
export const DEFAULT_KANBAN_COLUMNS: KanbanColumn[] = [
21+
{ id: 'saved', label: 'Salvata', color: 'var(--color-dim)', cards: [] },
22+
{ id: 'applied', label: 'Inviata', color: 'var(--color-blue)', cards: [] },
23+
{ id: 'interview', label: 'Colloquio', color: 'var(--color-yellow)', cards: [] },
24+
{ id: 'offer', label: 'Offerta', color: 'var(--color-green)', cards: [] },
25+
{ id: 'rejected', label: 'Rifiutata', color: 'var(--color-red)', cards: [] },
26+
]
27+
28+
type KanbanBoardProps = {
29+
columns?: KanbanColumn[]
30+
onChange?: (cols: KanbanColumn[]) => void
31+
className?: string
32+
}
33+
34+
function Card({ card, onDragStart }: { card: KanbanCard; onDragStart: () => void }) {
35+
const [hover, setHover] = useState(false)
36+
return (
37+
<div
38+
draggable
39+
onDragStart={onDragStart}
40+
onMouseEnter={() => setHover(true)}
41+
onMouseLeave={() => setHover(false)}
42+
className="rounded-lg px-3 py-2.5 cursor-grab active:cursor-grabbing select-none transition-all"
43+
style={{
44+
background: hover ? 'var(--color-deep)' : 'var(--color-card)',
45+
border: `1px solid var(--color-border)`,
46+
transform: hover ? 'translateY(-1px)' : 'none',
47+
boxShadow: hover ? '0 4px 12px rgba(0,0,0,0.3)' : 'none',
48+
}}>
49+
<p className="text-[11px] font-semibold truncate" style={{ color: 'var(--color-bright)' }}>{card.title}</p>
50+
<p className="text-[10px] truncate" style={{ color: 'var(--color-muted)' }}>{card.company}</p>
51+
{card.meta && <p className="text-[9px] font-mono mt-0.5 truncate" style={{ color: 'var(--color-dim)' }}>{card.meta}</p>}
52+
{card.tag && (
53+
<span className="inline-block mt-1.5 px-1.5 py-0.5 rounded text-[8px] font-semibold"
54+
style={{ background: 'var(--color-border)', color: 'var(--color-dim)' }}>{card.tag}</span>
55+
)}
56+
</div>
57+
)
58+
}
59+
60+
function Column({ col, onDrop, onDragStart, dragOver, setDragOver }:
61+
{ col: KanbanColumn; onDrop: () => void; onDragStart: (cardId: string) => void; dragOver: boolean; setDragOver: (v: boolean) => void }) {
62+
const color = col.color ?? 'var(--color-dim)'
63+
return (
64+
<div className="flex flex-col gap-2 min-w-[180px] flex-1"
65+
style={{ maxWidth: 240 }}
66+
onDragOver={e => { e.preventDefault(); setDragOver(true) }}
67+
onDragLeave={() => setDragOver(false)}
68+
onDrop={() => { setDragOver(false); onDrop() }}>
69+
70+
{/* Column header */}
71+
<div className="flex items-center justify-between px-2 py-1.5 rounded-lg"
72+
style={{ background: `${color}12`, border: `1px solid ${color}33` }}>
73+
<span className="text-[10px] font-bold uppercase tracking-wider" style={{ color }}>{col.label}</span>
74+
<span className="text-[10px] font-mono" style={{ color }}>{col.cards.length}</span>
75+
</div>
76+
77+
{/* Drop zone */}
78+
<div className="flex flex-col gap-2 rounded-lg flex-1 p-1.5 transition-colors min-h-[80px]"
79+
style={{ background: dragOver ? `${color}08` : 'transparent', border: dragOver ? `1px dashed ${color}44` : '1px dashed transparent' }}>
80+
{col.cards.map(card => (
81+
<Card key={card.id} card={card} onDragStart={() => onDragStart(card.id)} />
82+
))}
83+
{col.cards.length === 0 && !dragOver && (
84+
<p className="text-[9px] text-center py-4" style={{ color: 'var(--color-dim)' }}>Vuota</p>
85+
)}
86+
</div>
87+
</div>
88+
)
89+
}
90+
91+
export function KanbanBoard({ columns, onChange, className }: KanbanBoardProps) {
92+
const [cols, setCols] = useState<KanbanColumn[]>(columns ?? DEFAULT_KANBAN_COLUMNS)
93+
const [dragOver, setDragOver] = useState<string | null>(null)
94+
const dragRef = useRef<{ cardId: string; fromCol: string } | null>(null)
95+
96+
const handleDragStart = (cardId: string, fromCol: string) => { dragRef.current = { cardId, fromCol } }
97+
98+
const handleDrop = (toColId: string) => {
99+
if (!dragRef.current) return
100+
const { cardId, fromCol } = dragRef.current
101+
if (fromCol === toColId) return
102+
setCols(prev => {
103+
const next = prev.map(c => ({ ...c, cards: [...c.cards] }))
104+
const src = next.find(c => c.id === fromCol)
105+
const dst = next.find(c => c.id === toColId)
106+
if (!src || !dst) return prev
107+
const idx = src.cards.findIndex(c => c.id === cardId)
108+
if (idx === -1) return prev
109+
const [card] = src.cards.splice(idx, 1)
110+
dst.cards.push(card)
111+
onChange?.(next)
112+
return next
113+
})
114+
dragRef.current = null
115+
}
116+
117+
return (
118+
<div className={`flex gap-3 overflow-x-auto pb-2 ${className ?? ''}`}>
119+
{cols.map(col => (
120+
<Column key={col.id} col={col}
121+
dragOver={dragOver === col.id}
122+
setDragOver={v => setDragOver(v ? col.id : null)}
123+
onDragStart={cardId => handleDragStart(cardId, col.id)}
124+
onDrop={() => handleDrop(col.id)} />
125+
))}
126+
</div>
127+
)
128+
}

0 commit comments

Comments
 (0)