From 11c45ad479e111814805422956bef1935a67b8b7 Mon Sep 17 00:00:00 2001 From: SwiichyCode Date: Tue, 19 Mar 2024 15:06:20 +0100 Subject: [PATCH 1/3] feat: prototype of Projects page --- src/app/(app)/projects/layout.tsx | 5 +++ src/app/(app)/projects/page.tsx | 20 ++++++++++ .../layouts/Sidebar/SidebarNavigation.tsx | 6 +++ src/config/constants/index.ts | 1 + src/config/lib/next-safe-action.ts | 38 ++++++++++++++++--- src/config/providers/ErrorBoundary.tsx | 2 +- .../projects/components/AccessTokenAlert.tsx | 18 +++++++++ .../projects/components/ProjectsList.tsx | 5 +++ .../PersonnalAccessTokenRecommandation.tsx | 5 +++ src/services/actions/get-user-repositories.ts | 15 ++++++++ src/services/github.service.ts | 8 ++++ 11 files changed, 116 insertions(+), 7 deletions(-) create mode 100644 src/app/(app)/projects/layout.tsx create mode 100644 src/app/(app)/projects/page.tsx create mode 100644 src/modules/projects/components/AccessTokenAlert.tsx create mode 100644 src/modules/projects/components/ProjectsList.tsx create mode 100644 src/modules/settings/components/PersonnalAccessTokenRecommandation.tsx create mode 100644 src/services/actions/get-user-repositories.ts diff --git a/src/app/(app)/projects/layout.tsx b/src/app/(app)/projects/layout.tsx new file mode 100644 index 0000000..49057e5 --- /dev/null +++ b/src/app/(app)/projects/layout.tsx @@ -0,0 +1,5 @@ +import React, { PropsWithChildren } from "react"; + +export default function ProjectsLayout({ children }: PropsWithChildren) { + return
{children}
; +} diff --git a/src/app/(app)/projects/page.tsx b/src/app/(app)/projects/page.tsx new file mode 100644 index 0000000..faf9be2 --- /dev/null +++ b/src/app/(app)/projects/page.tsx @@ -0,0 +1,20 @@ +import { AccessTokenAlert } from "@/modules/projects/components/AccessTokenAlert"; +import { getUserRepositoriesAction } from "@/services/actions/get-user-repositories"; + +export default async function ProjectsPage() { + const { data, serverError } = await getUserRepositoriesAction(); + + return ( + <> + {serverError && } +
+ {data?.map((repo) => ( +
+

{repo.name}

+

{repo.description}

+
+ ))} +
+ + ); +} diff --git a/src/components/layouts/Sidebar/SidebarNavigation.tsx b/src/components/layouts/Sidebar/SidebarNavigation.tsx index 9c6fc80..4d493cd 100644 --- a/src/components/layouts/Sidebar/SidebarNavigation.tsx +++ b/src/components/layouts/Sidebar/SidebarNavigation.tsx @@ -3,6 +3,7 @@ import { PersonIcon, RepoIcon, FileDirectoryIcon, + ProjectIcon, } from "@primer/octicons-react"; import { Separator } from "@/components/ui/separator"; import { SidebarNavigationLink } from "./SidebarNavigationLink"; @@ -31,6 +32,11 @@ const SidebarNavigationItems = [ href: URL.RESOURCES, icon: FileDirectoryIcon, }, + { + name: "Projects", + href: URL.PROJECTS, + icon: ProjectIcon, + }, { name: "Settings", href: URL.SETTINGS, diff --git a/src/config/constants/index.ts b/src/config/constants/index.ts index 8ff696c..537216f 100644 --- a/src/config/constants/index.ts +++ b/src/config/constants/index.ts @@ -2,6 +2,7 @@ export const URL = { HOME: "/", REPOSITORIES: "/repositories", RESOURCES: "/resources", + PROJECTS: "/projects", PROFILE: "/profile", SETTINGS: "/settings", LINKEDIN: "https://www.linkedin.com/in/swapnil-singh-1a1b3b1b3/", diff --git a/src/config/lib/next-safe-action.ts b/src/config/lib/next-safe-action.ts index 900eb17..4776cfa 100644 --- a/src/config/lib/next-safe-action.ts +++ b/src/config/lib/next-safe-action.ts @@ -1,5 +1,7 @@ import { getServerAuthSession } from "@/config/server/auth"; import { createSafeActionClient } from "next-safe-action"; +import { DEFAULT_SERVER_ERROR } from "next-safe-action"; +import userService from "@/services/user.service"; import type { Session } from "next-auth"; export const action = createSafeActionClient(); @@ -7,15 +9,12 @@ export const action = createSafeActionClient(); export class ActionError extends Error {} export const adminAction = createSafeActionClient({ - //@ts-expect-error - Return type is not correct handleReturnedServerError(e) { if (e instanceof ActionError) { return e.message; } - return { - serverError: "Something went wrong", - }; + return DEFAULT_SERVER_ERROR; }, async middleware() { @@ -32,23 +31,50 @@ export const adminAction = createSafeActionClient({ export const userAction = createSafeActionClient<{ session: Session; }>({ - //@ts-expect-error - Return type is not correct handleReturnedServerError(e) { if (e instanceof ActionError) { return e.message; } + return DEFAULT_SERVER_ERROR; + }, + + async middleware() { + const session = await getServerAuthSession(); + if (!session) throw new ActionError("Not logged in"); + return { - serverError: "Something went wrong", + session, }; }, +}); + +export const githubAction = createSafeActionClient<{ + session: Session; + userAccessToken: string; +}>({ + handleReturnedServerError(e) { + if (e instanceof ActionError) { + return e.message; + } + + return DEFAULT_SERVER_ERROR; + }, async middleware() { const session = await getServerAuthSession(); if (!session) throw new ActionError("Not logged in"); + const userAccessToken = await userService.getPersonalAccessToken({ + userId: session.user.id, + }); + + if (!userAccessToken) + throw new ActionError("User has no personal access token"); + return { session, + userAccessToken, }; }, }); diff --git a/src/config/providers/ErrorBoundary.tsx b/src/config/providers/ErrorBoundary.tsx index 5d18081..b2f805c 100644 --- a/src/config/providers/ErrorBoundary.tsx +++ b/src/config/providers/ErrorBoundary.tsx @@ -27,7 +27,7 @@ export class ErrorBoundary extends Component { public render() { if (this.state.hasError) { - return this.props.fallback || null; + return this.props.fallback ?? null; } return this.props.children; diff --git a/src/modules/projects/components/AccessTokenAlert.tsx b/src/modules/projects/components/AccessTokenAlert.tsx new file mode 100644 index 0000000..5533a37 --- /dev/null +++ b/src/modules/projects/components/AccessTokenAlert.tsx @@ -0,0 +1,18 @@ +import Link from "next/link"; +import { URL } from "@/config/constants"; + +export const AccessTokenAlert = () => { + return ( +
+

Access token required

+

+ You need to provide a GitHub access token to see your repositories. You + can add it in the settings page. +

+ + + Go to settings + +
+ ); +}; diff --git a/src/modules/projects/components/ProjectsList.tsx b/src/modules/projects/components/ProjectsList.tsx new file mode 100644 index 0000000..af23fec --- /dev/null +++ b/src/modules/projects/components/ProjectsList.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const ProjectsList = () => { + return
ProjectsList
; +}; diff --git a/src/modules/settings/components/PersonnalAccessTokenRecommandation.tsx b/src/modules/settings/components/PersonnalAccessTokenRecommandation.tsx new file mode 100644 index 0000000..b3ad2b2 --- /dev/null +++ b/src/modules/settings/components/PersonnalAccessTokenRecommandation.tsx @@ -0,0 +1,5 @@ +import React from "react"; + +export const PersonnalAccessTokenRecommandation = () => { + return
PersonnalAccessTokenRecommandation
; +}; diff --git a/src/services/actions/get-user-repositories.ts b/src/services/actions/get-user-repositories.ts new file mode 100644 index 0000000..ad4a109 --- /dev/null +++ b/src/services/actions/get-user-repositories.ts @@ -0,0 +1,15 @@ +"use server"; +import { githubAction } from "@/config/lib/next-safe-action"; +import { OctokitService } from "@/services/github.service"; +import * as z from "zod"; + +export const getUserRepositoriesAction = githubAction( + z.void(), + async (_, ctx) => { + const userAccessToken = ctx.userAccessToken; + const octokitService = new OctokitService(userAccessToken); + const response = await octokitService.getUserRepositories(); + + return response.data; + }, +); diff --git a/src/services/github.service.ts b/src/services/github.service.ts index 400c9d7..3c72211 100644 --- a/src/services/github.service.ts +++ b/src/services/github.service.ts @@ -72,6 +72,14 @@ export class OctokitService { } } + async getUserRepositories() { + return await this.octokit.request("GET /user/repos", { + sort: "updated", + page: 1, + per_page: 10, + }); + } + /** * Query to get the social accounts of a user. * @param {string} username - The username of the user. From e9ebe0dc600775fe1b2db125b3f6553ed343358d Mon Sep 17 00:00:00 2001 From: SwiichyCode Date: Wed, 20 Mar 2024 19:57:23 +0100 Subject: [PATCH 2/3] feat: implement part of project feature --- package.json | 2 + pnpm-lock.yaml | 59 ++++++ prisma/schema.prisma | 52 ++++++ src/app/(app)/projects/[id]/error.tsx | 5 + src/app/(app)/projects/[id]/layout.tsx | 9 + src/app/(app)/projects/[id]/page.tsx | 26 +++ src/app/(app)/projects/error.tsx | 5 + src/app/(app)/projects/layout.tsx | 4 +- src/app/(app)/projects/page.tsx | 24 ++- .../layouts/SharingFilter/_index.tsx | 2 +- src/components/ui/datatable.tsx | 101 +++++++++++ src/components/ui/dialog-wrapper.tsx | 2 - src/components/ui/popover.tsx | 31 ++++ src/components/ui/table.tsx | 124 +++++++++++++ src/config/constants/index.ts | 23 +++ src/config/providers/ErrorBoundary.tsx | 2 +- src/config/providers/TanstackProvider.tsx | 2 +- src/config/types/prisma.type.ts | 8 + src/jobs/examples.ts | 4 +- src/jobs/sync-repositories.ts | 2 +- src/middleware.ts | 2 +- .../ColumnAction/DeleteColumn.tsx | 49 +++++ .../ColumnHeader/ColumnAction/_index.tsx | 33 ++++ .../Column/ColumnHeader/ColumnColor.tsx | 23 +++ .../Column/ColumnHeader/ColumnCount.tsx | 13 ++ .../Column/ColumnHeader/ColumnDescription.tsx | 7 + .../Column/ColumnHeader/ColumnName.tsx | 9 + .../components/Column/ColumnHeader/_index.tsx | 27 +++ .../components/Column/ColumnsList.tsx | 5 + .../projects/components/Column/_index.tsx | 13 ++ .../components/PresentationalCard.tsx | 18 ++ .../projects/components/ProjectsList.tsx | 5 - .../components/Task/AddTaskButton.tsx | 16 ++ .../components/Task/AddTaskDialog.tsx | 17 ++ .../projects/components/Task/TasksList.tsx | 19 ++ .../components/_forms/add-project-form.tsx | 67 +++++++ .../components/_forms/add-project-schema.ts | 6 + .../components/_forms/add-task-form.tsx | 50 ++++++ .../components/_forms/add-task-schema.ts | 6 + .../components/_tables/projects-columns.tsx | 15 ++ .../components/_tables/projects-table.tsx | 19 ++ .../projects/context/columnContext.tsx | 30 ++++ .../projects/context/projectContext.tsx | 34 ++++ .../projects/hooks/use-fetch-project-page.tsx | 16 ++ .../hooks/use-fetch-projects-page.tsx | 12 ++ .../projects/stores/useAddTaskModal.ts | 11 ++ .../utils/generate-column-background-color.ts | 24 +++ .../utils/generate-column-border-color.ts | 22 +++ src/services/actions/add-project.ts | 27 +++ src/services/actions/add-task.ts | 33 ++++ src/services/actions/remove-column.ts | 22 +++ src/services/project.service.ts | 169 ++++++++++++++++++ src/services/repository.service.ts | 2 +- src/services/types/project.type.ts | 33 ++++ tailwind.config.ts | 66 ------- 55 files changed, 1310 insertions(+), 97 deletions(-) create mode 100644 src/app/(app)/projects/[id]/error.tsx create mode 100644 src/app/(app)/projects/[id]/layout.tsx create mode 100644 src/app/(app)/projects/[id]/page.tsx create mode 100644 src/app/(app)/projects/error.tsx create mode 100644 src/components/ui/datatable.tsx create mode 100644 src/components/ui/popover.tsx create mode 100644 src/components/ui/table.tsx create mode 100644 src/modules/projects/components/Column/ColumnHeader/ColumnAction/DeleteColumn.tsx create mode 100644 src/modules/projects/components/Column/ColumnHeader/ColumnAction/_index.tsx create mode 100644 src/modules/projects/components/Column/ColumnHeader/ColumnColor.tsx create mode 100644 src/modules/projects/components/Column/ColumnHeader/ColumnCount.tsx create mode 100644 src/modules/projects/components/Column/ColumnHeader/ColumnDescription.tsx create mode 100644 src/modules/projects/components/Column/ColumnHeader/ColumnName.tsx create mode 100644 src/modules/projects/components/Column/ColumnHeader/_index.tsx create mode 100644 src/modules/projects/components/Column/ColumnsList.tsx create mode 100644 src/modules/projects/components/Column/_index.tsx create mode 100644 src/modules/projects/components/PresentationalCard.tsx delete mode 100644 src/modules/projects/components/ProjectsList.tsx create mode 100644 src/modules/projects/components/Task/AddTaskButton.tsx create mode 100644 src/modules/projects/components/Task/AddTaskDialog.tsx create mode 100644 src/modules/projects/components/Task/TasksList.tsx create mode 100644 src/modules/projects/components/_forms/add-project-form.tsx create mode 100644 src/modules/projects/components/_forms/add-project-schema.ts create mode 100644 src/modules/projects/components/_forms/add-task-form.tsx create mode 100644 src/modules/projects/components/_forms/add-task-schema.ts create mode 100644 src/modules/projects/components/_tables/projects-columns.tsx create mode 100644 src/modules/projects/components/_tables/projects-table.tsx create mode 100644 src/modules/projects/context/columnContext.tsx create mode 100644 src/modules/projects/context/projectContext.tsx create mode 100644 src/modules/projects/hooks/use-fetch-project-page.tsx create mode 100644 src/modules/projects/hooks/use-fetch-projects-page.tsx create mode 100644 src/modules/projects/stores/useAddTaskModal.ts create mode 100644 src/modules/projects/utils/generate-column-background-color.ts create mode 100644 src/modules/projects/utils/generate-column-border-color.ts create mode 100644 src/services/actions/add-project.ts create mode 100644 src/services/actions/add-task.ts create mode 100644 src/services/actions/remove-column.ts create mode 100644 src/services/project.service.ts create mode 100644 src/services/types/project.type.ts diff --git a/package.json b/package.json index d7b8f8b..f3ea77a 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-slot": "^1.0.2", "@radix-ui/react-toast": "^1.1.5", @@ -46,6 +47,7 @@ "@tanstack/react-query": "^5.24.1", "@tanstack/react-query-devtools": "^5.24.8", "@tanstack/react-query-next-experimental": "^5.24.1", + "@tanstack/react-table": "^8.14.0", "@tiptap/extension-heading": "^2.2.4", "@tiptap/react": "^2.2.4", "@tiptap/starter-kit": "^2.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5caa788..99e6333 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -47,6 +47,9 @@ dependencies: '@radix-ui/react-label': specifier: ^2.0.2 version: 2.0.2(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-popover': + specifier: ^1.0.7 + version: 1.0.7(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-select': specifier: ^2.0.0 version: 2.0.0(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) @@ -74,6 +77,9 @@ dependencies: '@tanstack/react-query-next-experimental': specifier: ^5.24.1 version: 5.25.0(@tanstack/react-query@5.25.0)(next@14.1.3)(react@18.2.0) + '@tanstack/react-table': + specifier: ^8.14.0 + version: 8.14.0(react-dom@18.2.0)(react@18.2.0) '@tiptap/extension-heading': specifier: ^2.2.4 version: 2.2.4(@tiptap/core@2.2.4) @@ -2032,6 +2038,41 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-popover@1.0.7(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.64)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.64)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.64)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.64)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.64)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.64)(react@18.2.0) + '@types/react': 18.2.64 + '@types/react-dom': 18.2.21 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.64)(react@18.2.0) + dev: false + /@radix-ui/react-popper@1.1.3(@types/react-dom@18.2.21)(@types/react@18.2.64)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w==} peerDependencies: @@ -2542,6 +2583,18 @@ packages: react: 18.2.0 dev: false + /@tanstack/react-table@8.14.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-l9iwO99oz/azy5RT5VkVRsHHuy7o//fiXgLxzl3fad8qf7Bj+9ihsfmE6Q+BNjH4wHbxZkahwxtb3ngGq9FQxA==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + '@tanstack/table-core': 8.14.0 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@tanstack/react-virtual@3.1.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-YCzcbF/Ws/uZ0q3Z6fagH+JVhx4JLvbSflgldMgLsuvB8aXjZLLb3HvrEVxY480F9wFlBiXlvQxOyXb5ENPrNA==} peerDependencies: @@ -2553,6 +2606,11 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@tanstack/table-core@8.14.0: + resolution: {integrity: sha512-wDhpKJahGHWhmRt4RxtV3pES63CoeadljGWS/xeS9OJr1HBl2NB+OO44ht3sxDH5j5TRDAbQzC0NvSlsUfn7lQ==} + engines: {node: '>=12'} + dev: false + /@tanstack/virtual-core@3.1.3: resolution: {integrity: sha512-Y5B4EYyv1j9V8LzeAoOVeTg0LI7Fo5InYKgAjkY1Pu9GjtUwX/EKxNcU7ng3sKr99WEf+bPTcktAeybyMOYo+g==} dev: false @@ -7900,6 +7958,7 @@ packages: /react-remove-scroll-bar@2.3.5(@types/react@18.2.64)(react@18.2.0): resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==} engines: {node: '>=10'} + deprecated: please update to the following version as this contains a bug (https://github.com/theKashey/react-remove-scroll-bar/issues/57) peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 03b7c4f..145de0a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -78,6 +78,9 @@ model User { likes Like[] Comment Comment[] Resource Resource[] + Project Project[] + Column Column[] + Task Task[] githubProfile GithubProfile? } @@ -133,6 +136,44 @@ model Repository { @@index([url]) } +model Project { + id String @id @default(uuid()) + name String + description String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy User @relation(fields: [createdById], references: [id]) + createdById String + columns Column[] +} + +model Column { + id String @id @default(uuid()) + name String + description String + color ColumnColor + maxSize Int @default(99) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy User @relation(fields: [createdById], references: [id]) + createdById String + project Project @relation(fields: [projectId], references: [id]) + projectId String + tasks Task[] +} + +model Task { + id String @id @default(uuid()) + name String + description String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy User @relation(fields: [createdById], references: [id]) + createdById String + column Column @relation(fields: [columnId], references: [id]) + columnId String +} + model Resource { id Int @id @default(autoincrement()) url String @unique @@ -198,3 +239,14 @@ enum ResourceType { COURSE BOOK } + +enum ColumnColor { + GRAY + BLUE + GREEN + YELLOW + ORANGE + RED + PINK + PURPLE +} diff --git a/src/app/(app)/projects/[id]/error.tsx b/src/app/(app)/projects/[id]/error.tsx new file mode 100644 index 0000000..392541f --- /dev/null +++ b/src/app/(app)/projects/[id]/error.tsx @@ -0,0 +1,5 @@ +"use client"; + +export default function ProjectError() { + return
Error occured
; +} diff --git a/src/app/(app)/projects/[id]/layout.tsx b/src/app/(app)/projects/[id]/layout.tsx new file mode 100644 index 0000000..03e6e87 --- /dev/null +++ b/src/app/(app)/projects/[id]/layout.tsx @@ -0,0 +1,9 @@ +import type { PropsWithChildren } from "react"; + +export default function ProjectLayout({ children }: PropsWithChildren) { + return ( +
+ {children} +
+ ); +} diff --git a/src/app/(app)/projects/[id]/page.tsx b/src/app/(app)/projects/[id]/page.tsx new file mode 100644 index 0000000..d2efb4a --- /dev/null +++ b/src/app/(app)/projects/[id]/page.tsx @@ -0,0 +1,26 @@ +import { useFetchProjectPage } from "@/modules/projects/hooks/use-fetch-project-page"; +import { ColumnsList } from "@/modules/projects/components/Column/ColumnsList"; +import { Column } from "@/modules/projects/components/Column/_index"; +import { ProjectProvider } from "@/modules/projects/context/projectContext"; +import { ColumnProvider } from "@/modules/projects/context/columnContext"; + +export default async function ProjectPage({ + params, +}: { + params: { id: string }; +}) { + const { project } = await useFetchProjectPage({ projectId: params.id }); + if (!project) return null; + + return ( + + + {project?.columns.map((column) => ( + + + + ))} + + + ); +} diff --git a/src/app/(app)/projects/error.tsx b/src/app/(app)/projects/error.tsx new file mode 100644 index 0000000..42c80aa --- /dev/null +++ b/src/app/(app)/projects/error.tsx @@ -0,0 +1,5 @@ +"use client"; + +export default function ProjectsError() { + return
Error occured
; +} diff --git a/src/app/(app)/projects/layout.tsx b/src/app/(app)/projects/layout.tsx index 49057e5..a2e27f4 100644 --- a/src/app/(app)/projects/layout.tsx +++ b/src/app/(app)/projects/layout.tsx @@ -1,5 +1,5 @@ -import React, { PropsWithChildren } from "react"; +import type { PropsWithChildren } from "react"; export default function ProjectsLayout({ children }: PropsWithChildren) { - return
{children}
; + return
{children}
; } diff --git a/src/app/(app)/projects/page.tsx b/src/app/(app)/projects/page.tsx index faf9be2..14baf95 100644 --- a/src/app/(app)/projects/page.tsx +++ b/src/app/(app)/projects/page.tsx @@ -1,20 +1,16 @@ -import { AccessTokenAlert } from "@/modules/projects/components/AccessTokenAlert"; -import { getUserRepositoriesAction } from "@/services/actions/get-user-repositories"; +import { useFetchProjectsPage } from "@/modules/projects/hooks/use-fetch-projects-page"; +import { PresentationalCard } from "@/modules/projects/components/PresentationalCard"; +import { ProjectsTable } from "@/modules/projects/components/_tables/projects-table"; +import { AddProjectForm } from "@/modules/projects/components/_forms/add-project-form"; export default async function ProjectsPage() { - const { data, serverError } = await getUserRepositoriesAction(); + const { projects } = await useFetchProjectsPage(); return ( - <> - {serverError && } -
- {data?.map((repo) => ( -
-

{repo.name}

-

{repo.description}

-
- ))} -
- +
+ + + {projects && } +
); } diff --git a/src/components/layouts/SharingFilter/_index.tsx b/src/components/layouts/SharingFilter/_index.tsx index 9634c9f..de81d1c 100644 --- a/src/components/layouts/SharingFilter/_index.tsx +++ b/src/components/layouts/SharingFilter/_index.tsx @@ -2,7 +2,7 @@ import { usePathname } from "next/navigation"; import { SharingFilterMobile } from "./SharingFilterMobile"; import { SharingFilterDesktop } from "./SharingFilterDesktop"; -import { URL } from "@/config/constants"; +import type { URL } from "@/config/constants"; export type PathnameType = typeof URL.REPOSITORIES | typeof URL.RESOURCES; diff --git a/src/components/ui/datatable.tsx b/src/components/ui/datatable.tsx new file mode 100644 index 0000000..cbaad1f --- /dev/null +++ b/src/components/ui/datatable.tsx @@ -0,0 +1,101 @@ +"use client"; +import { useRouter } from "next/navigation"; + +import { + type ColumnDef, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table"; + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { cn } from "@/lib/utils"; + +interface DataTableProps { + columns: ColumnDef[]; + data: TData[]; + asRowLink?: boolean; + route?: string; +} + +type IDataWithId = TData & { + id: string | number; +}; + +export function DataTable({ + columns, + asRowLink, + route, + data, +}: DataTableProps) { + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + }); + const router = useRouter(); + + const handleRowClick = (id: string | number) => { + if (!asRowLink) return; + + router.push(`${route}/${id}`); + }; + + return ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + + ); + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + handleRowClick((row.original as IDataWithId).id) + } + className={cn(asRowLink && "cursor-pointer")} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+ ); +} diff --git a/src/components/ui/dialog-wrapper.tsx b/src/components/ui/dialog-wrapper.tsx index a6ffd33..0dafcfd 100644 --- a/src/components/ui/dialog-wrapper.tsx +++ b/src/components/ui/dialog-wrapper.tsx @@ -1,14 +1,12 @@ import { Dialog, DialogTrigger, DialogContent } from "@/components/ui/dialog"; type Props = { - state?: boolean; triggerChildren: React.ReactNode; className?: string; children: React.ReactNode; }; export const DialogWrapper = ({ - state, triggerChildren, className, children, diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 0000000..a0ec48b --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx new file mode 100644 index 0000000..84f6c35 --- /dev/null +++ b/src/components/ui/table.tsx @@ -0,0 +1,124 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className, + )} + {...props} + /> +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)); +TableCaption.displayName = "TableCaption"; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/src/config/constants/index.ts b/src/config/constants/index.ts index 537216f..257993f 100644 --- a/src/config/constants/index.ts +++ b/src/config/constants/index.ts @@ -58,3 +58,26 @@ export const SHARE_ACTION = { COMMENT: "COMMENT_CONTENT", POINT: "LIKE_POINTS", } as const; + +export const DEFAULT_COLUMN_NAME = { + TO_DO: "To Do", + IN_PROGRESS: "In Progress", + DONE: "Done", +} as const; + +export const DEFAULT_COLUMN_DESCRIPTION = { + TO_DO: "This item hasn't been started", + IN_PROGRESS: "This is actively being worked on", + DONE: "This has been completed", +} as const; + +export const COLUMN_COLOR = { + GRAY: "GRAY", + BLUE: "BLUE", + GREEN: "GREEN", + YELLOW: "YELLOW", + ORANGE: "ORANGE", + RED: "RED", + PINK: "PINK", + PURPLE: "PURPLE", +} as const; diff --git a/src/config/providers/ErrorBoundary.tsx b/src/config/providers/ErrorBoundary.tsx index b2f805c..0bbbcad 100644 --- a/src/config/providers/ErrorBoundary.tsx +++ b/src/config/providers/ErrorBoundary.tsx @@ -1,6 +1,6 @@ "use client"; -import { Component, ErrorInfo, ReactNode } from "react"; +import { Component, type ErrorInfo, type ReactNode } from "react"; interface Props { children?: ReactNode; diff --git a/src/config/providers/TanstackProvider.tsx b/src/config/providers/TanstackProvider.tsx index e4d18e4..39fe08d 100644 --- a/src/config/providers/TanstackProvider.tsx +++ b/src/config/providers/TanstackProvider.tsx @@ -11,7 +11,7 @@ export const TanstackProvider = ({ children }: PropsWithChildren) => { {children} - {/* */} + ); diff --git a/src/config/types/prisma.type.ts b/src/config/types/prisma.type.ts index c30f1be..52ba22f 100644 --- a/src/config/types/prisma.type.ts +++ b/src/config/types/prisma.type.ts @@ -8,6 +8,14 @@ export type Resource = Prisma.ResourceGetPayload<{ include: { createdBy: true }; }>; +export type Project = Prisma.ProjectGetPayload<{ + include: { createdBy: true }; +}>; + +export type Column = Prisma.ColumnGetPayload<{ + include: { tasks: true }; +}>; + export type User = Prisma.UserGetPayload<{ include: { likes: true }; }>; diff --git a/src/jobs/examples.ts b/src/jobs/examples.ts index cec5619..5ca5b24 100644 --- a/src/jobs/examples.ts +++ b/src/jobs/examples.ts @@ -1,4 +1,4 @@ -import { eventTrigger, intervalTrigger } from "@trigger.dev/sdk"; +import { intervalTrigger } from "@trigger.dev/sdk"; import { client } from "@/trigger"; // Your first job @@ -11,7 +11,7 @@ client.defineJob({ enabled: false, // This is triggered by an event using eventTrigger. You can also trigger Jobs with webhooks, on schedules, and more: https://trigger.dev/docs/documentation/concepts/triggers/introduction trigger: intervalTrigger({ seconds: 60 }), - run: async (payload, io, ctx) => { + run: async (_, io) => { // Use a Task to generate a random number. Using a Tasks means it only runs once. const result = await io.runTask("generate-random-number", async () => { return { diff --git a/src/jobs/sync-repositories.ts b/src/jobs/sync-repositories.ts index 4faeec4..695b0be 100644 --- a/src/jobs/sync-repositories.ts +++ b/src/jobs/sync-repositories.ts @@ -12,7 +12,7 @@ client.defineJob({ cron: "0 0 * * *", }), - run: async (payload, io, ctx) => { + run: async (_, io) => { await io.runTask("sync-repositories", async () => { return await repositoryService.updatedSyncRepositories(); }); diff --git a/src/middleware.ts b/src/middleware.ts index 61e404c..a3c31fb 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -13,4 +13,4 @@ export default withAuth({ secret: env.NEXTAUTH_SECRET, }); -export const config = { matcher: ["/profile", "/settings"] }; +export const config = { matcher: ["/profile", "/settings", "/projects"] }; diff --git a/src/modules/projects/components/Column/ColumnHeader/ColumnAction/DeleteColumn.tsx b/src/modules/projects/components/Column/ColumnHeader/ColumnAction/DeleteColumn.tsx new file mode 100644 index 0000000..7149dbe --- /dev/null +++ b/src/modules/projects/components/Column/ColumnHeader/ColumnAction/DeleteColumn.tsx @@ -0,0 +1,49 @@ +"use client"; +import { useTransition } from "react"; +import { TrashIcon } from "@primer/octicons-react"; +import { DialogWrapper } from "@/components/ui/dialog-wrapper"; +import { SubmitButton } from "@/components/ui/submit-button"; +import { removeColumnAction } from "@/services/actions/remove-column"; +import type { Task } from "@prisma/client"; +import { useProjectsContext } from "@/modules/projects/context/projectContext"; + +type Props = { + columnId: string; + tasks: Task[]; +}; + +export const DeleteColumn = ({ columnId, tasks }: Props) => { + const { project } = useProjectsContext(); + const [isPending, startTransition] = useTransition(); + + const handleDelete = async (e: React.FormEvent) => { + e.preventDefault(); + + startTransition(async () => { + await removeColumnAction({ columnId, projectId: project.id }); + }); + }; + + return ( + + + Delete + + } + > +
+

Delete column

+

This column content {tasks.length}

+

Are you sure you want to delete this column?

+ +
+ + Delete + +
+
+
+ ); +}; diff --git a/src/modules/projects/components/Column/ColumnHeader/ColumnAction/_index.tsx b/src/modules/projects/components/Column/ColumnHeader/ColumnAction/_index.tsx new file mode 100644 index 0000000..2b4744b --- /dev/null +++ b/src/modules/projects/components/Column/ColumnHeader/ColumnAction/_index.tsx @@ -0,0 +1,33 @@ +import { KebabHorizontalIcon } from "@primer/octicons-react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; + +import { DeleteColumn } from "./DeleteColumn"; +import type { Task } from "@prisma/client"; + +type Props = { + columnId: string; + tasks: Task[]; +}; + +export const ColumnAction = ({ columnId, tasks }: Props) => { + return ( + + + + + +

Column

+ +
+
+ ); +}; diff --git a/src/modules/projects/components/Column/ColumnHeader/ColumnColor.tsx b/src/modules/projects/components/Column/ColumnHeader/ColumnColor.tsx new file mode 100644 index 0000000..c2d8f22 --- /dev/null +++ b/src/modules/projects/components/Column/ColumnHeader/ColumnColor.tsx @@ -0,0 +1,23 @@ +import { generateColumnBorderColor } from "@/modules/projects/utils/generate-column-border-color"; +import { generateColumnBackgroundColor } from "@/modules/projects/utils/generate-column-background-color"; +import type { COLUMN_COLOR } from "@/config/constants"; + +type Props = { + color: string; +}; + +export const ColumnColor = ({ color }: Props) => { + return ( +
+ ); +}; diff --git a/src/modules/projects/components/Column/ColumnHeader/ColumnCount.tsx b/src/modules/projects/components/Column/ColumnHeader/ColumnCount.tsx new file mode 100644 index 0000000..b4b767b --- /dev/null +++ b/src/modules/projects/components/Column/ColumnHeader/ColumnCount.tsx @@ -0,0 +1,13 @@ +import type { Task } from "@prisma/client"; + +type Props = { + tasks: Task[]; +}; + +export const ColumnCount = ({ tasks }: Props) => { + return ( +
+ {tasks.length} +
+ ); +}; diff --git a/src/modules/projects/components/Column/ColumnHeader/ColumnDescription.tsx b/src/modules/projects/components/Column/ColumnHeader/ColumnDescription.tsx new file mode 100644 index 0000000..7cd6d4e --- /dev/null +++ b/src/modules/projects/components/Column/ColumnHeader/ColumnDescription.tsx @@ -0,0 +1,7 @@ +type Props = { + description: string; +}; + +export const ColumnDescription = ({ description }: Props) => { + return

{description}

; +}; diff --git a/src/modules/projects/components/Column/ColumnHeader/ColumnName.tsx b/src/modules/projects/components/Column/ColumnHeader/ColumnName.tsx new file mode 100644 index 0000000..283e297 --- /dev/null +++ b/src/modules/projects/components/Column/ColumnHeader/ColumnName.tsx @@ -0,0 +1,9 @@ +import React from "react"; + +type Props = { + name: string; +}; + +export const ColumnName = ({ name }: Props) => { + return

{name}

; +}; diff --git a/src/modules/projects/components/Column/ColumnHeader/_index.tsx b/src/modules/projects/components/Column/ColumnHeader/_index.tsx new file mode 100644 index 0000000..90a85ea --- /dev/null +++ b/src/modules/projects/components/Column/ColumnHeader/_index.tsx @@ -0,0 +1,27 @@ +"use client"; +import { ColumnColor } from "./ColumnColor"; +import { ColumnName } from "./ColumnName"; +import { ColumnCount } from "./ColumnCount"; +import { ColumnAction } from "./ColumnAction/_index"; +import { ColumnDescription } from "./ColumnDescription"; +import { useColumnContext } from "@/modules/projects/context/columnContext"; + +export const ColumnHeader = () => { + const { + column: { id, color, name, description, tasks }, + } = useColumnContext(); + + return ( +
+
+
+ + + +
+ +
+ +
+ ); +}; diff --git a/src/modules/projects/components/Column/ColumnsList.tsx b/src/modules/projects/components/Column/ColumnsList.tsx new file mode 100644 index 0000000..3f1e3d0 --- /dev/null +++ b/src/modules/projects/components/Column/ColumnsList.tsx @@ -0,0 +1,5 @@ +import type { PropsWithChildren } from "react"; + +export const ColumnsList = ({ children }: PropsWithChildren) => { + return
{children}
; +}; diff --git a/src/modules/projects/components/Column/_index.tsx b/src/modules/projects/components/Column/_index.tsx new file mode 100644 index 0000000..6e04dd7 --- /dev/null +++ b/src/modules/projects/components/Column/_index.tsx @@ -0,0 +1,13 @@ +import { ColumnHeader } from "./ColumnHeader/_index"; +import { TasksList } from "@/modules/projects/components/Task/TasksList"; +import { AddTaskDialog } from "@/modules/projects/components/Task/AddTaskDialog"; + +export const Column = () => { + return ( +
+ + + +
+ ); +}; diff --git a/src/modules/projects/components/PresentationalCard.tsx b/src/modules/projects/components/PresentationalCard.tsx new file mode 100644 index 0000000..ce7a46c --- /dev/null +++ b/src/modules/projects/components/PresentationalCard.tsx @@ -0,0 +1,18 @@ +import { Button } from "@/components/ui/button"; +import React from "react"; + +export const PresentationalCard = () => { + return ( +
+

+ Welcome to the gitsharespace kanban +

+

+ This is a kanban board that allows you to create, edit, and delete + tasks. It also allows you to move tasks between columns. It is built + with React, Tailwind CSS, and Zustand. +

+ +
+ ); +}; diff --git a/src/modules/projects/components/ProjectsList.tsx b/src/modules/projects/components/ProjectsList.tsx deleted file mode 100644 index af23fec..0000000 --- a/src/modules/projects/components/ProjectsList.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from "react"; - -export const ProjectsList = () => { - return
ProjectsList
; -}; diff --git a/src/modules/projects/components/Task/AddTaskButton.tsx b/src/modules/projects/components/Task/AddTaskButton.tsx new file mode 100644 index 0000000..ecc4b76 --- /dev/null +++ b/src/modules/projects/components/Task/AddTaskButton.tsx @@ -0,0 +1,16 @@ +import { PlusIcon } from "@primer/octicons-react"; +import React from "react"; + +interface AddTaskButtonProps + extends React.ButtonHTMLAttributes {} + +export const AddTaskButton = ({ ...props }: AddTaskButtonProps) => { + return ( + + ); +}; diff --git a/src/modules/projects/components/Task/AddTaskDialog.tsx b/src/modules/projects/components/Task/AddTaskDialog.tsx new file mode 100644 index 0000000..9bfa3b3 --- /dev/null +++ b/src/modules/projects/components/Task/AddTaskDialog.tsx @@ -0,0 +1,17 @@ +"use client"; +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; +import { AddTaskButton } from "@/modules/projects/components/Task/AddTaskButton"; +import { AddTaskForm } from "@/modules/projects/components/_forms/add-task-form"; + +export const AddTaskDialog = () => { + return ( + + + + + + + + + ); +}; diff --git a/src/modules/projects/components/Task/TasksList.tsx b/src/modules/projects/components/Task/TasksList.tsx new file mode 100644 index 0000000..9fbafce --- /dev/null +++ b/src/modules/projects/components/Task/TasksList.tsx @@ -0,0 +1,19 @@ +"use client"; +import { useColumnContext } from "@/modules/projects/context/columnContext"; + +export const TasksList = () => { + const { + column: { tasks }, + } = useColumnContext(); + + return ( +
+ {tasks.map((task) => ( +
+

{task.name}

+

{task.description}

+
+ ))} +
+ ); +}; diff --git a/src/modules/projects/components/_forms/add-project-form.tsx b/src/modules/projects/components/_forms/add-project-form.tsx new file mode 100644 index 0000000..5e5b01a --- /dev/null +++ b/src/modules/projects/components/_forms/add-project-form.tsx @@ -0,0 +1,67 @@ +"use client"; +import { useTransition } from "react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { useToast } from "@/components/ui/use-toast"; +import { addProjectAction } from "@/services/actions/add-project"; +import { addProjectSchema } from "./add-project-schema"; +import { Form } from "@/components/ui/form"; +import { InputForm } from "@/components/ui/input-form"; +import { TextAreaForm } from "@/components/ui/text-area-form"; +import { SubmitButton } from "@/components/ui/submit-button"; +import type * as z from "zod"; + +export const AddProjectForm = ({}) => { + const [isPending, startTransition] = useTransition(); + const { toast } = useToast(); + + const form = useForm>({ + resolver: zodResolver(addProjectSchema), + defaultValues: {}, + }); + + function onSubmit(data: z.infer) { + startTransition(async () => { + const response = await addProjectAction(data); + + if (!response.data?.error) { + form.reset({ + name: "", + description: "", + }); + + toast({ + title: "Project created", + description: "Your project has been created.", + }); + } + + if (response.data?.error) { + toast({ + title: "Error", + description: response.serverError, + }); + } + }); + } + + return ( +
+ + + + + Create project + + + ); +}; diff --git a/src/modules/projects/components/_forms/add-project-schema.ts b/src/modules/projects/components/_forms/add-project-schema.ts new file mode 100644 index 0000000..4f7106e --- /dev/null +++ b/src/modules/projects/components/_forms/add-project-schema.ts @@ -0,0 +1,6 @@ +import * as z from "zod"; + +export const addProjectSchema = z.object({ + name: z.string(), + description: z.string(), +}); diff --git a/src/modules/projects/components/_forms/add-task-form.tsx b/src/modules/projects/components/_forms/add-task-form.tsx new file mode 100644 index 0000000..21da920 --- /dev/null +++ b/src/modules/projects/components/_forms/add-task-form.tsx @@ -0,0 +1,50 @@ +"use client"; +import { useTransition } from "react"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useColumnContext } from "@/modules/projects/context/columnContext"; +import { useProjectsContext } from "../../context/projectContext"; +import { addTaskAction } from "@/services/actions/add-task"; +import { addTaskFormSchema } from "./add-task-schema"; +import { Form } from "@/components/ui/form"; +import { InputForm } from "@/components/ui/input-form"; +import { TextAreaForm } from "@/components/ui/text-area-form"; +import { SubmitButton } from "@/components/ui/submit-button"; +import type * as z from "zod"; + +export const AddTaskForm = () => { + const { project } = useProjectsContext(); + const { column } = useColumnContext(); + const [isPending, startTransition] = useTransition(); + + const form = useForm>({ + resolver: zodResolver(addTaskFormSchema), + }); + + function onSubmit(data: z.infer) { + startTransition(async () => { + await addTaskAction({ + name: data.name, + description: data.description, + columnId: column.id, + projectId: project.id, + }); + }); + + form.reset(); + } + + return ( +
+ + + + Create task + + + ); +}; diff --git a/src/modules/projects/components/_forms/add-task-schema.ts b/src/modules/projects/components/_forms/add-task-schema.ts new file mode 100644 index 0000000..36743ca --- /dev/null +++ b/src/modules/projects/components/_forms/add-task-schema.ts @@ -0,0 +1,6 @@ +import * as z from "zod"; + +export const addTaskFormSchema = z.object({ + name: z.string(), + description: z.string(), +}); diff --git a/src/modules/projects/components/_tables/projects-columns.tsx b/src/modules/projects/components/_tables/projects-columns.tsx new file mode 100644 index 0000000..1be5dc1 --- /dev/null +++ b/src/modules/projects/components/_tables/projects-columns.tsx @@ -0,0 +1,15 @@ +"use client"; + +import type { Project } from "@prisma/client"; +import type { ColumnDef } from "@tanstack/react-table"; + +export const projectColumns: ColumnDef[] = [ + { + accessorKey: "name", + header: "Name", + }, + { + accessorKey: "description", + header: "Description", + }, +]; diff --git a/src/modules/projects/components/_tables/projects-table.tsx b/src/modules/projects/components/_tables/projects-table.tsx new file mode 100644 index 0000000..2dea827 --- /dev/null +++ b/src/modules/projects/components/_tables/projects-table.tsx @@ -0,0 +1,19 @@ +import { DataTable } from "@/components/ui/datatable"; +import { projectColumns } from "./projects-columns"; +import type { Project } from "@/config/types/prisma.type"; +import { URL } from "@/config/constants"; + +type Props = { + projects: Project[]; +}; + +export const ProjectsTable = ({ projects }: Props) => { + return ( + + ); +}; diff --git a/src/modules/projects/context/columnContext.tsx b/src/modules/projects/context/columnContext.tsx new file mode 100644 index 0000000..03f63a8 --- /dev/null +++ b/src/modules/projects/context/columnContext.tsx @@ -0,0 +1,30 @@ +"use client"; + +import { type PropsWithChildren, createContext, useContext } from "react"; +import type { Column } from "@/config/types/prisma.type"; + +interface ColumnContext { + column: Column; +} + +type ColumnProviderProps = PropsWithChildren; + +const ColumnContext = createContext(null); + +export const ColumnProvider = ({ column, children }: ColumnProviderProps) => { + return ( + + {children} + + ); +}; + +export const useColumnContext = () => { + const context = useContext(ColumnContext); + + if (!context) { + throw new Error("useColumnContext must be used within a ColumnProvider"); + } + + return context; +}; diff --git a/src/modules/projects/context/projectContext.tsx b/src/modules/projects/context/projectContext.tsx new file mode 100644 index 0000000..275395a --- /dev/null +++ b/src/modules/projects/context/projectContext.tsx @@ -0,0 +1,34 @@ +"use client"; +import { type PropsWithChildren, createContext, useContext } from "react"; +import type { Project } from "@/config/types/prisma.type"; + +interface ProjectContext { + project: Project; +} + +type ProjectProviderProps = PropsWithChildren; + +const ProjectContext = createContext(null); + +export const ProjectProvider = ({ + project, + children, +}: ProjectProviderProps) => { + return ( + + {children} + + ); +}; + +export const useProjectsContext = () => { + const context = useContext(ProjectContext); + + if (!context) { + throw new Error( + "useProjectsContext must be used within a ProjectsProvider", + ); + } + + return context; +}; diff --git a/src/modules/projects/hooks/use-fetch-project-page.tsx b/src/modules/projects/hooks/use-fetch-project-page.tsx new file mode 100644 index 0000000..c47642d --- /dev/null +++ b/src/modules/projects/hooks/use-fetch-project-page.tsx @@ -0,0 +1,16 @@ +import { getServerAuthSession } from "@/config/server/auth"; +import projectService from "@/services/project.service"; + +type Props = { + projectId: string; +}; + +export const useFetchProjectPage = async ({ projectId }: Props) => { + const session = await getServerAuthSession(); + const project = await projectService.getUserProjectById({ + projectId: projectId, + userId: session?.user.id ?? "", + }); + + return { project }; +}; diff --git a/src/modules/projects/hooks/use-fetch-projects-page.tsx b/src/modules/projects/hooks/use-fetch-projects-page.tsx new file mode 100644 index 0000000..a4814b0 --- /dev/null +++ b/src/modules/projects/hooks/use-fetch-projects-page.tsx @@ -0,0 +1,12 @@ +import { getServerAuthSession } from "@/config/server/auth"; +import projectService from "@/services/project.service"; + +export const useFetchProjectsPage = async () => { + const session = await getServerAuthSession(); + + const projects = await projectService.getUserProjects({ + userId: session?.user.id ?? "", + }); + + return { projects }; +}; diff --git a/src/modules/projects/stores/useAddTaskModal.ts b/src/modules/projects/stores/useAddTaskModal.ts new file mode 100644 index 0000000..d3e5969 --- /dev/null +++ b/src/modules/projects/stores/useAddTaskModal.ts @@ -0,0 +1,11 @@ +import { create } from "zustand"; + +interface AddTaskModalState { + open: boolean; + setOpen: (open: boolean) => void; +} + +export const useAddTaskModal = create((set) => ({ + open: false, + setOpen: (open) => set({ open }), +})); diff --git a/src/modules/projects/utils/generate-column-background-color.ts b/src/modules/projects/utils/generate-column-background-color.ts new file mode 100644 index 0000000..618b25c --- /dev/null +++ b/src/modules/projects/utils/generate-column-background-color.ts @@ -0,0 +1,24 @@ +import { COLUMN_COLOR } from "@/config/constants"; + +export const generateColumnBackgroundColor = ( + color: keyof typeof COLUMN_COLOR, +) => { + switch (color) { + case COLUMN_COLOR.GRAY: + return "#161B22"; + case COLUMN_COLOR.BLUE: + return "#1A2639"; + case COLUMN_COLOR.GREEN: + return "#1A2F27"; + case COLUMN_COLOR.YELLOW: + return "#2F2A1E"; + case COLUMN_COLOR.ORANGE: + return "#2A2323"; + case COLUMN_COLOR.RED: + return "#2D2026"; + case COLUMN_COLOR.PINK: + return "#2A2230"; + case COLUMN_COLOR.PURPLE: + return "#252438"; + } +}; diff --git a/src/modules/projects/utils/generate-column-border-color.ts b/src/modules/projects/utils/generate-column-border-color.ts new file mode 100644 index 0000000..735bb71 --- /dev/null +++ b/src/modules/projects/utils/generate-column-border-color.ts @@ -0,0 +1,22 @@ +import { COLUMN_COLOR } from "@/config/constants"; + +export const generateColumnBorderColor = (color: keyof typeof COLUMN_COLOR) => { + switch (color) { + case COLUMN_COLOR.GRAY: + return "#848D97"; + case COLUMN_COLOR.BLUE: + return "#2F81F7"; + case COLUMN_COLOR.GREEN: + return "#3FB950"; + case COLUMN_COLOR.YELLOW: + return "#D29922"; + case COLUMN_COLOR.ORANGE: + return "#D29922"; + case COLUMN_COLOR.RED: + return "#D29922"; + case COLUMN_COLOR.PINK: + return "#DB61A2"; + case COLUMN_COLOR.PURPLE: + return "#DB61A2"; + } +}; diff --git a/src/services/actions/add-project.ts b/src/services/actions/add-project.ts new file mode 100644 index 0000000..5974e97 --- /dev/null +++ b/src/services/actions/add-project.ts @@ -0,0 +1,27 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { userAction } from "@/config/lib/next-safe-action"; +import projectService from "../project.service"; +import { URL } from "@/config/constants"; +import * as z from "zod"; + +const schema = z.object({ + name: z.string(), + description: z.string(), +}); + +export const addProjectAction = userAction(schema, async (data, ctx) => { + try { + await projectService.createProject({ + userId: ctx.session.user.id, + name: data.name, + description: data.description, + }); + } catch (error) { + if (error instanceof Error) { + return { error: error.message }; + } + } + + revalidatePath(URL.PROJECTS); +}); diff --git a/src/services/actions/add-task.ts b/src/services/actions/add-task.ts new file mode 100644 index 0000000..a98a042 --- /dev/null +++ b/src/services/actions/add-task.ts @@ -0,0 +1,33 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { userAction } from "@/config/lib/next-safe-action"; +import projectService from "../project.service"; +import { URL } from "@/config/constants"; +import * as z from "zod"; + +const schema = z.object({ + name: z.string(), + description: z.string(), + columnId: z.string(), + projectId: z.string(), +}); + +export const addTaskAction = userAction( + schema, + async ({ name, description, projectId, columnId }, ctx) => { + try { + await projectService.createTask({ + name, + description, + columnId, + createdById: ctx.session.user.id, + }); + } catch (error) { + if (error instanceof Error) { + return { error: error.message }; + } + } + + revalidatePath(`${URL.PROJECTS}/${projectId}`); + }, +); diff --git a/src/services/actions/remove-column.ts b/src/services/actions/remove-column.ts new file mode 100644 index 0000000..724c9a1 --- /dev/null +++ b/src/services/actions/remove-column.ts @@ -0,0 +1,22 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { userAction } from "@/config/lib/next-safe-action"; +import projectService from "@/services/project.service"; +import { URL } from "@/config/constants"; +import * as z from "zod"; + +const schema = z.object({ + columnId: z.string(), + projectId: z.string(), +}); + +export const removeColumnAction = userAction( + schema, + async ({ columnId, projectId }) => { + try { + await projectService.deleteColumn({ columnId }); + } catch (error) {} + + revalidatePath(`${URL.PROJECTS}/${projectId}`); + }, +); diff --git a/src/services/project.service.ts b/src/services/project.service.ts new file mode 100644 index 0000000..9fb8e7f --- /dev/null +++ b/src/services/project.service.ts @@ -0,0 +1,169 @@ +import { db } from "@/config/server/db"; +import type { + CreateProject, + CreateTaskType, + DeleteColumn, + GetUserProjectById, + GetUserProjects, +} from "./types/project.type"; + +import { + COLUMN_COLOR, + DEFAULT_COLUMN_DESCRIPTION, + DEFAULT_COLUMN_NAME, +} from "@/config/constants"; + +class ProjectService { + /** + * Query to get all projects created by a user. + * @param {GetUserProjects} - The user id. + * @throws {Error} - Throws an error if there's an error accessing the database. + */ + + async getUserProjects({ userId }: GetUserProjects) { + try { + return await db.project.findMany({ + where: { + createdById: userId, + }, + include: { + createdBy: true, + }, + }); + } catch (error) { + if (error instanceof Error) { + console.log(error.message); + } + } + } + + /** + * Query to get a project created by a user. + * @param {GetUserProjectById} - The user id and project id. + * @throws {Error} - Throws an error if there's an error accessing the database. + */ + + async getUserProjectById({ userId, projectId }: GetUserProjectById) { + try { + return await db.project.findFirst({ + where: { + id: projectId, + createdById: userId, + }, + include: { + createdBy: true, + columns: { + include: { + tasks: true, + }, + }, + }, + }); + } catch (error) { + if (error instanceof Error) { + console.log(error.message); + } + } + } + + /** + * Query to create a project. + * @param {CreateProject} - The user id, project name, and project description. + * @throws {Error} - Throws an error if there's an error accessing the database. + */ + + async createProject({ userId, name, description }: CreateProject) { + try { + await db.project.create({ + data: { + name, + description, + columns: { + create: [ + { + name: DEFAULT_COLUMN_NAME.TO_DO, + description: DEFAULT_COLUMN_DESCRIPTION.TO_DO, + color: COLUMN_COLOR.GRAY, + createdById: userId, + }, + { + name: DEFAULT_COLUMN_NAME.IN_PROGRESS, + description: DEFAULT_COLUMN_DESCRIPTION.IN_PROGRESS, + color: COLUMN_COLOR.YELLOW, + createdById: userId, + }, + { + name: DEFAULT_COLUMN_NAME.DONE, + description: DEFAULT_COLUMN_DESCRIPTION.DONE, + color: COLUMN_COLOR.PURPLE, + createdById: userId, + }, + ], + }, + + createdBy: { + connect: { + id: userId, + }, + }, + }, + }); + } catch (error) { + if (error instanceof Error) { + console.log(error.message); + } + } + } + + /** + * Query to delete a column. + * @param {DeleteColumn} - The column id. + * @throws {Error} - Throws an error if there's an error accessing the database. + */ + + async deleteColumn({ columnId }: DeleteColumn) { + try { + await db.$transaction([ + db.column.delete({ + where: { + id: columnId, + }, + }), + db.task.deleteMany({ + where: { + columnId, + }, + }), + ]); + } catch (error) { + if (error instanceof Error) { + console.log(error.message); + } + } + } + + async createTask({ + name, + description, + columnId, + createdById, + }: CreateTaskType) { + try { + await db.task.create({ + data: { + name, + description, + createdById, + columnId, + }, + }); + } catch (error) { + if (error instanceof Error) { + console.log(error.message); + } + } + } +} + +const projectService = new ProjectService(); +export default projectService; diff --git a/src/services/repository.service.ts b/src/services/repository.service.ts index 54385a9..42db516 100644 --- a/src/services/repository.service.ts +++ b/src/services/repository.service.ts @@ -340,7 +340,7 @@ class RepositoryService { query: GET_MULTIPLE_REPOSITORIES(repositories), }); - const updates = Object.entries(data as Data).map(([key, value]) => { + const updates = Object.entries(data as Data).map(([_, value]) => { const repositoryData = value; const repository = repositories.find( (repo) => repo.repositoryName === repositoryData.name, diff --git a/src/services/types/project.type.ts b/src/services/types/project.type.ts new file mode 100644 index 0000000..e230fdf --- /dev/null +++ b/src/services/types/project.type.ts @@ -0,0 +1,33 @@ +type GetUserProjects = { + userId: string; +}; + +type GetUserProjectById = { + userId: string; + projectId: string; +}; + +type CreateProject = { + userId: string; + name: string; + description: string; +}; + +type DeleteColumn = { + columnId: string; +}; + +type CreateTaskType = { + name: string; + description: string; + columnId: string; + createdById: string; +}; + +export type { + GetUserProjects, + GetUserProjectById, + CreateProject, + DeleteColumn, + CreateTaskType, +}; diff --git a/tailwind.config.ts b/tailwind.config.ts index 3b3570b..459324e 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -107,69 +107,3 @@ const config = { } satisfies Config; export default config; - -// import type { Config } from "tailwindcss"; - -// const config = { -// darkMode: ["class"], -// content: [ -// "./pages/**/*.{ts,tsx}", -// "./components/**/*.{ts,tsx}", -// "./app/**/*.{ts,tsx}", -// "./src/**/*.{ts,tsx}", -// ], -// prefix: "", -// theme: { -// container: { -// center: true, -// padding: "2rem", -// screens: { -// "2xl": "1400px", -// }, -// }, -// extend: { -// keyframes: { -// "accordion-down": { -// from: { height: "0" }, -// to: { height: "var(--radix-accordion-content-height)" }, -// }, -// "accordion-up": { -// from: { height: "var(--radix-accordion-content-height)" }, -// to: { height: "0" }, -// }, -// }, -// backgroundColor: { -// default: "#0D1117", -// overlay: "#161B22", -// inset: "#010409", -// subtle: "#6E7781", -// success: "#238636", -// skeleton: "#0D1117", -// "subtle-hover": "#292F36", -// "success-hover": "#2EA043", -// "button-hover": "#171B20", -// }, -// textColor: { -// default: "#E6EDF3", -// subtle: "#6E7781", -// link: "#2F81E8", -// icon: "#848D97", -// }, -// boxShadow: { -// overlay: "0 0 0 1px #30363d, 0 16px 32px rgba(1,4,9,0.85)", -// }, -// borderColor: { -// default: "#21262D", -// card: "#30363D", -// }, - -// animation: { -// "accordion-down": "accordion-down 0.2s ease-out", -// "accordion-up": "accordion-up 0.2s ease-out", -// }, -// }, -// }, -// plugins: [require("tailwindcss-animate")], -// } satisfies Config; - -// export default config; From d67dcbfeb667b053539dc914b6f64821838b1674 Mon Sep 17 00:00:00 2001 From: SwiichyCode Date: Wed, 10 Apr 2024 16:53:42 +0200 Subject: [PATCH 3/3] feat: projects --- .eslintrc.cjs | 1 + prisma/schema.prisma | 10 +-- src/app/(app)/projects/[id]/page.tsx | 29 +++++--- src/app/(app)/projects/page.tsx | 17 ++++- src/components/ui/datatable.tsx | 2 +- src/components/ui/dialog-wrapper.tsx | 2 +- src/components/ui/link.tsx | 12 ++++ src/components/ui/popover-kebab-btn.tsx | 9 +++ src/components/ui/popover.tsx | 24 +++---- src/components/ui/table.tsx | 2 +- src/middleware.ts | 15 +++- .../ColumnHeader/ColumnAction/_index.tsx | 35 ++++++---- .../components/Column/ColumnHeader/_index.tsx | 7 +- .../Column/ColumnHeader/_layout.tsx | 5 ++ .../projects/components/Column/_index.tsx | 2 +- .../Column/{ColumnsList.tsx => _layout.tsx} | 2 +- .../projects/components/DeleteActionBtn.tsx | 17 +++++ .../components/DeleteColumnDialog.tsx | 68 +++++++++++++++++++ .../projects/components/PopoverAction.tsx | 36 ++++++++++ .../components/ProjectsNavigation.tsx | 8 +++ .../TaskAction/DeleteTask.tsx} | 4 +- .../Task/TaskAction/TaskCardAction.tsx | 9 +++ .../Task/TaskCard/TaskCardDescription.tsx | 7 ++ .../components/Task/TaskCard/_index.tsx | 20 ++++++ .../components/Task/TaskCard/_layout.tsx | 9 +++ .../projects/components/Task/TasksList.tsx | 19 ------ .../projects/components/Task/_index.tsx | 18 +++++ .../projects/components/Task/_layout.tsx | 9 +++ .../components/_dialogs/project-dialogs.tsx | 29 ++++++++ .../components/_forms/add-task-form.tsx | 4 +- .../components/_forms/close-project-form.tsx | 29 ++++++++ .../components/_tables/projects-columns.tsx | 19 +++++- .../components/_tables/projects-table.tsx | 7 +- .../projects/hooks/use-projects-params.tsx | 10 +++ .../hooks/use-query-parser-project-page.tsx | 18 +++++ .../projects/stores/useActionDialog.ts | 17 +++++ src/services/actions/close-project.ts | 24 +++++++ src/services/project.service.ts | 19 ++++++ src/services/types/project.type.ts | 5 ++ tsconfig.json | 3 +- 40 files changed, 499 insertions(+), 83 deletions(-) create mode 100644 src/components/ui/link.tsx create mode 100644 src/components/ui/popover-kebab-btn.tsx create mode 100644 src/modules/projects/components/Column/ColumnHeader/_layout.tsx rename src/modules/projects/components/Column/{ColumnsList.tsx => _layout.tsx} (65%) create mode 100644 src/modules/projects/components/DeleteActionBtn.tsx create mode 100644 src/modules/projects/components/DeleteColumnDialog.tsx create mode 100644 src/modules/projects/components/PopoverAction.tsx create mode 100644 src/modules/projects/components/ProjectsNavigation.tsx rename src/modules/projects/components/{Column/ColumnHeader/ColumnAction/DeleteColumn.tsx => Task/TaskAction/DeleteTask.tsx} (92%) create mode 100644 src/modules/projects/components/Task/TaskAction/TaskCardAction.tsx create mode 100644 src/modules/projects/components/Task/TaskCard/TaskCardDescription.tsx create mode 100644 src/modules/projects/components/Task/TaskCard/_index.tsx create mode 100644 src/modules/projects/components/Task/TaskCard/_layout.tsx delete mode 100644 src/modules/projects/components/Task/TasksList.tsx create mode 100644 src/modules/projects/components/Task/_index.tsx create mode 100644 src/modules/projects/components/Task/_layout.tsx create mode 100644 src/modules/projects/components/_dialogs/project-dialogs.tsx create mode 100644 src/modules/projects/components/_forms/close-project-form.tsx create mode 100644 src/modules/projects/hooks/use-projects-params.tsx create mode 100644 src/modules/projects/hooks/use-query-parser-project-page.tsx create mode 100644 src/modules/projects/stores/useActionDialog.ts create mode 100644 src/services/actions/close-project.ts diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 769f110..dc7a64d 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -28,6 +28,7 @@ const config = { "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/dot-notation": "off", "@typescript-eslint/no-misused-promises": [ "error", { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 145de0a..8544b23 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -137,13 +137,15 @@ model Repository { } model Project { - id String @id @default(uuid()) + id String @id @default(uuid()) name String description String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - createdBy User @relation(fields: [createdById], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy User @relation(fields: [createdById], references: [id]) createdById String + closed Boolean @default(false) + closedAt DateTime? columns Column[] } diff --git a/src/app/(app)/projects/[id]/page.tsx b/src/app/(app)/projects/[id]/page.tsx index d2efb4a..ac98acc 100644 --- a/src/app/(app)/projects/[id]/page.tsx +++ b/src/app/(app)/projects/[id]/page.tsx @@ -1,26 +1,39 @@ import { useFetchProjectPage } from "@/modules/projects/hooks/use-fetch-project-page"; -import { ColumnsList } from "@/modules/projects/components/Column/ColumnsList"; import { Column } from "@/modules/projects/components/Column/_index"; import { ProjectProvider } from "@/modules/projects/context/projectContext"; import { ColumnProvider } from "@/modules/projects/context/columnContext"; +import { ColumnsLayout } from "@/modules/projects/components/Column/_layout"; +import { DeleteColumnDialog } from "@/modules/projects/components/DeleteColumnDialog"; -export default async function ProjectPage({ - params, -}: { - params: { id: string }; -}) { +import { + useQueryParserProjectPage, + type SearchParamsType, +} from "@/modules/projects/hooks/use-query-parser-project-page"; + +type PageProps = { + params: { + id: string; + }; +} & SearchParamsType; + +export default async function ProjectPage({ params, searchParams }: PageProps) { const { project } = await useFetchProjectPage({ projectId: params.id }); + const { projectId, columnId } = useQueryParserProjectPage({ searchParams }); + if (!project) return null; return ( - + {project?.columns.map((column) => ( ))} - + + + {/* All dialog action */} + ); } diff --git a/src/app/(app)/projects/page.tsx b/src/app/(app)/projects/page.tsx index 14baf95..98ae735 100644 --- a/src/app/(app)/projects/page.tsx +++ b/src/app/(app)/projects/page.tsx @@ -2,15 +2,28 @@ import { useFetchProjectsPage } from "@/modules/projects/hooks/use-fetch-project import { PresentationalCard } from "@/modules/projects/components/PresentationalCard"; import { ProjectsTable } from "@/modules/projects/components/_tables/projects-table"; import { AddProjectForm } from "@/modules/projects/components/_forms/add-project-form"; +import { ProjectsNavigation } from "@/modules/projects/components/ProjectsNavigation"; + +export default async function ProjectsPage({ + searchParams, +}: { + searchParams: Record; +}) { + const { query } = searchParams; -export default async function ProjectsPage() { const { projects } = await useFetchProjectsPage(); return (
- {projects && } + + {projects && ( +
+ + +
+ )}
); } diff --git a/src/components/ui/datatable.tsx b/src/components/ui/datatable.tsx index cbaad1f..0bc24ea 100644 --- a/src/components/ui/datatable.tsx +++ b/src/components/ui/datatable.tsx @@ -53,7 +53,7 @@ export function DataTable({ {table.getHeaderGroups().map((headerGroup) => ( - + {headerGroup.headers.map((header) => { return ( diff --git a/src/components/ui/dialog-wrapper.tsx b/src/components/ui/dialog-wrapper.tsx index 0dafcfd..2cfe3a7 100644 --- a/src/components/ui/dialog-wrapper.tsx +++ b/src/components/ui/dialog-wrapper.tsx @@ -13,7 +13,7 @@ export const DialogWrapper = ({ }: Props) => { return ( - {triggerChildren} + {triggerChildren} {children} ); diff --git a/src/components/ui/link.tsx b/src/components/ui/link.tsx new file mode 100644 index 0000000..5682d05 --- /dev/null +++ b/src/components/ui/link.tsx @@ -0,0 +1,12 @@ +import Link, { type LinkProps } from "next/link"; +import type { LinkHTMLAttributes } from "react"; + +interface CustomLinkProps extends LinkHTMLAttributes {} + +export const CustomLink = (props: CustomLinkProps) => { + return ( + + {props.children} + + ); +}; diff --git a/src/components/ui/popover-kebab-btn.tsx b/src/components/ui/popover-kebab-btn.tsx new file mode 100644 index 0000000..c3bd711 --- /dev/null +++ b/src/components/ui/popover-kebab-btn.tsx @@ -0,0 +1,9 @@ +import { KebabHorizontalIcon } from "@primer/octicons-react"; + +export const PopoverKebabBtn = () => { + return ( +
+ +
+ ); +}; diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx index a0ec48b..e5e9abb 100644 --- a/src/components/ui/popover.tsx +++ b/src/components/ui/popover.tsx @@ -1,31 +1,31 @@ -"use client" +"use client"; -import * as React from "react" -import * as PopoverPrimitive from "@radix-ui/react-popover" +import * as React from "react"; +import * as PopoverPrimitive from "@radix-ui/react-popover"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const Popover = PopoverPrimitive.Root +const Popover = PopoverPrimitive.Root; -const PopoverTrigger = PopoverPrimitive.Trigger +const PopoverTrigger = PopoverPrimitive.Trigger; const PopoverContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( +>(({ className, align = "end", sideOffset = 4, ...props }, ref) => ( -)) -PopoverContent.displayName = PopoverPrimitive.Content.displayName +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; -export { Popover, PopoverTrigger, PopoverContent } +export { Popover, PopoverTrigger, PopoverContent }; diff --git a/src/components/ui/table.tsx b/src/components/ui/table.tsx index 84f6c35..045dc2c 100644 --- a/src/components/ui/table.tsx +++ b/src/components/ui/table.tsx @@ -65,7 +65,7 @@ const TableRow = React.forwardRef<
{ - const sessionToken = req.cookies.get(env.SESSION_TOKEN_NAME); - if (!sessionToken) return false; + authorized: async ({ req }) => { + const session = await getToken({ + req, + secret: env.NEXTAUTH_SECRET, + cookieName: + env.NODE_ENV === "production" + ? "__Secure-next-auth.session-token" + : "next-auth.session-token", + }); + + if (!session) return false; return true; }, diff --git a/src/modules/projects/components/Column/ColumnHeader/ColumnAction/_index.tsx b/src/modules/projects/components/Column/ColumnHeader/ColumnAction/_index.tsx index 2b4744b..90fcdea 100644 --- a/src/modules/projects/components/Column/ColumnHeader/ColumnAction/_index.tsx +++ b/src/modules/projects/components/Column/ColumnHeader/ColumnAction/_index.tsx @@ -1,32 +1,39 @@ -import { KebabHorizontalIcon } from "@primer/octicons-react"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { DeleteColumn } from "./DeleteColumn"; -import type { Task } from "@prisma/client"; +import { PopoverKebabBtn } from "@/components/ui/popover-kebab-btn"; +import { useProjectParams } from "@/modules/projects/hooks/use-projects-params"; +import { useProjectsContext } from "@/modules/projects/context/projectContext"; +import { DeleteActionBtn } from "@/modules/projects/components/DeleteActionBtn"; +import { useActionDialog } from "@/modules/projects/stores/useActionDialog"; type Props = { columnId: string; - tasks: Task[]; }; -export const ColumnAction = ({ columnId, tasks }: Props) => { +export const ColumnAction = ({ columnId }: Props) => { + const { project } = useProjectsContext(); + const { params, setParams } = useProjectParams(); + const { setOpen } = useActionDialog(); + + const handleParams = async () => { + await setParams({ ...params, columnId, projectId: project.id }); + }; + return ( - - + + - +

Column

- + setOpen("column", true)} + />
); diff --git a/src/modules/projects/components/Column/ColumnHeader/_index.tsx b/src/modules/projects/components/Column/ColumnHeader/_index.tsx index 90a85ea..0e22106 100644 --- a/src/modules/projects/components/Column/ColumnHeader/_index.tsx +++ b/src/modules/projects/components/Column/ColumnHeader/_index.tsx @@ -1,4 +1,5 @@ "use client"; +import { ColumnHeaderLayout } from "./_layout"; import { ColumnColor } from "./ColumnColor"; import { ColumnName } from "./ColumnName"; import { ColumnCount } from "./ColumnCount"; @@ -12,16 +13,16 @@ export const ColumnHeader = () => { } = useColumnContext(); return ( -
+
- +
-
+ ); }; diff --git a/src/modules/projects/components/Column/ColumnHeader/_layout.tsx b/src/modules/projects/components/Column/ColumnHeader/_layout.tsx new file mode 100644 index 0000000..563468f --- /dev/null +++ b/src/modules/projects/components/Column/ColumnHeader/_layout.tsx @@ -0,0 +1,5 @@ +import type { PropsWithChildren } from "react"; + +export const ColumnHeaderLayout = ({ children }: PropsWithChildren) => { + return
{children}
; +}; diff --git a/src/modules/projects/components/Column/_index.tsx b/src/modules/projects/components/Column/_index.tsx index 6e04dd7..e41adc2 100644 --- a/src/modules/projects/components/Column/_index.tsx +++ b/src/modules/projects/components/Column/_index.tsx @@ -1,5 +1,5 @@ import { ColumnHeader } from "./ColumnHeader/_index"; -import { TasksList } from "@/modules/projects/components/Task/TasksList"; +import { TasksList } from "@/modules/projects/components/Task/_index"; import { AddTaskDialog } from "@/modules/projects/components/Task/AddTaskDialog"; export const Column = () => { diff --git a/src/modules/projects/components/Column/ColumnsList.tsx b/src/modules/projects/components/Column/_layout.tsx similarity index 65% rename from src/modules/projects/components/Column/ColumnsList.tsx rename to src/modules/projects/components/Column/_layout.tsx index 3f1e3d0..3f4bb91 100644 --- a/src/modules/projects/components/Column/ColumnsList.tsx +++ b/src/modules/projects/components/Column/_layout.tsx @@ -1,5 +1,5 @@ import type { PropsWithChildren } from "react"; -export const ColumnsList = ({ children }: PropsWithChildren) => { +export const ColumnsLayout = ({ children }: PropsWithChildren) => { return
{children}
; }; diff --git a/src/modules/projects/components/DeleteActionBtn.tsx b/src/modules/projects/components/DeleteActionBtn.tsx new file mode 100644 index 0000000..1c2de1f --- /dev/null +++ b/src/modules/projects/components/DeleteActionBtn.tsx @@ -0,0 +1,17 @@ +import { TrashIcon } from "@primer/octicons-react"; + +interface Props extends React.ButtonHTMLAttributes { + text: string; +} + +export const DeleteActionBtn = ({ text, ...props }: Props) => { + return ( + + ); +}; diff --git a/src/modules/projects/components/DeleteColumnDialog.tsx b/src/modules/projects/components/DeleteColumnDialog.tsx new file mode 100644 index 0000000..7b664e1 --- /dev/null +++ b/src/modules/projects/components/DeleteColumnDialog.tsx @@ -0,0 +1,68 @@ +"use client"; +import { useTransition } from "react"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { SubmitButton } from "@/components/ui/submit-button"; +import { Button } from "@/components/ui/button"; +import { useActionDialog } from "../stores/useActionDialog"; +import { removeColumnAction } from "@/services/actions/remove-column"; + +type Props = { + projectId: string; + columnId: string; +}; + +export const DeleteColumnDialog = ({ projectId, columnId }: Props) => { + const { dialogs, setOpen } = useActionDialog(); + const [isPending, startTransition] = useTransition(); + + const handleDelete = (e: React.FormEvent) => { + e.preventDefault(); + + startTransition(async () => { + await removeColumnAction({ columnId, projectId }); + + setOpen("column", false); + }); + }; + + return ( + setOpen("column", false)} + > + + + Delete column + + +

+ This column content items. Are you sure you want to delete this + column? +

+
+ + +
+ + + Delete + + +
+
+
+ ); +}; diff --git a/src/modules/projects/components/PopoverAction.tsx b/src/modules/projects/components/PopoverAction.tsx new file mode 100644 index 0000000..a9814b3 --- /dev/null +++ b/src/modules/projects/components/PopoverAction.tsx @@ -0,0 +1,36 @@ +"use client"; + +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { PopoverKebabBtn } from "@/components/ui/popover-kebab-btn"; + +type PopoverActionProps = { + children: React.ReactNode; +}; + +type PopoverTitleProps = { + title: string; +}; + +export const PopoverTitle = ({ title }: PopoverTitleProps) => { + return

{title}

; +}; + +export const PopoverAction = ({ children }: PopoverActionProps) => { + return ( + + + + + + {children} + + + ); +}; diff --git a/src/modules/projects/components/ProjectsNavigation.tsx b/src/modules/projects/components/ProjectsNavigation.tsx new file mode 100644 index 0000000..8bb2b39 --- /dev/null +++ b/src/modules/projects/components/ProjectsNavigation.tsx @@ -0,0 +1,8 @@ +import { useQueryState } from "nuqs"; +import React from "react"; + +export const ProjectsNavigation = () => { + const [isClosed, setIsClosed] = useQueryState("isClosed"); + + return
ProjectsNavigation
; +}; diff --git a/src/modules/projects/components/Column/ColumnHeader/ColumnAction/DeleteColumn.tsx b/src/modules/projects/components/Task/TaskAction/DeleteTask.tsx similarity index 92% rename from src/modules/projects/components/Column/ColumnHeader/ColumnAction/DeleteColumn.tsx rename to src/modules/projects/components/Task/TaskAction/DeleteTask.tsx index 7149dbe..be985af 100644 --- a/src/modules/projects/components/Column/ColumnHeader/ColumnAction/DeleteColumn.tsx +++ b/src/modules/projects/components/Task/TaskAction/DeleteTask.tsx @@ -12,7 +12,7 @@ type Props = { tasks: Task[]; }; -export const DeleteColumn = ({ columnId, tasks }: Props) => { +export const DeleteTask = ({ columnId, tasks }: Props) => { const { project } = useProjectsContext(); const [isPending, startTransition] = useTransition(); @@ -29,7 +29,7 @@ export const DeleteColumn = ({ columnId, tasks }: Props) => { triggerChildren={ } > diff --git a/src/modules/projects/components/Task/TaskAction/TaskCardAction.tsx b/src/modules/projects/components/Task/TaskAction/TaskCardAction.tsx new file mode 100644 index 0000000..cd08713 --- /dev/null +++ b/src/modules/projects/components/Task/TaskAction/TaskCardAction.tsx @@ -0,0 +1,9 @@ +import { PopoverAction } from "@/modules/projects/components/PopoverAction"; + +export const TaskCardAction = () => { + return ( + +

action

+
+ ); +}; diff --git a/src/modules/projects/components/Task/TaskCard/TaskCardDescription.tsx b/src/modules/projects/components/Task/TaskCard/TaskCardDescription.tsx new file mode 100644 index 0000000..42c99b5 --- /dev/null +++ b/src/modules/projects/components/Task/TaskCard/TaskCardDescription.tsx @@ -0,0 +1,7 @@ +type Props = { + description: string; +}; + +export const TaskCardDescription = ({ description }: Props) => { + return

{description}

; +}; diff --git a/src/modules/projects/components/Task/TaskCard/_index.tsx b/src/modules/projects/components/Task/TaskCard/_index.tsx new file mode 100644 index 0000000..ec7ed23 --- /dev/null +++ b/src/modules/projects/components/Task/TaskCard/_index.tsx @@ -0,0 +1,20 @@ +import { TaskCardLayout } from "./_layout"; +import { TaskCardAction } from "../TaskAction/TaskCardAction"; +import { TaskCardDescription } from "./TaskCardDescription"; +import type { Task } from "@prisma/client"; + +type Props = { + task: Task; +}; + +export const TaskCard = ({ task }: Props) => { + const { name } = task; + + return ( + + + + + + ); +}; diff --git a/src/modules/projects/components/Task/TaskCard/_layout.tsx b/src/modules/projects/components/Task/TaskCard/_layout.tsx new file mode 100644 index 0000000..fd1cb7c --- /dev/null +++ b/src/modules/projects/components/Task/TaskCard/_layout.tsx @@ -0,0 +1,9 @@ +import type { PropsWithChildren } from "react"; + +export const TaskCardLayout = ({ children }: PropsWithChildren) => { + return ( +
+ {children} +
+ ); +}; diff --git a/src/modules/projects/components/Task/TasksList.tsx b/src/modules/projects/components/Task/TasksList.tsx deleted file mode 100644 index 9fbafce..0000000 --- a/src/modules/projects/components/Task/TasksList.tsx +++ /dev/null @@ -1,19 +0,0 @@ -"use client"; -import { useColumnContext } from "@/modules/projects/context/columnContext"; - -export const TasksList = () => { - const { - column: { tasks }, - } = useColumnContext(); - - return ( -
- {tasks.map((task) => ( -
-

{task.name}

-

{task.description}

-
- ))} -
- ); -}; diff --git a/src/modules/projects/components/Task/_index.tsx b/src/modules/projects/components/Task/_index.tsx new file mode 100644 index 0000000..e57e9a5 --- /dev/null +++ b/src/modules/projects/components/Task/_index.tsx @@ -0,0 +1,18 @@ +"use client"; +import { useColumnContext } from "@/modules/projects/context/columnContext"; +import { TaskCard } from "./TaskCard/_index"; +import { TaskListLayout } from "./_layout"; + +export const TasksList = () => { + const { + column: { tasks }, + } = useColumnContext(); + + return ( + + {tasks.map((task) => ( + + ))} + + ); +}; diff --git a/src/modules/projects/components/Task/_layout.tsx b/src/modules/projects/components/Task/_layout.tsx new file mode 100644 index 0000000..b59b47d --- /dev/null +++ b/src/modules/projects/components/Task/_layout.tsx @@ -0,0 +1,9 @@ +import type { PropsWithChildren } from "react"; + +export const TaskListLayout = ({ children }: PropsWithChildren) => { + return ( +
+ {children} +
+ ); +}; diff --git a/src/modules/projects/components/_dialogs/project-dialogs.tsx b/src/modules/projects/components/_dialogs/project-dialogs.tsx new file mode 100644 index 0000000..168efc8 --- /dev/null +++ b/src/modules/projects/components/_dialogs/project-dialogs.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { useState } from "react"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { PopoverKebabBtn } from "@/components/ui/popover-kebab-btn"; +import { CloseProjectForm } from "../_forms/close-project-form"; + +type Props = { + projectId: string; +}; + +export const ProjectDialogs = ({ projectId }: Props) => { + const [open, setOpen] = useState(false); + + return ( + + + + + + + + + ); +}; diff --git a/src/modules/projects/components/_forms/add-task-form.tsx b/src/modules/projects/components/_forms/add-task-form.tsx index 21da920..47ab093 100644 --- a/src/modules/projects/components/_forms/add-task-form.tsx +++ b/src/modules/projects/components/_forms/add-task-form.tsx @@ -29,9 +29,9 @@ export const AddTaskForm = () => { columnId: column.id, projectId: project.id, }); - }); - form.reset(); + form.reset(); + }); } return ( diff --git a/src/modules/projects/components/_forms/close-project-form.tsx b/src/modules/projects/components/_forms/close-project-form.tsx new file mode 100644 index 0000000..f53cd61 --- /dev/null +++ b/src/modules/projects/components/_forms/close-project-form.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { closeProjectAction } from "@/services/actions/close-project"; +import { useTransition } from "react"; +import { DeleteActionBtn } from "../DeleteActionBtn"; + +type Props = { + projectId: string; + setOpen: (open: boolean) => void; +}; + +export const CloseProjectForm = ({ projectId, setOpen }: Props) => { + const [isPending, startTransition] = useTransition(); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + startTransition(async () => { + await closeProjectAction({ projectId }); + setOpen(false); + }); + }; + + return ( +
+ + + ); +}; diff --git a/src/modules/projects/components/_tables/projects-columns.tsx b/src/modules/projects/components/_tables/projects-columns.tsx index 1be5dc1..45f8dbc 100644 --- a/src/modules/projects/components/_tables/projects-columns.tsx +++ b/src/modules/projects/components/_tables/projects-columns.tsx @@ -2,14 +2,31 @@ import type { Project } from "@prisma/client"; import type { ColumnDef } from "@tanstack/react-table"; +import { ProjectDialogs } from "@/modules/projects/components/_dialogs/project-dialogs"; +import { CustomLink } from "@/components/ui/link"; -export const projectColumns: ColumnDef[] = [ +export const projectColumns: ColumnDef[] = [ { accessorKey: "name", header: "Name", + cell: (props) => { + return ( + + {props.row.original.name} + + ); + }, }, { accessorKey: "description", header: "Description", }, + { + accessorKey: "deleteProject", + header: "Delete", + + cell: (props) => { + return ; + }, + }, ]; diff --git a/src/modules/projects/components/_tables/projects-table.tsx b/src/modules/projects/components/_tables/projects-table.tsx index 2dea827..d824df7 100644 --- a/src/modules/projects/components/_tables/projects-table.tsx +++ b/src/modules/projects/components/_tables/projects-table.tsx @@ -9,11 +9,6 @@ type Props = { export const ProjectsTable = ({ projects }: Props) => { return ( - + ); }; diff --git a/src/modules/projects/hooks/use-projects-params.tsx b/src/modules/projects/hooks/use-projects-params.tsx new file mode 100644 index 0000000..9468e3e --- /dev/null +++ b/src/modules/projects/hooks/use-projects-params.tsx @@ -0,0 +1,10 @@ +import { parseAsString, useQueryStates } from "nuqs"; + +export const useProjectParams = () => { + const [params, setParams] = useQueryStates({ + columnId: parseAsString.withDefault(""), + projectId: parseAsString.withDefault(""), + }); + + return { params, setParams }; +}; diff --git a/src/modules/projects/hooks/use-query-parser-project-page.tsx b/src/modules/projects/hooks/use-query-parser-project-page.tsx new file mode 100644 index 0000000..680b73d --- /dev/null +++ b/src/modules/projects/hooks/use-query-parser-project-page.tsx @@ -0,0 +1,18 @@ +import { parseAsString } from "nuqs/server"; + +export type SearchParamsType = { + searchParams?: { + projectId: string; + columnId: string; + }; +}; + +export const useQueryParserProjectPage = ({ + searchParams, +}: SearchParamsType) => { + const queryParser = parseAsString.withDefault(""); + const projectId = queryParser.parseServerSide(searchParams?.projectId); + const columnId = queryParser.parseServerSide(searchParams?.columnId); + + return { projectId, columnId }; +}; diff --git a/src/modules/projects/stores/useActionDialog.ts b/src/modules/projects/stores/useActionDialog.ts new file mode 100644 index 0000000..efb9b6b --- /dev/null +++ b/src/modules/projects/stores/useActionDialog.ts @@ -0,0 +1,17 @@ +import { create } from "zustand"; + +type DialogKey = "column"; + +interface ActionDialogState { + dialogs: Record; + setOpen: (key: DialogKey, open: boolean) => void; +} + +export const useActionDialog = create((set) => ({ + dialogs: { + column: false, + }, + + setOpen: (key, open) => + set((state) => ({ dialogs: { ...state.dialogs, [key]: open } })), +})); diff --git a/src/services/actions/close-project.ts b/src/services/actions/close-project.ts new file mode 100644 index 0000000..aafb0d5 --- /dev/null +++ b/src/services/actions/close-project.ts @@ -0,0 +1,24 @@ +"use server"; +import { revalidatePath } from "next/cache"; +import { userAction } from "@/config/lib/next-safe-action"; +import projectService from "../project.service"; +import { URL } from "@/config/constants"; +import * as z from "zod"; + +const schema = z.object({ + projectId: z.string(), +}); + +export const closeProjectAction = userAction(schema, async (data, ctx) => { + try { + await projectService.closeProject({ + projectId: data.projectId, + }); + } catch (error) { + if (error instanceof Error) { + return { error: error.message }; + } + } + + revalidatePath(URL.PROJECTS); +}); diff --git a/src/services/project.service.ts b/src/services/project.service.ts index 9fb8e7f..b2ae133 100644 --- a/src/services/project.service.ts +++ b/src/services/project.service.ts @@ -1,5 +1,6 @@ import { db } from "@/config/server/db"; import type { + CloseProjectType, CreateProject, CreateTaskType, DeleteColumn, @@ -115,6 +116,24 @@ class ProjectService { } } + async closeProject({ projectId }: CloseProjectType) { + try { + await db.project.update({ + where: { + id: projectId, + }, + data: { + closed: true, + closedAt: new Date(), + }, + }); + } catch (error) { + if (error instanceof Error) { + console.log(error.message); + } + } + } + /** * Query to delete a column. * @param {DeleteColumn} - The column id. diff --git a/src/services/types/project.type.ts b/src/services/types/project.type.ts index e230fdf..d288b4a 100644 --- a/src/services/types/project.type.ts +++ b/src/services/types/project.type.ts @@ -13,6 +13,10 @@ type CreateProject = { description: string; }; +type CloseProjectType = { + projectId: string; +}; + type DeleteColumn = { columnId: string; }; @@ -30,4 +34,5 @@ export type { CreateProject, DeleteColumn, CreateTaskType, + CloseProjectType, }; diff --git a/tsconfig.json b/tsconfig.json index 54b27fb..c5eef6e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -36,8 +36,7 @@ "**/*.tsx", "**/*.cjs", "**/*.js", - ".next/types/**/*.ts", - "src/modules/repositories/components/_forms/update-user-role-formtsx" + ".next/types/**/*.ts" ], "exclude": ["node_modules"] }