-
-
Notifications
You must be signed in to change notification settings - Fork 58
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
added the whole kanban view with mock data
- Loading branch information
Showing
22 changed files
with
668 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
apps/web/src/components/kanban-board/column/column-dropzone.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
import type { Column } from "@/types/workspace"; | ||
import { useDroppable } from "@dnd-kit/core"; | ||
import { | ||
SortableContext, | ||
verticalListSortingStrategy, | ||
} from "@dnd-kit/sortable"; | ||
import TaskCard from "../task-card"; | ||
|
||
interface ColumnDropzoneProps { | ||
column: Column; | ||
} | ||
|
||
export function ColumnDropzone({ column }: ColumnDropzoneProps) { | ||
const { setNodeRef } = useDroppable({ | ||
id: column.id, | ||
data: { | ||
type: "column", | ||
column, | ||
}, | ||
}); | ||
|
||
return ( | ||
<div ref={setNodeRef} className="flex-1"> | ||
<SortableContext | ||
items={column.tasks} | ||
strategy={verticalListSortingStrategy} | ||
> | ||
<div className="flex flex-col gap-2"> | ||
{column.tasks.map((task) => ( | ||
<TaskCard key={task.id} task={task} /> | ||
))} | ||
</div> | ||
</SortableContext> | ||
</div> | ||
); | ||
} |
27 changes: 27 additions & 0 deletions
27
apps/web/src/components/kanban-board/column/column-header.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import type { Column } from "@/types/workspace"; | ||
import { MoreHorizontal } from "lucide-react"; | ||
|
||
interface ColumnHeaderProps { | ||
column: Column; | ||
} | ||
|
||
export function ColumnHeader({ column }: ColumnHeaderProps) { | ||
return ( | ||
<div className="flex items-center justify-between"> | ||
<div className="flex items-center gap-2"> | ||
<h3 className="font-medium text-zinc-900 dark:text-zinc-100"> | ||
{column.name} | ||
</h3> | ||
<span className="text-sm text-zinc-500 dark:text-zinc-500"> | ||
{column.tasks.length} | ||
</span> | ||
</div> | ||
<button | ||
type="button" | ||
className="p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors" | ||
> | ||
<MoreHorizontal className="w-4 h-4 text-zinc-500 dark:text-zinc-400" /> | ||
</button> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import type { Column as ColumnType } from "@/types/workspace"; | ||
import { ColumnDropzone } from "./column-dropzone"; | ||
import { ColumnHeader } from "./column-header"; | ||
|
||
interface ColumnProps { | ||
column: ColumnType; | ||
} | ||
|
||
function Column({ column }: ColumnProps) { | ||
return ( | ||
<div className="flex flex-col w-[calc(100vw-2rem)] md:w-80 shrink-0 bg-zinc-50/50 dark:bg-zinc-900/50 backdrop-blur-sm rounded-lg border border-zinc-200 dark:border-zinc-800/50 snap-center h-full shadow-sm"> | ||
<div className="p-3 border-b border-zinc-200 dark:border-zinc-800/50"> | ||
<ColumnHeader column={column} /> | ||
</div> | ||
<div className="p-3 overflow-y-auto flex-1"> | ||
<ColumnDropzone column={column} /> | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default Column; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import generateProject from "@/lib/workspace/generate-project"; | ||
import { | ||
DndContext, | ||
type DragEndEvent, | ||
DragOverlay, | ||
type DragStartEvent, | ||
type UniqueIdentifier, | ||
closestCorners, | ||
} from "@dnd-kit/core"; | ||
import { useState } from "react"; | ||
import Column from "./column"; | ||
import TaskCard from "./task-card"; | ||
|
||
function KanbanBoard() { | ||
const [project, setProject] = useState( | ||
generateProject({ | ||
projectId: "sample-1", | ||
workspaceId: "workspace-1", | ||
tasksPerColumn: 4, | ||
}), | ||
); | ||
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null); | ||
|
||
const handleDragStart = (event: DragStartEvent) => { | ||
setActiveId(event.active.id); | ||
}; | ||
|
||
// TODO: Simplify this function | ||
const handleDragEnd = (event: DragEndEvent) => { | ||
const { active, over } = event; | ||
|
||
if (!over) return; | ||
|
||
const activeId = active.id.toString(); | ||
const overId = over.id.toString(); | ||
|
||
const sourceColumn = project.columns.find((col) => | ||
col.tasks.some((task) => task.id === activeId), | ||
); | ||
|
||
const destinationColumn = project.columns.find((col) => { | ||
if (col.id === overId) return true; | ||
return col.tasks.some((task) => task.id === overId); | ||
}); | ||
|
||
if (!sourceColumn || !destinationColumn) return; | ||
|
||
setProject((currentProject) => { | ||
const updatedColumns = [...currentProject.columns]; | ||
|
||
const sourceColumnIndex = updatedColumns.findIndex( | ||
(col) => col.id === sourceColumn.id, | ||
); | ||
const destinationColumnIndex = updatedColumns.findIndex( | ||
(col) => col.id === destinationColumn.id, | ||
); | ||
|
||
const sourceTaskIndex = sourceColumn.tasks.findIndex( | ||
(task) => task.id === activeId, | ||
); | ||
const task = sourceColumn.tasks[sourceTaskIndex]; | ||
|
||
updatedColumns[sourceColumnIndex] = { | ||
...sourceColumn, | ||
tasks: sourceColumn.tasks.filter((t) => t.id !== activeId), | ||
}; | ||
|
||
if (sourceColumn.id === destinationColumn.id) { | ||
const destinationIndex = destinationColumn.tasks.findIndex( | ||
(task) => task.id === overId, | ||
); | ||
const newTasks = [...destinationColumn.tasks]; | ||
newTasks.splice(sourceTaskIndex, 1); | ||
newTasks.splice(destinationIndex, 0, task); | ||
|
||
updatedColumns[destinationColumnIndex] = { | ||
...destinationColumn, | ||
tasks: newTasks, | ||
}; | ||
} else { | ||
const updatedTask = { ...task, status: destinationColumn.id }; | ||
|
||
if (overId === destinationColumn.id) { | ||
updatedColumns[destinationColumnIndex] = { | ||
...destinationColumn, | ||
tasks: [...destinationColumn.tasks, updatedTask], | ||
}; | ||
} else { | ||
const destinationIndex = destinationColumn.tasks.findIndex( | ||
(task) => task.id === overId, | ||
); | ||
const newTasks = [...destinationColumn.tasks]; | ||
newTasks.splice(destinationIndex, 0, updatedTask); | ||
|
||
updatedColumns[destinationColumnIndex] = { | ||
...destinationColumn, | ||
tasks: newTasks, | ||
}; | ||
} | ||
} | ||
|
||
return { | ||
...currentProject, | ||
columns: updatedColumns, | ||
}; | ||
}); | ||
|
||
setActiveId(null); | ||
}; | ||
|
||
const activeTask = activeId | ||
? project.columns | ||
.flatMap((col) => col.tasks) | ||
.find((task) => task.id === activeId) | ||
: null; | ||
|
||
return ( | ||
<DndContext | ||
collisionDetection={closestCorners} | ||
onDragStart={handleDragStart} | ||
onDragEnd={handleDragEnd} | ||
> | ||
<div className="h-full flex flex-col"> | ||
<header className="mb-6 space-y-6 flex-shrink-0 px-4 md:px-0"> | ||
<div className="flex items-center justify-between"> | ||
<h1 className="text-2xl font-semibold text-zinc-900 dark:text-zinc-100"> | ||
{project.name} | ||
</h1> | ||
</div> | ||
</header> | ||
|
||
<div className="flex-1 relative min-h-0"> | ||
<div className="flex gap-6 overflow-x-auto pb-6 px-4 md:px-6 h-full snap-x snap-mandatory scrollbar-thin scrollbar-track-zinc-100 scrollbar-thumb-zinc-300 dark:scrollbar-track-zinc-900 dark:scrollbar-thumb-zinc-700"> | ||
{project.columns.map((column) => ( | ||
<Column key={column.id} column={column} /> | ||
))} | ||
</div> | ||
</div> | ||
</div> | ||
|
||
<DragOverlay> | ||
{activeTask ? ( | ||
<div className="transform rotate-3"> | ||
<TaskCard task={activeTask} /> | ||
</div> | ||
) : null} | ||
</DragOverlay> | ||
</DndContext> | ||
); | ||
} | ||
|
||
export default KanbanBoard; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import type { Task } from "@/types/workspace"; | ||
import { useSortable } from "@dnd-kit/sortable"; | ||
import { CSS } from "@dnd-kit/utilities"; | ||
import { format } from "date-fns"; | ||
import { Calendar, Flag } from "lucide-react"; | ||
|
||
interface TaskCardProps { | ||
task: Task; | ||
} | ||
|
||
function TaskCard({ task }: TaskCardProps) { | ||
const { | ||
attributes, | ||
listeners, | ||
setNodeRef, | ||
transform, | ||
transition, | ||
isDragging, | ||
} = useSortable({ id: task.id }); | ||
|
||
const style = { | ||
transform: CSS.Transform.toString(transform), | ||
transition, | ||
opacity: isDragging ? 0.5 : 1, | ||
}; | ||
|
||
const priorityColors = { | ||
low: "bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-500", | ||
medium: | ||
"bg-yellow-50 text-yellow-700 dark:bg-yellow-500/10 dark:text-yellow-500", | ||
high: "bg-orange-50 text-orange-700 dark:bg-orange-500/10 dark:text-orange-500", | ||
urgent: "bg-red-50 text-red-700 dark:bg-red-500/10 dark:text-red-500", | ||
}; | ||
|
||
return ( | ||
<div | ||
ref={setNodeRef} | ||
style={style} | ||
{...attributes} | ||
{...listeners} | ||
className="bg-white dark:bg-zinc-800/50 backdrop-blur-sm rounded-lg border border-zinc-200 dark:border-zinc-700/50 p-3 cursor-move hover:border-zinc-300 dark:hover:border-zinc-700 transition-colors shadow-sm" | ||
> | ||
<h3 className="font-medium text-zinc-900 dark:text-zinc-100 mb-1"> | ||
{task.title} | ||
</h3> | ||
|
||
<p className="text-sm text-zinc-600 dark:text-zinc-400 mb-3"> | ||
{task.description} | ||
</p> | ||
|
||
<div className="flex items-center justify-between"> | ||
<div className="flex items-center gap-2"> | ||
<span | ||
className={`text-xs px-2 py-1 rounded-full ${priorityColors[task.priority as keyof typeof priorityColors]}`} | ||
> | ||
<Flag className="w-3 h-3 inline-block mr-1" /> | ||
{task.priority} | ||
</span> | ||
</div> | ||
|
||
{task.dueDate && ( | ||
<div className="text-xs text-zinc-600 dark:text-zinc-400 flex items-center"> | ||
<Calendar className="w-3 h-3 mr-1" /> | ||
{format(new Date(task.dueDate), "MMM d")} | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
} | ||
|
||
export default TaskCard; |
Oops, something went wrong.