Skip to content

Commit

Permalink
Merge pull request #9 from Hasnainahmad04/feat/board
Browse files Browse the repository at this point in the history
feat: created basic ui for kanban board
  • Loading branch information
Hasnainahmad04 authored Aug 14, 2024
2 parents d43f689 + 4dfc5ef commit 1f73c56
Show file tree
Hide file tree
Showing 26 changed files with 7,171 additions and 4,592 deletions.
11 changes: 11 additions & 0 deletions app/actions/issue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import prisma from '@/prisma/client';

export const getIssueDetail = async (id: number) => {
const issue = await prisma.issue.findUnique({ where: { id } });
return issue;
};

export const getAllIssues = async () => {
const issues = await prisma.issue.findMany();
return issues;
};
6 changes: 0 additions & 6 deletions app/actions/task.ts

This file was deleted.

10 changes: 8 additions & 2 deletions app/dashboard/SideNav.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import type { LucideIcon } from 'lucide-react';
import { LayoutDashboard, ShieldAlertIcon } from 'lucide-react';
import { LayoutDashboard, ListTodoIcon, SquareKanbanIcon } from 'lucide-react';
import type { LinkProps } from 'next/link';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
Expand Down Expand Up @@ -33,7 +33,13 @@ const links: NavItem[] = [
{
title: 'Issues',
href: '/dashboard/issue/list',
icon: ShieldAlertIcon,
icon: ListTodoIcon,
variant: 'ghost',
},
{
title: 'Board',
href: '/dashboard/issue/board',
icon: SquareKanbanIcon,
variant: 'ghost',
},
];
Expand Down
8 changes: 6 additions & 2 deletions app/dashboard/issue/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { notFound } from 'next/navigation';

import { getIssueDetail } from '@/app/actions/task';
import { getIssueDetail } from '@/app/actions/issue';

import IssueDetail from './IssueDetail';

Expand All @@ -15,7 +15,11 @@ const ViewTaskPage = async ({ params }: Props) => {

if (!issue) return notFound();

return <IssueDetail issue={issue} />;
return (
<div className="p-8">
<IssueDetail issue={issue} />
</div>
);
};

export default ViewTaskPage;
85 changes: 85 additions & 0 deletions app/dashboard/issue/board/Board.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use client';

import type { DragEndEvent } from '@dnd-kit/core';
import type { Issue, Status } from '@prisma/client';
import React, { useState } from 'react';

import useUpdateIssue from '@/hooks/issue/useUpdateIssue';

import Column from './Column';
import { KanbanBoard, KanbanBoardContainer } from './Kanban';
import KanbanCard from './KanbanCard';
import KanbanItem from './KanbanItem';

type Props = {
data: Issue[];
};

const Board = ({ data }: React.PropsWithChildren<Props>) => {
const initialState: Record<Status, Issue[]> = {
TODO: [],
BACKLOG: [],
IN_PROGRESS: [],
DONE: [],
CANCELLED: [],
};
const { mutate: updateIssueStatus } = useUpdateIssue();
const [taskStages, setTaskStages] = useState({
...initialState,
...Object.groupBy(data, (item) => item.status),
});

const handleOnDragEnd = (event: DragEndEvent) => {
const status = event.over?.id as undefined | Status | null;
const taskId = event.active.id as string;
const issue = event.active.data.current?.issue as Issue;
const prevStatus = event.active.data.current?.prevStatus as Status;

if (status) {
const state = { ...taskStages };
const prevStatusState = [...state[prevStatus]];
const updatedPrevState = prevStatusState.filter(
(item) => item.id !== Number(taskId),
);
state[prevStatus] = updatedPrevState;
state[status].push({ ...issue, status });

setTaskStages(state);
updateIssueStatus(
{ id: Number(taskId), data: { ...issue, status } },

{
onSuccess: () => {
alert('Status Updated');
},
onError: () => {
setTaskStages(taskStages);
},
},
);
}
};

return (
<KanbanBoardContainer>
<KanbanBoard onDragEnd={handleOnDragEnd}>
{taskStages &&
Object.entries(taskStages).map(([status, issues]) => (
<Column key={status} id={status} title={status as Status}>
{issues.map((issue) => (
<KanbanItem
key={issue.id}
id={issue.id.toString()}
data={{ issue, prevStatus: status }}
>
<KanbanCard issue={issue} />
</KanbanItem>
))}
</Column>
))}
</KanbanBoard>
</KanbanBoardContainer>
);
};

export default Board;
59 changes: 59 additions & 0 deletions app/dashboard/issue/board/Column.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { UseDroppableArguments } from '@dnd-kit/core';
import { useDroppable } from '@dnd-kit/core';
import type { Status } from '@prisma/client';

import { cn } from '@/lib/utils';

type Props = {
id: string;
title: Status;
data?: UseDroppableArguments['data'];
};

const statusMetadata: Record<Status, { title: string; background: string }> = {
TODO: { background: 'bg-neutral-500', title: 'Todo' },
IN_PROGRESS: { background: 'bg-sky-500', title: 'In Progress' },
CANCELLED: { background: 'bg-red-500', title: 'Cancelled' },
DONE: { background: 'bg-green-500', title: 'Done' },
BACKLOG: { background: 'bg-yellow-500', title: 'Backlog' },
};

const Column = ({
id,
title,
data,
children,
}: React.PropsWithChildren<Props>) => {
const { isOver, setNodeRef, active } = useDroppable({ id, data });

return (
<div ref={setNodeRef} className="flex w-96 flex-col px-4">
<div
className={cn(
'flex w-full items-center justify-between rounded-lg px-3 py-2',
statusMetadata[title].background,
)}
>
<span
className="truncate text-xs font-bold uppercase text-white"
title={title}
>
{statusMetadata[title].title}
</span>
{/* {Boolean(count) && (
<span className="inline-flex size-6 items-center justify-center rounded-full bg-neutral-100 text-xs text-zinc-800">
{count}
</span>
)} */}
</div>
<div
style={{ overflowY: active ? 'unset' : 'auto' }}
className={`column-scrollbar flex h-screen flex-col gap-2 rounded-md border-2 border-dashed p-1 ${isOver ? 'border-gray-500' : 'border-transparent'}`}
>
{children}
</div>
</div>
);
};

export default Column;
46 changes: 46 additions & 0 deletions app/dashboard/issue/board/Kanban.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { DragEndEvent } from '@dnd-kit/core';
import {
DndContext,
MouseSensor,
TouchSensor,
useSensor,
useSensors,
} from '@dnd-kit/core';
import React from 'react';

export const KanbanBoardContainer = ({ children }: React.PropsWithChildren) => {
return (
<div className="flex h-[100vh-64px] w-full flex-col">
<div className="flex h-[90vh] w-full overflow-auto p-8">{children}</div>
</div>
);
};

type Props = {
onDragEnd: (event: DragEndEvent) => void;
};

export const KanbanBoard = ({
children,
onDragEnd,
}: React.PropsWithChildren<Props>) => {
const mouseSensor = useSensor(MouseSensor, {
activationConstraint: {
distance: 5,
},
});

const touchSensor = useSensor(TouchSensor, {
activationConstraint: {
distance: 5,
},
});

const sensors = useSensors(mouseSensor, touchSensor);

return (
<DndContext onDragEnd={onDragEnd} sensors={sensors}>
{children}
</DndContext>
);
};
13 changes: 13 additions & 0 deletions app/dashboard/issue/board/KanbanCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Issue } from '@prisma/client';

const KanbanCard = ({ issue }: { issue: Issue }) => {
return (
<div className="divide-y rounded-lg border border-neutral-200 bg-white">
<span className="block p-2 text-sm font-medium text-zinc-900">
{issue.title}
</span>
</div>
);
};

export default KanbanCard;
40 changes: 40 additions & 0 deletions app/dashboard/issue/board/KanbanItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type { UseDraggableArguments } from '@dnd-kit/core';
import { DragOverlay, useDraggable } from '@dnd-kit/core';

interface Props {
id: string;
data?: UseDraggableArguments['data'];
}

const KanbanItem = ({ children, id, data }: React.PropsWithChildren<Props>) => {
const { attributes, listeners, setNodeRef, active } = useDraggable({
id,
data,
});

return (
<div className="relative">
<div
ref={setNodeRef}
{...attributes}
{...listeners}
className="relative cursor-grab rounded-lg"
style={{ opacity: active && active.id !== id ? 0.5 : 1 }}
>
{active?.id === id && (
<DragOverlay zIndex={1000}>
<div
className="cursor-grabbing rounded-lg"
style={{ boxShadow: 'rgba(149, 157, 165, 0.2) 0px 8px 24px' }}
>
{children}
</div>
</DragOverlay>
)}
{children}
</div>
</div>
);
};

export default KanbanItem;
9 changes: 9 additions & 0 deletions app/dashboard/issue/board/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { getAllIssues } from '@/app/actions/issue';

import Board from './Board';

export default async function Page() {
const data = await getAllIssues();

return <Board data={data} />;
}
3 changes: 1 addition & 2 deletions app/dashboard/issue/edit/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { notFound } from 'next/navigation';
import React from 'react';

import { getIssueDetail } from '@/app/actions/task';
import { getIssueDetail } from '@/app/actions/issue';
import IssueForm from '@/components/IssueForm';

type Props = {
Expand Down
2 changes: 1 addition & 1 deletion app/dashboard/issue/list/data-table/ColumnHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
title: string;
}

const sortFields = ['title', 'createdAt'];
const sortFields = ['title', 'createdAt', 'status'];

export function DataTableColumnHeader({ column, title, className }: Props) {
const router = useRouter();
Expand Down
11 changes: 7 additions & 4 deletions app/dashboard/issue/list/data-table/DataTablePagination.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ChevronsLeftIcon,
ChevronsRightIcon,
} from 'lucide-react';
import { useRouter, useSearchParams } from 'next/navigation';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';

import { Button } from '@/components/ui/button';
import {
Expand All @@ -23,6 +23,7 @@ interface DataTablePaginationProps {

export function DataTablePagination({ metadata }: DataTablePaginationProps) {
const router = useRouter();
const currentPath = usePathname();
const searchParams = useSearchParams();

const updatePagination = ({
Expand All @@ -38,15 +39,17 @@ export function DataTablePagination({ metadata }: DataTablePaginationProps) {
newSearchParams.set('page', page.toString());
newSearchParams.set('limit', limit.toString());

router.push(`?${newSearchParams.toString()}`);
router.push(`${currentPath}?${newSearchParams.toString()}`, {
scroll: false,
});
};

const currentPage = Number(searchParams.get('page')) || 1;
const currentLimit = Number(searchParams.get('limit')) || INITIAL_LIMIT;
const pageCount = Math.ceil(metadata.total / currentLimit);

const start = INITIAL_LIMIT * (currentPage - 1);
const end = INITIAL_LIMIT * currentPage;
const start = currentLimit * (currentPage - 1);
const end = currentLimit * currentPage;

return (
<div className="my-2 flex flex-col justify-between px-2 md:flex-row md:items-center">
Expand Down
Loading

0 comments on commit 1f73c56

Please sign in to comment.