diff --git a/.eslintrc.json b/.eslintrc.json
index dda6b92..77fdbee 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,74 +1,29 @@
{
- // Configuration for JavaScript files
+ "plugins": ["import"],
"extends": [
- "airbnb-base",
- "next/core-web-vitals", // Needed to avoid warning in next.js build: 'The Next.js plugin was not detected in your ESLint configuration'
- "plugin:prettier/recommended"
+ "next/core-web-vitals",
+ "plugin:tailwindcss/recommended",
+ "prettier"
],
"rules": {
- "prettier/prettier": [
+ "no-undef": "off",
+ "import/order": [
"error",
{
- "singleQuote": true,
- "endOfLine": "auto"
+ "groups": [
+ "builtin",
+ "external",
+ "internal",
+ "parent",
+ "sibling",
+ "index"
+ ],
+ "newlines-between": "always",
+ "alphabetize": {
+ "order": "asc",
+ "caseInsensitive": true
+ }
}
- ] // Avoid conflict rule between Prettier and Airbnb Eslint
- },
- "overrides": [
- // Configuration for TypeScript files
- {
- "files": ["**/*.ts", "**/*.tsx", "**/*.mts"],
- "plugins": [
- "@typescript-eslint",
- "unused-imports",
- "tailwindcss",
- "simple-import-sort"
- ],
- "extends": [
- "plugin:tailwindcss/recommended",
- "airbnb",
- "airbnb-typescript",
- "next/core-web-vitals",
- "plugin:prettier/recommended"
- ],
- "parser": "@typescript-eslint/parser",
- "parserOptions": {
- "project": "./tsconfig.json"
- },
- "rules": {
- "@typescript-eslint/lines-between-class-members": "off",
- "@typescript-eslint/no-throw-literal": "off",
- "prettier/prettier": [
- "error",
- {
- "singleQuote": true,
- "endOfLine": "auto"
- }
- ], // Avoid conflict rule between Prettier and Airbnb Eslint
- "import/extensions": "off", // Avoid missing file extension errors, TypeScript already provides a similar feature
- "react/function-component-definition": "off", // Disable Airbnb's specific function type
- "react/destructuring-assignment": "off", // Vscode doesn't support automatically destructuring, it's a pain to add a new variable
- "react/require-default-props": "off", // Allow non-defined react props as undefined
- "react/jsx-props-no-spreading": "off", // _app.tsx uses spread operator and also, react-hook-form
- "@typescript-eslint/comma-dangle": "off", // Avoid conflict rule between Eslint and Prettier
- "@typescript-eslint/consistent-type-imports": "error", // Ensure import type is used when it's necessary
- "no-restricted-syntax": [
- "error",
- "ForInStatement",
- "LabeledStatement",
- "WithStatement"
- ], // Overrides Airbnb configuration and enable no-restricted-syntax
- "import/prefer-default-export": "off", // Named export is easier to refactor automatically
- "simple-import-sort/imports": "error", // Import configuration for eslint-plugin-simple-import-sort
- "simple-import-sort/exports": "error", // Export configuration for eslint-plugin-simple-import-sort
- "import/order": "off", // Avoid conflict rule between eslint-plugin-import and eslint-plugin-simple-import-sort
- "@typescript-eslint/no-unused-vars": "off",
- "unused-imports/no-unused-imports": "error",
- "unused-imports/no-unused-vars": [
- "error",
- { "argsIgnorePattern": "^_" }
- ]
- }
- }
- ]
+ ]
+ }
}
diff --git a/app/actions/issue.ts b/app/actions/issue.ts
deleted file mode 100644
index b138f8b..0000000
--- a/app/actions/issue.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-'use server';
-
-import prisma from '@/prisma/client';
-
-export const getIssueDetail = async (id: number) => {
- const issue = await prisma.issue.findUnique({
- where: { id },
- include: { assets: true },
- });
- return issue;
-};
-
-export const getAllIssues = async () => {
- const issues = await prisma.issue.findMany({
- include: { assets: true },
- });
- return issues;
-};
diff --git a/app/api/issue/[id]/route.ts b/app/api/issue/[id]/route.ts
deleted file mode 100644
index 4df5ec4..0000000
--- a/app/api/issue/[id]/route.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { NextRequest } from 'next/server';
-import { NextResponse } from 'next/server';
-
-import { createIssueSchema } from '@/lib/validators';
-import prisma from '@/prisma/client';
-
-export const PATCH = async (
- req: NextRequest,
- { params }: { params: { id: string } },
-) => {
- const body = await req.json();
- const validation = createIssueSchema.safeParse(body);
- if (!validation.success) {
- return NextResponse.json(validation.error.flatten(), { status: 400 });
- }
- // eslint-disable-next-line unused-imports/no-unused-vars
- const { assets, ...issue } = validation.data;
- const updatedIssue = await prisma.issue.update({
- where: { id: Number(params.id) },
- data: issue,
- });
-
- return NextResponse.json(updatedIssue, { status: 200 });
-};
-
-export const DELETE = async (
- _req: NextRequest,
- { params }: { params: { id: string } },
-) => {
- try {
- await prisma.issue.delete({
- where: { id: Number(params.id) },
- });
- return NextResponse.json(null, { status: 200 });
- } catch (error) {
- return NextResponse.json(
- { message: 'Internal server error' },
- { status: 500 },
- );
- }
-};
diff --git a/app/api/issue/route.ts b/app/api/issue/route.ts
deleted file mode 100644
index 50b8a0e..0000000
--- a/app/api/issue/route.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import type { NextRequest } from 'next/server';
-import { NextResponse } from 'next/server';
-
-import { createIssueSchema, searchParamsSchema } from '@/lib/validators';
-import prisma from '@/prisma/client';
-
-export const dynamic = 'force-dynamic';
-
-export const POST = async (req: NextRequest) => {
- const body = await req.json();
- const validation = createIssueSchema.safeParse(body);
- if (!validation.success) {
- return NextResponse.json(validation.error.flatten(), { status: 400 });
- }
- const { assets, ...issue } = validation.data;
- const newIssue = await prisma.issue.create({
- data: {
- ...issue,
- assets: { createMany: { data: assets } },
- },
- });
-
- return NextResponse.json(newIssue, { status: 201 });
-};
-
-export const GET = async (req: NextRequest) => {
- const { searchParams } = new URL(req.url);
- const { data, error } = searchParamsSchema.safeParse(
- Object.fromEntries(searchParams),
- );
- if (error) {
- return NextResponse.json({ error: error.flatten() }, { status: 422 });
- }
-
- const { page = 1, limit = 10, q, orderBy, sort } = data;
-
- const [total, issues] = await prisma.$transaction([
- prisma.issue.count({
- where: {
- OR: q
- ? [{ title: { contains: q } }, { description: { contains: q } }]
- : undefined,
- },
- }),
- prisma.issue.findMany({
- include: { assets: true },
- skip: (page - 1) * limit,
- take: limit,
- where: {
- OR: q
- ? [{ title: { contains: q } }, { description: { contains: q } }]
- : undefined,
- },
- orderBy: orderBy && sort ? { [orderBy]: sort } : { createdAt: 'desc' },
- }),
- ]);
- return NextResponse.json(
- { data: issues, metadata: { page, limit, total } },
- { status: 200 },
- );
-};
diff --git a/app/dashboard/error.tsx b/app/dashboard/error.tsx
new file mode 100644
index 0000000..c5e01e3
--- /dev/null
+++ b/app/dashboard/error.tsx
@@ -0,0 +1,40 @@
+'use client';
+
+import Image from 'next/image';
+import { useEffect } from 'react';
+
+import { Button } from '@/components/ui/button';
+
+export default function Error({
+ error,
+ reset,
+}: {
+ error: Error & { digest?: string };
+ reset: () => void;
+}) {
+ useEffect(() => {
+ // Log the error to an error reporting service
+ console.error(error);
+ }, [error]);
+
+ return (
+
+
+ Something went wrong!
+
+ We encountered an unexpected error. Please try refreshing the page or
+ come back later. If the problem persists, contact support for
+ assistance.
+
+ reset()} className="mt-6">
+ Try again
+
+
+ );
+}
diff --git a/app/dashboard/issue/[id]/IssueDetail.tsx b/app/dashboard/issue/[id]/IssueDetail.tsx
index 134a8c4..6403242 100644
--- a/app/dashboard/issue/[id]/IssueDetail.tsx
+++ b/app/dashboard/issue/[id]/IssueDetail.tsx
@@ -15,7 +15,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
-import useDeleteIssue from '@/hooks/issue/useDeleteIssue';
+import { deleteIssue } from '@/lib/actions/issue';
import { cn, formatDate } from '@/lib/utils';
import type { Issue } from '@/types';
@@ -26,9 +26,21 @@ type Props = {
const IssueDetail = ({ issue }: Props) => {
const [open, setOpen] = useState(false);
const [selectedImage, setSelectedImage] = useState(0);
- const { mutate: deleteIssue, isPending } = useDeleteIssue();
+ const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
+ const handleDelete = async () => {
+ setIsLoading(true);
+ try {
+ await deleteIssue(issue.id);
+ router.replace('/dashboard/issue/list');
+ } catch (error) {
+ console.log(error);
+ }
+
+ setIsLoading(false);
+ };
+
return (
@@ -69,15 +81,8 @@ const IssueDetail = ({ issue }: Props) => {
onOpenChange={setOpen}
open={open}
issue={issue}
- loading={isPending}
- onDelete={() => {
- deleteIssue(issue.id, {
- onSuccess: () => {
- setOpen(false);
- router.replace('/dashboard/issue/list');
- },
- });
- }}
+ loading={isLoading}
+ onDelete={handleDelete}
/>
diff --git a/app/dashboard/issue/[id]/page.tsx b/app/dashboard/issue/[id]/page.tsx
index 233e205..d85310d 100644
--- a/app/dashboard/issue/[id]/page.tsx
+++ b/app/dashboard/issue/[id]/page.tsx
@@ -1,6 +1,6 @@
import { notFound } from 'next/navigation';
-import { getIssueDetail } from '@/app/actions/issue';
+import { getIssueDetail } from '@/lib/actions/issue';
import IssueDetail from './IssueDetail';
diff --git a/app/dashboard/issue/board/Board.tsx b/app/dashboard/issue/board/Board.tsx
index 1a832d0..ddca77e 100644
--- a/app/dashboard/issue/board/Board.tsx
+++ b/app/dashboard/issue/board/Board.tsx
@@ -4,9 +4,8 @@ import type { DragEndEvent } from '@dnd-kit/core';
import type { Status } from '@prisma/client';
import React, { useState } from 'react';
-import useUpdateIssue from '@/hooks/issue/useUpdateIssue';
import { groupBy } from '@/lib/utils';
-import type { Issue } from '@/types';
+import type { IssueList } from '@/types';
import Column from './Column';
import { KanbanBoard, KanbanBoardContainer } from './Kanban';
@@ -14,27 +13,26 @@ import KanbanCard from './KanbanCard';
import KanbanItem from './KanbanItem';
type Props = {
- data: Issue[];
+ data: IssueList[];
};
const Board = ({ data }: React.PropsWithChildren
) => {
- const initialState: Record = {
+ const initialState: Record = {
TODO: [],
BACKLOG: [],
IN_PROGRESS: [],
DONE: [],
CANCELLED: [],
};
- const { mutate: updateIssueStatus } = useUpdateIssue();
const [taskStages, setTaskStages] = useState({
...initialState,
- ...groupBy(data, (item) => item.status),
+ ...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 issue = event.active.data.current?.issue as IssueList;
const prevStatus = event.active.data.current?.prevStatus as Status;
if (status) {
@@ -47,18 +45,6 @@ const Board = ({ data }: React.PropsWithChildren) => {
state[status].push({ ...issue, status });
setTaskStages(state);
- updateIssueStatus(
- { id: Number(taskId), data: { ...issue, status } },
-
- {
- onSuccess: () => {
- alert('Status Updated');
- },
- onError: () => {
- setTaskStages(taskStages);
- },
- },
- );
}
};
diff --git a/app/dashboard/issue/board/KanbanCard.tsx b/app/dashboard/issue/board/KanbanCard.tsx
index 4c03703..dfb69a0 100644
--- a/app/dashboard/issue/board/KanbanCard.tsx
+++ b/app/dashboard/issue/board/KanbanCard.tsx
@@ -1,6 +1,6 @@
-import type { Issue } from '@/types';
+import type { IssueList } from '@/types';
-const KanbanCard = ({ issue }: { issue: Issue }) => {
+const KanbanCard = ({ issue }: { issue: IssueList }) => {
return (
diff --git a/app/dashboard/issue/board/page.tsx b/app/dashboard/issue/board/page.tsx
index 2085317..425fa0b 100644
--- a/app/dashboard/issue/board/page.tsx
+++ b/app/dashboard/issue/board/page.tsx
@@ -1,11 +1,11 @@
-import { getAllIssues } from '@/app/actions/issue';
+import { getBoardIssues } from '@/lib/actions/issue';
import Board from './Board';
export const dynamic = 'force-dynamic';
export default async function Page() {
- const data = await getAllIssues();
+ const issues = await getBoardIssues();
- return ;
+ return ;
}
diff --git a/app/dashboard/issue/edit/[id]/page.tsx b/app/dashboard/issue/edit/[id]/page.tsx
deleted file mode 100644
index e1b62a0..0000000
--- a/app/dashboard/issue/edit/[id]/page.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { notFound } from 'next/navigation';
-
-import { getIssueDetail } from '@/app/actions/issue';
-import IssueForm from '@/components/IssueForm';
-
-type Props = {
- params: { id: string };
-};
-
-const EditIssueRoute = async ({ params }: Props) => {
- const task = await getIssueDetail(Number(params.id));
- if (!task) return notFound();
-
- return (
-
-
Edit Issue
-
-
- );
-};
-
-export default EditIssueRoute;
diff --git a/app/dashboard/issue/list/data-table/DataTable.tsx b/app/dashboard/issue/list/data-table/DataTable.tsx
index c4a3f6a..c4b018a 100644
--- a/app/dashboard/issue/list/data-table/DataTable.tsx
+++ b/app/dashboard/issue/list/data-table/DataTable.tsx
@@ -59,7 +59,7 @@ export function DataTable({
params.delete('q');
}
router.replace(`${currentPath}?${params.toString()}`);
- }, 500);
+ }, 200);
return (
<>
diff --git a/app/dashboard/issue/list/data-table/DataTablePagination.tsx b/app/dashboard/issue/list/data-table/DataTablePagination.tsx
index 024059d..805fdb2 100644
--- a/app/dashboard/issue/list/data-table/DataTablePagination.tsx
+++ b/app/dashboard/issue/list/data-table/DataTablePagination.tsx
@@ -5,6 +5,7 @@ import {
ChevronsRightIcon,
} from 'lucide-react';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
+import React from 'react';
import { Button } from '@/components/ui/button';
import {
@@ -21,6 +22,29 @@ interface DataTablePaginationProps {
metadata: MetaData;
}
+const PaginationButton = ({
+ onClick,
+ disabled,
+ title,
+ children,
+}: {
+ onClick: () => void;
+ disabled: boolean;
+ title: string;
+} & React.PropsWithChildren) => {
+ return (
+
+ {title}
+ {children}
+
+ );
+};
+
export function DataTablePagination({ metadata }: DataTablePaginationProps) {
const router = useRouter();
const currentPath = usePathname();
@@ -70,7 +94,7 @@ export function DataTablePagination({ metadata }: DataTablePaginationProps) {
}}
>
-
+
{[10, 20, 30, 40, 50].map((pageSize) => (
@@ -85,48 +109,40 @@ export function DataTablePagination({ metadata }: DataTablePaginationProps) {
Page {currentPage} of {pageCount}
-
updatePagination({ page: 1, limit: currentLimit })}
+ updatePagination({ page: 1, limit: currentLimit })}
+ title="Go to first page"
>
- Go to first page
-
-
+
updatePagination({ page: currentPage - 1, limit: currentLimit })
}
disabled={currentPage === 1}
>
- Go to previous page
-
-
+
updatePagination({ page: currentPage + 1, limit: currentLimit })
}
disabled={currentPage === pageCount}
+ title="Go to next page"
>
- Go to next page
-
-
+
updatePagination({ page: pageCount, limit: currentLimit })
}
disabled={currentPage === pageCount}
>
- Go to last page
-
+
diff --git a/app/dashboard/issue/list/data-table/columns.tsx b/app/dashboard/issue/list/data-table/columns.tsx
index 8a61ede..0c10b20 100644
--- a/app/dashboard/issue/list/data-table/columns.tsx
+++ b/app/dashboard/issue/list/data-table/columns.tsx
@@ -1,34 +1,18 @@
'use client';
-/* eslint-disable react-hooks/rules-of-hooks */
-
import type { ColumnDef } from '@tanstack/react-table';
-import { MoreHorizontal } from 'lucide-react';
import Link from 'next/link';
-import { useRouter } from 'next/navigation';
-import { useState } from 'react';
-import DeleteDialog from '@/components/DeleteDialog';
-import { AlertDialogTrigger } from '@/components/ui/alert-dialog';
-import { Button } from '@/components/ui/button';
-import {
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuLabel,
- DropdownMenuSeparator,
- DropdownMenuTrigger,
-} from '@/components/ui/dropdown-menu';
-import useDeleteIssue from '@/hooks/issue/useDeleteIssue';
-import { formatDate } from '@/lib/utils';
-import type { Issue } from '@/types';
+import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
+import { formatDate, getInitials } from '@/lib/utils';
+import type { IssueList } from '@/types';
import { DataTableColumnHeader } from './ColumnHeader';
import Priority from './Priority';
import Status from './Status';
import Title from './Title';
-export const columns: ColumnDef[] = [
+export const columns: ColumnDef[] = [
{
accessorKey: 'id',
header: ({ column }) => (
@@ -90,47 +74,23 @@ export const columns: ColumnDef[] = [
enableSorting: true,
},
{
- id: 'actions',
- cell: ({ row }) => {
- const issue = row.original;
- const { mutateAsync: deleteIssue, isPending } = useDeleteIssue();
- const router = useRouter();
- const [open, setOpen] = useState(false);
-
+ accessorKey: 'assignee.image',
+ header: ({ column }) => (
+
+ ),
+ cell({ row }) {
return (
-
-
-
- Open menu
-
-
-
-
- Actions
-
- Edit
-
-
-
- Delete}
- onOpenChange={setOpen}
- open={open}
- issue={issue}
- loading={isPending}
- onDelete={() => {
- deleteIssue(issue.id, {
- onSuccess: () => {
- setOpen(false);
- router.refresh();
- },
- });
- }}
- />
-
-
-
+
+ {row.original.assignee.image ? (
+
+ ) : (
+
+ {getInitials(row.original.assignee.name || '')}
+
+ )}
+
);
},
+ enableSorting: false,
},
];
diff --git a/app/dashboard/issue/list/page.tsx b/app/dashboard/issue/list/page.tsx
index 1522cbb..1b1ad77 100644
--- a/app/dashboard/issue/list/page.tsx
+++ b/app/dashboard/issue/list/page.tsx
@@ -1,23 +1,33 @@
+import { Suspense } from 'react';
+
+import { getAllIssues } from '@/lib/actions/issue';
import { INITIAL_LIMIT } from '@/lib/constants';
-import { getIssues } from '@/services/issue';
-import type { QueryParams } from '@/types';
+import type { SearchFilters } from '@/types';
import { columns } from './data-table/columns';
import { DataTable } from './data-table/DataTable';
+import Loading from './loading';
export const dynamic = 'force-dynamic';
+export const revalidate = 0;
const IssuesRoute = async ({
searchParams: { limit = INITIAL_LIMIT, page = 1, orderBy, sort, q },
}: {
- searchParams: QueryParams;
+ searchParams: SearchFilters;
}) => {
- const issues = await getIssues({ page, limit, orderBy, sort, q });
+ const issues = await getAllIssues({ page, limit, orderBy, sort, q });
+
+ if (!issues) return;
+
+ console.log(JSON.stringify(issues, null, 4), 'list');
return (
-
-
-
+ }>
+
+
+
+
);
};
diff --git a/app/dashboard/issue/list/skeletons/index.tsx b/app/dashboard/issue/list/skeletons/index.tsx
index 291cee0..c60c49e 100644
--- a/app/dashboard/issue/list/skeletons/index.tsx
+++ b/app/dashboard/issue/list/skeletons/index.tsx
@@ -32,7 +32,7 @@ const TableRowSkeleton = () => {
-
+
);
@@ -48,6 +48,7 @@ export const TableSkeleton = () => {
Status
Priority
Created At
+ Assignee
diff --git a/app/layout.tsx b/app/layout.tsx
index 14a192f..9c67a96 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -2,6 +2,7 @@ import './globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
+import { SessionProvider } from 'next-auth/react';
import NextTopLoader from 'nextjs-toploader';
import { Toaster } from 'react-hot-toast';
@@ -26,7 +27,9 @@ export default function RootLayout({
>
- {children}
+
+ {children}
+