@@ -245,36 +245,203 @@ const Navigation = () => {
245245 )
246246}
247247
248- // Kanban Preview Component (Visual illustration)
248+ /**
249+ * Card data structure for the Kanban preview.
250+ */
251+ interface KanbanCard {
252+ /** Unique identifier for the card */
253+ id : string
254+ /** Display name of the card */
255+ name : string
256+ /** Tailwind CSS classes for background and border colors */
257+ color : string
258+ }
259+
260+ /**
261+ * Column data structure for the Kanban preview.
262+ */
263+ interface KanbanColumn {
264+ /** Unique identifier for the column */
265+ id : string
266+ /** Display title of the column */
267+ title : string
268+ /** Cards contained in this column */
269+ cards : KanbanCard [ ]
270+ }
271+
272+ /**
273+ * Initial columns data for the Kanban preview.
274+ * Includes Backlog (3 cards), In Progress (1), Review (1), Done (2).
275+ */
276+ const INITIAL_COLUMNS : KanbanColumn [ ] = [
277+ {
278+ id : 'backlog' ,
279+ title : 'Backlog' ,
280+ cards : [
281+ {
282+ id : 'react' ,
283+ name : 'react' ,
284+ color : 'bg-blue-500/20 border-blue-500/30' ,
285+ } ,
286+ {
287+ id : 'vue' ,
288+ name : 'vue' ,
289+ color : 'bg-emerald-500/20 border-emerald-500/30' ,
290+ } ,
291+ {
292+ id : 'angular' ,
293+ name : 'angular' ,
294+ color : 'bg-orange-500/20 border-orange-500/30' ,
295+ } ,
296+ ] ,
297+ } ,
298+ {
299+ id : 'in-progress' ,
300+ title : 'In Progress' ,
301+ cards : [
302+ {
303+ id : 'nextjs' ,
304+ name : 'next.js' ,
305+ color : 'bg-purple-500/20 border-purple-500/30' ,
306+ } ,
307+ ] ,
308+ } ,
309+ {
310+ id : 'review' ,
311+ title : 'Review' ,
312+ cards : [
313+ {
314+ id : 'typescript' ,
315+ name : 'typescript' ,
316+ color : 'bg-amber-500/20 border-amber-500/30' ,
317+ } ,
318+ ] ,
319+ } ,
320+ {
321+ id : 'done' ,
322+ title : 'Done' ,
323+ cards : [
324+ {
325+ id : 'tailwind' ,
326+ name : 'tailwind' ,
327+ color : 'bg-cyan-500/20 border-cyan-500/30' ,
328+ } ,
329+ {
330+ id : 'prisma' ,
331+ name : 'prisma' ,
332+ color : 'bg-rose-500/20 border-rose-500/30' ,
333+ } ,
334+ ] ,
335+ } ,
336+ ]
337+
338+ /**
339+ * Interactive Kanban Preview Component with HTML5 Drag & Drop.
340+ * Demonstrates drag-and-drop functionality for the landing page hero section.
341+ */
249342const KanbanPreview = ( ) => {
250- const columns = [
251- {
252- title : 'Backlog' ,
253- cards : [
254- { name : 'react' , color : 'bg-blue-500/20 border-blue-500/30' } ,
255- { name : 'vue' , color : 'bg-emerald-500/20 border-emerald-500/30' } ,
256- ] ,
257- } ,
258- {
259- title : 'In Progress' ,
260- cards : [
261- { name : 'next.js' , color : 'bg-purple-500/20 border-purple-500/30' } ,
262- ] ,
263- } ,
264- {
265- title : 'Review' ,
266- cards : [
267- { name : 'typescript' , color : 'bg-amber-500/20 border-amber-500/30' } ,
268- ] ,
269- } ,
270- {
271- title : 'Done' ,
272- cards : [
273- { name : 'tailwind' , color : 'bg-cyan-500/20 border-cyan-500/30' } ,
274- { name : 'prisma' , color : 'bg-rose-500/20 border-rose-500/30' } ,
275- ] ,
276- } ,
277- ]
343+ const [ columns , setColumns ] = useState < KanbanColumn [ ] > ( INITIAL_COLUMNS )
344+ const [ draggedCard , setDraggedCard ] = useState < KanbanCard | null > ( null )
345+ const [ dragOverColumn , setDragOverColumn ] = useState < string | null > ( null )
346+
347+ /**
348+ * Handles the start of a drag operation.
349+ * @param e - The drag event
350+ * @param card - The card being dragged
351+ * @param sourceColumnId - The ID of the column the card is being dragged from
352+ */
353+ const handleDragStart = (
354+ e : React . DragEvent < HTMLDivElement > ,
355+ card : KanbanCard ,
356+ sourceColumnId : string ,
357+ ) => {
358+ setDraggedCard ( card )
359+ e . dataTransfer . setData ( 'cardId' , card . id )
360+ e . dataTransfer . setData ( 'sourceColumnId' , sourceColumnId )
361+ e . dataTransfer . effectAllowed = 'move'
362+
363+ // Apply drag styling after a brief delay to ensure visual feedback
364+ requestAnimationFrame ( ( ) => {
365+ const target = e . target as HTMLElement
366+ target . style . opacity = '0.5'
367+ } )
368+ }
369+
370+ /**
371+ * Handles the end of a drag operation.
372+ * @param e - The drag event
373+ */
374+ const handleDragEnd = ( e : React . DragEvent < HTMLDivElement > ) => {
375+ const target = e . target as HTMLElement
376+ target . style . opacity = '1'
377+ setDraggedCard ( null )
378+ setDragOverColumn ( null )
379+ }
380+
381+ /**
382+ * Handles drag over event for columns.
383+ * @param e - The drag event
384+ * @param columnId - The ID of the column being dragged over
385+ */
386+ const handleDragOver = (
387+ e : React . DragEvent < HTMLDivElement > ,
388+ columnId : string ,
389+ ) => {
390+ e . preventDefault ( )
391+ e . dataTransfer . dropEffect = 'move'
392+ setDragOverColumn ( columnId )
393+ }
394+
395+ /**
396+ * Handles drag leave event for columns.
397+ */
398+ const handleDragLeave = ( ) => {
399+ setDragOverColumn ( null )
400+ }
401+
402+ /**
403+ * Handles drop event for columns.
404+ * Moves the card from source column to target column.
405+ * @param e - The drag event
406+ * @param targetColumnId - The ID of the column where the card is dropped
407+ */
408+ const handleDrop = (
409+ e : React . DragEvent < HTMLDivElement > ,
410+ targetColumnId : string ,
411+ ) => {
412+ e . preventDefault ( )
413+ const cardId = e . dataTransfer . getData ( 'cardId' )
414+ const sourceColumnId = e . dataTransfer . getData ( 'sourceColumnId' )
415+
416+ if ( sourceColumnId === targetColumnId ) {
417+ setDragOverColumn ( null )
418+ return
419+ }
420+
421+ setColumns ( ( prevColumns ) => {
422+ const newColumns = prevColumns . map ( ( col ) => ( {
423+ ...col ,
424+ cards : [ ...col . cards ] ,
425+ } ) )
426+
427+ const sourceColumn = newColumns . find ( ( col ) => col . id === sourceColumnId )
428+ const targetColumn = newColumns . find ( ( col ) => col . id === targetColumnId )
429+
430+ if ( ! sourceColumn || ! targetColumn ) return prevColumns
431+
432+ const cardIndex = sourceColumn . cards . findIndex (
433+ ( card ) => card . id === cardId ,
434+ )
435+ if ( cardIndex === - 1 ) return prevColumns
436+
437+ const [ movedCard ] = sourceColumn . cards . splice ( cardIndex , 1 )
438+ targetColumn . cards . push ( movedCard )
439+
440+ return newColumns
441+ } )
442+
443+ setDragOverColumn ( null )
444+ }
278445
279446 return (
280447 < div className = "relative w-full max-w-4xl mx-auto mt-16" >
@@ -299,29 +466,46 @@ const KanbanPreview = () => {
299466 < div className = "p-6 grid grid-cols-4 gap-4" >
300467 { columns . map ( ( column , idx ) => (
301468 < div
302- key = { column . title }
303- className = "space-y-3"
469+ key = { column . id }
470+ data-testid = { `kanban-column-${ column . id } ` }
471+ className = { cn (
472+ 'p-3 rounded-lg border border-border/40 shadow-sm bg-muted/20' ,
473+ 'transition-all duration-200' ,
474+ dragOverColumn === column . id &&
475+ 'border-primary/50 bg-primary/5 shadow-md' ,
476+ ) }
477+ onDragOver = { ( e ) => handleDragOver ( e , column . id ) }
478+ onDragLeave = { handleDragLeave }
479+ onDrop = { ( e ) => handleDrop ( e , column . id ) }
304480 style = { {
305481 animation : `fadeInUp 0.5s ease-out ${ idx * 0.1 } s forwards` ,
306482 opacity : 0 ,
307483 } }
308484 >
309- < div className = "flex items-center justify-between" >
485+ < div className = "flex items-center justify-between mb-3 " >
310486 < span className = "text-xs font-medium text-muted-foreground" >
311487 { column . title }
312488 </ span >
313- < span className = "text-xs text-muted-foreground/60" >
489+ < span
490+ className = "text-xs text-muted-foreground/60"
491+ data-testid = { `column-count-${ column . id } ` }
492+ >
314493 { column . cards . length }
315494 </ span >
316495 </ div >
317- < div className = "space-y-2" >
496+ < div className = "space-y-2 min-h-[60px] " >
318497 { column . cards . map ( ( card , cardIdx ) => (
319498 < div
320- key = { card . name }
499+ key = { card . id }
500+ data-testid = { `kanban-card-${ card . id } ` }
501+ draggable
502+ onDragStart = { ( e ) => handleDragStart ( e , card , column . id ) }
503+ onDragEnd = { handleDragEnd }
321504 className = { cn (
322505 'p-3 rounded-lg border transition-all duration-200' ,
323- 'hover:scale-[1.02] hover:shadow-md cursor-grab' ,
506+ 'hover:scale-[1.02] hover:shadow-md cursor-grab active:cursor-grabbing ' ,
324507 card . color ,
508+ draggedCard ?. id === card . id && 'opacity-50' ,
325509 ) }
326510 style = { {
327511 animation : `fadeInUp 0.4s ease-out ${ idx * 0.1 + cardIdx * 0.05 + 0.2 } s forwards` ,
0 commit comments