Beta Preview

Kanban Board

A kanban board with accessible drag and drop, styled with Tailwind CSS.

Open

7 tasks
UI Button Alignment Issue#101Buttons in the Settings menu are misaligned on smaller screens.Gilberto Miguel
Login Page Redesign#102Requesting a redesign of the login page to improve user experience.Maia Pettegree
Feature: Dark Mode#104Implement a dark mode option for improved accessibility and user preference.Sarah Lee
Missing User Profile Pictures#105Some user profile pictures are not displaying properly in the user dashboard.David Chen
Broken Link on Homepage#107The "Learn More" link on the homepage is leading to a 404 error.Alex Turner
Feature: Export to PDF#108Implement a feature to allow users to export their data to PDF format.Maia Pettegree
Mobile Responsiveness Issue#109The application is not rendering properly on certain mobile devices.Kevin Williams

In Progress

2 tasks
Database Connection Error#103Users are experiencing intermittent connection errors when accessing the database.Mike Johnson
Feature: Two-Factor Authentication#110Requesting the addition of two-factor authentication for improved security.Maia Pettegree

Closed

1 task
Performance Optimization#106Requesting performance optimization for the application to reduce load times.Sarah Lee
import {Button, DropIndicator, GridList, GridListItem, isTextDropItem, Text, useDragAndDrop} from 'react-aria-components';
import {ListData, useListData} from 'react-stately';
import React, {ReactNode} from 'react';

export default function KanbanBoard(): ReactNode { let list = useListData({ initialItems: tickets }); return ( <div className="grid grid-cols-[repeat(3,minmax(280px,1fr))] md:justify-center gap-4 -mx-8 px-8 py-8 box-border w-full overflow-auto relative snap-x snap-mandatory no-scrollbar"> <Column status="Open" list={list} itemClassName="selected:bg-green-100 selected:border-green-500 dark:selected:bg-green-900 dark:selected:border-green-700" /> <Column status="In Progress" list={list} itemClassName="selected:bg-blue-100 selected:border-blue-500 dark:selected:bg-blue-900 dark:selected:border-blue-700" /> <Column status="Closed" list={list} itemClassName="selected:bg-red-100 selected:border-red-500 dark:selected:bg-red-900 dark:selected:border-red-700" /> </div> ); } interface ColumnProps { list: ListData<typeof tickets[0]>, status: string, itemClassName?: string } function Column({list, status, itemClassName}: ColumnProps) { let items = list.items.filter(t => t.status === status); let {dragAndDropHooks} = useDragAndDrop({ // Provide drag data in a custom format as well as plain text. getItems(keys, items) { return items.map((item) => ({ 'issue-id': item.id, 'text/plain': item.title })); }, renderDropIndicator(target) { return ( <DropIndicator target={target} className="h-0 -my-1.5 -translate-y-[5px] -mx-2 invisible drop-target:visible"> <svg height={10} className="block w-full stroke-blue-500 fill-none forced-colors:stroke-[Highlight]"> <circle cx={5} cy={5} r={5 - 1} strokeWidth={2} /> <line x1={20} x2="100%" transform="translate(-10 0)" y1={5} y2={5} strokeWidth={2} /> <circle cx="100%" cy={5} r={5 - 1} transform="translate(-5 0)" strokeWidth={2} /> </svg> </DropIndicator> ); }, // Accept drops with the custom format. acceptedDragTypes: ['issue-id'], // Ensure items are always moved rather than copied. getDropOperation: () => 'move', // Handle drops between items from other lists. async onInsert(e) { let ids = await Promise.all( e.items.filter(isTextDropItem).map(item => item.getText('issue-id')) ); for (let id of ids) { list.update(id, {...list.getItem(id)!, status}); } if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, ids); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, ids); } }, // Handle drops on the collection when empty. async onRootDrop(e) { let ids = await Promise.all( e.items.filter(isTextDropItem).map(item => item.getText('issue-id')) ); for (let id of ids) { list.update(id, {...list.getItem(id)!, status}); } }, // Handle reordering items within the same list. onReorder(e) { if (e.target.dropPosition === 'before') { list.moveBefore(e.target.key, e.keys); } else if (e.target.dropPosition === 'after') { list.moveAfter(e.target.key, e.keys); } } }); return ( <section className="flex flex-col gap-2 snap-center"> <header> <h3 className="font-semibold text-zinc-800 dark:text-zinc-200 my-0">{status}</h3> <span className="text-sm text-zinc-700 dark:text-zinc-400">{items.length} {items.length === 1 ? 'task' : 'tasks'}</span> </header> <GridList items={items} aria-label={status} selectionMode="multiple" dragAndDropHooks={dragAndDropHooks} renderEmptyState={() => 'No tasks.'} className="h-[320px] p-2 md:p-4 overflow-y-auto overflow-x-hidden relative outline outline-0 bg-white/70 dark:bg-zinc-900/60 backdrop-blur-sm border border-black/10 dark:border-white/10 bg-clip-padding text-gray-700 dark:text-zinc-400 flex flex-col gap-3 rounded-xl shadow-xl drop-target:bg-blue-200 dark:drop-target:bg-blue-800/60 drop-target:outline-2 outline-blue-500 forced-colors:outline-[Highlight] -outline-offset-2 empty:items-center empty:justify-center"> {item => <Card item={item} className={itemClassName} />} </GridList> </section> ); } interface CardProps { id?: string, item: typeof tickets[0], className?: string } function Card({id, item, className}: CardProps) { return ( <GridListItem id={id} value={item} textValue={item.title} className={`group grid grid-cols-[1fr_auto] gap-1 p-2 rounded-lg border border-solid border-black/10 hover:border-black/20 dark:border-white/10 dark:hover:border-white/20 forced-colors:border-[ButtonBorder]! bg-white/80 dark:bg-zinc-900/70 bg-clip-padding hover:shadow-md selected:shadow-md dragging:opacity-50 transition text-slate-700 dark:text-slate-200 cursor-default select-none outline outline-0 outline-offset-2 focus-visible:outline-2 outline-blue-500 forced-colors:outline-[Highlight] forced-colors:text-[ButtonText]! forced-colors:selected:bg-[Highlight]! forced-colors:selected:text-[HighlightText]! forced-color-adjust-none ${className}`}> <span className="font-bold truncate">{item.title}</span> <span className="text-sm justify-self-end">{item.id}</span> <Text slot="description" className="text-sm line-clamp-2 col-span-2 text-slate-500 dark:text-zinc-300 forced-colors:text-inherit!">{item.description}</Text> <span className="flex items-center gap-1"> <img src={item.avatar} alt="" className="h-4 w-4 rounded-full" /> <span className="text-sm">{item.assignee}</span> </span> <Button slot="drag" className="bg-transparent border-none text-gray-500 dark:text-zinc-300 text-base leading-none w-fit aspect-square p-0 justify-self-end outline outline-0 focus-visible:outline-2 outline-blue-500 forced-colors:outline-[Highlight] rounded-xs sr-only group-focus-visible:not-sr-only focus:not-sr-only forced-colors:group-selected:text-[HighlightText] forced-colors:group-selected:outline-[HighlightText]">≡</Button> </GridListItem> ); }

Components