├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── actions.ts ├── components │ ├── AssignTask.tsx │ ├── AuthWrapper.tsx │ ├── EmptyState.tsx │ ├── Navbar.tsx │ ├── ProjectComponent.tsx │ ├── TaskComponent.tsx │ ├── UserInfo.tsx │ └── Wrapper.tsx ├── favicon.ico ├── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── general-projects │ └── page.tsx ├── globals.css ├── layout.tsx ├── new-tasks │ └── [projectId] │ │ └── page.tsx ├── page.tsx ├── project │ └── [projectId] │ │ └── page.tsx ├── sign-in │ └── [[...sign-in]] │ │ └── page.tsx ├── sign-up │ └── [[...sign-up]] │ │ └── page.tsx └── task-details │ └── [taskId] │ └── page.tsx ├── lib └── prisma.ts ├── middleware.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma ├── dev.db ├── migrations │ ├── 20241117141302_add_models │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public ├── empty-project.png ├── empty-task.png └── profile.avif ├── tailwind.config.ts ├── tsconfig.json └── type.ts /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Node.js dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # Testing 10 | /coverage 11 | 12 | # Next.js 13 | /.next/ 14 | /out/ 15 | 16 | # Production 17 | /build 18 | 19 | # Miscellaneous 20 | .DS_Store 21 | *.pem 22 | 23 | # Debug logs 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Local environment files 29 | .env*.local 30 | .env 31 | 32 | # Vercel 33 | .vercel 34 | 35 | # TypeScript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # Prisma 40 | prisma/fyb.db 41 | 42 | # Media files (for now) 43 | public/vid1.mp4 44 | public/vid2.mp4 45 | public/vid3.mp4 46 | public/vid4.mp4 47 | 48 | env/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Application de Gestion de Projet (Taskflow) 3 | 4 | ❤️ SOUTIENS-MOI POUR DES TUTOS DE QUALITÉ (Seulement 2 €/mois) : https://fr.tipeee.com/faizdev/ 5 | 6 | Pour un guide pas à pas, consultez la vidéo complète du tutoriel sur YouTube : 7 | [Regarder le tuto complet](https://youtu.be/upvBeVBPEM8) 8 | 9 | ![Copie de Copie de Copie de Top 10 des mythes Dark web](https://github.com/user-attachments/assets/395965da-a366-4b0c-af59-397c930b5ebe) 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import prisma from "@/lib/prisma"; 4 | import { randomBytes } from "crypto"; 5 | 6 | export async function checkAndAddUser(email: string, name: string) { 7 | if (!email) return 8 | try { 9 | const existingUser = await prisma.user.findUnique({ 10 | where: { 11 | email: email 12 | } 13 | }) 14 | if (!existingUser && name) { 15 | await prisma.user.create({ 16 | data: { 17 | email, 18 | name 19 | } 20 | }) 21 | console.error("Erreur lors de la vérification de l'utilisateur:"); 22 | } else { 23 | console.error("Utilisateur déjà présent dans la base de données"); 24 | } 25 | } catch (error) { 26 | console.error("Erreur lors de la vérification de l'utilisateur:", error); 27 | } 28 | } 29 | 30 | function generateUniqueCode(): string { 31 | return randomBytes(6).toString('hex') 32 | } 33 | 34 | export async function createProject(name: string, description: string, email: string) { 35 | try { 36 | 37 | const inviteCode = generateUniqueCode() 38 | const user = await prisma.user.findUnique({ 39 | where: { 40 | email 41 | } 42 | }) 43 | if (!user) { 44 | throw new Error('User not found'); 45 | } 46 | 47 | const newProject = await prisma.project.create({ 48 | data: { 49 | name, 50 | description, 51 | inviteCode, 52 | createdById: user.id 53 | } 54 | }) 55 | return newProject; 56 | } catch (error) { 57 | console.error(error) 58 | throw new Error 59 | } 60 | } 61 | 62 | export async function getProjectsCreatedByUser(email: string) { 63 | try { 64 | 65 | const projects = await prisma.project.findMany({ 66 | where: { 67 | createdBy: { email } 68 | }, 69 | include: { 70 | tasks: { 71 | include: { 72 | user: true, 73 | createdBy: true 74 | } 75 | }, 76 | users: { 77 | select: { 78 | user: { 79 | select: { 80 | id: true, 81 | name: true, 82 | email: true 83 | } 84 | } 85 | } 86 | } 87 | } 88 | }) 89 | 90 | const formattedProjects = projects.map((project) => ({ 91 | ...project, 92 | users: project.users.map((userEntry) => userEntry.user) 93 | })) 94 | 95 | return formattedProjects 96 | 97 | } catch (error) { 98 | console.error(error) 99 | throw new Error 100 | } 101 | } 102 | 103 | 104 | export async function deleteProjectById(projectId: string) { 105 | try { 106 | await prisma.project.delete({ 107 | where: { 108 | id: projectId 109 | } 110 | }) 111 | console.log(`Projet avec l'ID ${projectId} supprimé avec succès.`); 112 | } catch (error) { 113 | console.error(error) 114 | throw new Error 115 | } 116 | } 117 | 118 | export async function addUserToProject(email: string, inviteCode: string) { 119 | try { 120 | 121 | const project = await prisma.project.findUnique({ 122 | where: { inviteCode } 123 | }) 124 | 125 | if (!project) { 126 | throw new Error('Projet non trouvé'); 127 | } 128 | 129 | const user = await prisma.user.findUnique({ 130 | where: { email } 131 | }) 132 | 133 | if (!user) { 134 | throw new Error('Utilisateur non trouvé'); 135 | } 136 | 137 | const existingAssociation = await prisma.projectUser.findUnique({ 138 | where: { 139 | userId_projectId: { 140 | userId: user.id, 141 | projectId: project.id 142 | } 143 | } 144 | }) 145 | 146 | if (existingAssociation) { 147 | throw new Error('Utilisateur déjà associé à ce projet'); 148 | } 149 | 150 | await prisma.projectUser.create({ 151 | data: { 152 | userId: user.id, 153 | projectId: project.id 154 | } 155 | }) 156 | return 'Utilisateur ajouté au projet avec succès'; 157 | } catch (error) { 158 | console.error(error) 159 | throw new Error 160 | } 161 | } 162 | 163 | export async function getProjectsAssociatedWithUser(email: string) { 164 | try { 165 | 166 | const projects = await prisma.project.findMany({ 167 | where: { 168 | users: { 169 | some: { 170 | user: { 171 | email 172 | } 173 | } 174 | } 175 | }, 176 | include: { 177 | tasks: true, 178 | users: { 179 | select: { 180 | user: { 181 | select: { 182 | id: true, 183 | name: true, 184 | email: true, 185 | } 186 | } 187 | } 188 | } 189 | } 190 | 191 | }) 192 | 193 | const formattedProjects = projects.map((project) => ({ 194 | ...project, 195 | users: project.users.map((userEntry) => userEntry.user) 196 | })) 197 | 198 | return formattedProjects 199 | 200 | } catch (error) { 201 | console.error(error) 202 | throw new Error 203 | } 204 | } 205 | 206 | export async function getProjectInfo(idProject: string, details: boolean) { 207 | try { 208 | const project = await prisma.project.findUnique({ 209 | where: { 210 | id: idProject 211 | }, 212 | include: details ? { 213 | tasks: { 214 | include: { 215 | user: true, 216 | createdBy: true 217 | } 218 | }, 219 | users: { 220 | select: { 221 | user: { 222 | select: { 223 | id: true, 224 | name: true, 225 | email: true, 226 | } 227 | } 228 | } 229 | }, 230 | createdBy: true 231 | } : undefined, 232 | }) 233 | 234 | if (!project) { 235 | throw new Error('Projet non trouvé'); 236 | } 237 | 238 | return project 239 | } catch (error) { 240 | console.error(error) 241 | throw new Error 242 | } 243 | } 244 | 245 | export async function getProjectUsers(idProject: string) { 246 | try { 247 | const projectWithUsers = await prisma.project.findUnique({ 248 | where: { 249 | id: idProject 250 | }, 251 | include: { 252 | users: { 253 | include: { 254 | user: true, 255 | } 256 | }, 257 | } 258 | 259 | }) 260 | 261 | const users = projectWithUsers?.users.map((projectUser => projectUser.user)) || [] 262 | return users 263 | 264 | } catch (error) { 265 | console.error(error) 266 | throw new Error 267 | } 268 | } 269 | 270 | export async function createTask( 271 | name: string, 272 | description: string, 273 | dueDate: Date | null, 274 | projectId: string, 275 | createdByEmail: string, 276 | assignToEmail: string | undefined 277 | ) { 278 | 279 | try { 280 | const createdBy = await prisma.user.findUnique({ 281 | where: { email: createdByEmail } 282 | }) 283 | 284 | if (!createdBy) { 285 | throw new Error(`Utilisateur avec l'email ${createdByEmail} introuvable`); 286 | } 287 | 288 | let assignedUserId = createdBy.id 289 | 290 | if (assignToEmail) { 291 | const assignedUser = await prisma.user.findUnique({ 292 | where: { email: assignToEmail } 293 | }) 294 | if (!assignedUser) { 295 | throw new Error(`Utilisateur avec l'email ${assignToEmail} introuvable`); 296 | } 297 | assignedUserId = assignedUser.id 298 | } 299 | 300 | const newTask = await prisma.task.create({ 301 | data: { 302 | name, 303 | description, 304 | dueDate, 305 | projectId, 306 | createdById: createdBy.id, 307 | userId: assignedUserId 308 | } 309 | }) 310 | 311 | console.log('Tâche créée avec succès:', newTask); 312 | return newTask; 313 | } catch (error) { 314 | console.error(error) 315 | throw new Error 316 | } 317 | 318 | } 319 | export async function deleteTaskById(taskId: string) { 320 | try { 321 | await prisma.task.delete({ 322 | where: { 323 | id: taskId 324 | } 325 | }) 326 | } catch (error) { 327 | console.error(error) 328 | throw new Error 329 | } 330 | } 331 | 332 | export const getTaskDetails = async (taskId: string) => { 333 | try { 334 | const task = await prisma.task.findUnique({ 335 | where: { id: taskId }, 336 | include: { 337 | project: true, 338 | user: true, 339 | createdBy: true 340 | } 341 | }) 342 | if (!task) { 343 | throw new Error('Tâche non trouvée'); 344 | } 345 | 346 | return task 347 | 348 | } catch (error) { 349 | console.error(error) 350 | throw new Error 351 | } 352 | } 353 | 354 | export const updateTaskStatus = async (taskId: string, newStatus: string, solutionDescription?: string) => { 355 | try { 356 | 357 | const existingTask = await prisma.task.findUnique({ 358 | where: { 359 | id: taskId 360 | } 361 | }) 362 | 363 | if (!existingTask) { 364 | throw new Error('Tâche non trouvée'); 365 | } 366 | 367 | if (newStatus === "Done" && solutionDescription) { 368 | await prisma.task.update({ 369 | where: { id: taskId }, 370 | data: { 371 | status: newStatus, 372 | solutionDescription 373 | } 374 | }) 375 | } else { 376 | await prisma.task.update({ 377 | where: { id: taskId }, 378 | data: { 379 | status: newStatus 380 | } 381 | }) 382 | } 383 | } catch (error) { 384 | console.error(error) 385 | throw new Error 386 | } 387 | } 388 | 389 | 390 | -------------------------------------------------------------------------------- /app/components/AssignTask.tsx: -------------------------------------------------------------------------------- 1 | import { User } from '@prisma/client' 2 | import React, { FC, use, useState } from 'react' 3 | import UserInfo from './UserInfo'; 4 | 5 | interface AssignTaskProps { 6 | users: User[]; 7 | projectId: string; 8 | onAssignTask : (user : User) => void; 9 | } 10 | 11 | const AssignTask: FC = ({ users, projectId , onAssignTask }) => { 12 | 13 | const [selectedUser, setSelectedUser] = useState(null) 14 | 15 | const handleAssign = (user: User) => { 16 | setSelectedUser(user) 17 | onAssignTask(user) 18 | const modal = document.getElementById('my_modal_3') as HTMLDialogElement 19 | if (modal) { 20 | modal.close() 21 | } 22 | } 23 | 24 | return ( 25 |
26 | {/* You can open the modal using document.getElementById('ID').showModal() method */} 27 |
(document.getElementById('my_modal_3') as HTMLDialogElement).showModal()}> 29 | 34 |
35 | 36 | 37 |
38 |
39 | {/* if there is a button in form, it will close the modal */} 40 | 41 |
42 |

Choissisez un collaborateur

43 |
44 | {users.map((user) => ( 45 |
handleAssign(user)} 47 | className='cursor-pointer border border-base-300 p-5 rounded-xl w-full mb-3' 48 | key={user.id}> 49 | 54 |
55 | ))} 56 |
57 |
58 |
59 |
60 | ) 61 | } 62 | 63 | export default AssignTask -------------------------------------------------------------------------------- /app/components/AuthWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { FolderGit2 } from 'lucide-react' 2 | import React from 'react' 3 | type WrapperProps = { 4 | children : React.ReactNode 5 | } 6 | 7 | const AuthWrapper = ({children} : WrapperProps ) => { 8 | return ( 9 |
10 |
11 | 12 |
13 | 14 |
15 | 16 | Task Flow 17 | 18 | 19 |
20 | 21 |
22 | {children} 23 |
24 |
25 | ) 26 | } 27 | 28 | export default AuthWrapper -------------------------------------------------------------------------------- /app/components/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import Image from 'next/image' 3 | interface EmptyStateProps { 4 | imageSrc: string; 5 | imageAlt: string; 6 | message: string; 7 | } 8 | const EmptyState: FC = ({ imageSrc, imageAlt, message }) => { 9 | return ( 10 |
11 | {imageAlt} 18 |

{message}

19 |
20 | ) 21 | } 22 | 23 | export default EmptyState -------------------------------------------------------------------------------- /app/components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { UserButton, useUser } from '@clerk/nextjs' 3 | import { FolderGit2, Menu, X } from 'lucide-react' 4 | import Link from 'next/link' 5 | import { usePathname } from 'next/navigation' 6 | 7 | import React, { useEffect, useState } from 'react' 8 | import { checkAndAddUser } from '../actions' 9 | 10 | const Navbar = () => { 11 | const { user } = useUser() 12 | const [menuOpen, setMenuOpen] = useState(false) 13 | const pathname = usePathname() 14 | 15 | const navLinks = [ 16 | { 17 | href: "/general-projects", label: "Collaborations", 18 | }, 19 | { 20 | href: "/", label: "Mes projets" 21 | } 22 | ] 23 | 24 | useEffect(() => { 25 | if (user?.primaryEmailAddress?.emailAddress && user?.fullName) { 26 | checkAndAddUser(user?.primaryEmailAddress?.emailAddress ,user?.fullName ) 27 | } 28 | }, [user]) 29 | 30 | const isActiveLink = (href: string) => 31 | pathname.replace(/\/$/, "") === href.replace(/\/$/, ""); 32 | 33 | const renderLinks = (classNames: string) => 34 | navLinks.map(({ href, label }) => { 35 | return 36 | {label} 37 | 38 | }) 39 | 40 | return ( 41 |
42 |
43 |
44 |
45 | 46 |
47 | 48 | Task Flow 49 | 50 |
51 | 52 | 55 | 56 |
57 | {renderLinks("btn")} 58 | 59 |
60 |
61 | 62 |
63 |
64 | 65 | 68 |
69 | {renderLinks("btn")} 70 |
71 |
72 | ) 73 | } 74 | 75 | export default Navbar -------------------------------------------------------------------------------- /app/components/ProjectComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Project } from '@/type' 2 | import { Copy, ExternalLink, FolderGit2, Trash } from 'lucide-react'; 3 | import Link from 'next/link'; 4 | import React, { FC } from 'react' 5 | import { toast } from 'react-toastify'; 6 | 7 | interface ProjectProps { 8 | project: Project 9 | admin: number; 10 | style: boolean; 11 | onDelete?: (id: string) => void; 12 | 13 | } 14 | 15 | const ProjectComponent: FC = ({ project, admin, style, onDelete }) => { 16 | 17 | const handleDeleteClick = () => { 18 | const isConfirmed = window.confirm("Êtes-vous sûr de vouloir supprimer ce projet ?") 19 | if (isConfirmed && onDelete) { 20 | onDelete(project.id) 21 | } 22 | } 23 | 24 | const totalTasks = project.tasks?.length; 25 | const tasksByStatus = project.tasks?.reduce( 26 | (acc, task) => { 27 | if (task.status === "To Do") acc.toDo++; 28 | else if (task.status === "In Progress") acc.inProgress++; 29 | else if (task.status === "Done") acc.done++; 30 | return acc 31 | }, 32 | { 33 | toDo: 0, inProgress: 0, done: 0 34 | } 35 | ) ?? { toDo: 0, inProgress: 0, done: 0 } 36 | 37 | const progressPercentage = totalTasks ? Math.round((tasksByStatus.done / totalTasks) * 100) : 0 38 | const inProgressPercentage = totalTasks ? Math.round((tasksByStatus.inProgress / totalTasks) * 100) : 0 39 | const toDoPercentage = totalTasks ? Math.round((tasksByStatus.toDo / totalTasks) * 100) : 0 40 | 41 | const textSizeClass = style ? 'text-sm' : 'text-md' 42 | 43 | const handleCopyCode = async () => { 44 | try { 45 | if (project.inviteCode) { 46 | await navigator.clipboard.writeText(project.inviteCode) 47 | toast.success("Code d'invitation copié") 48 | } 49 | } catch (error) { 50 | toast.error("Erreur lors de la copie du code d'invitation.") 51 | } 52 | } 53 | 54 | return ( 55 |
56 | 57 |
58 |
59 | 60 |
61 |
62 | {project.name} 63 |
64 |
65 | 66 | {style == false && ( 67 |

68 | {project.description} 69 |

70 | )} 71 | 72 |
73 | Collaborateurs 74 |
{project.users?.length}
75 |
76 | 77 | {admin === 1 && ( 78 |
79 |

80 | {project.inviteCode} 81 |

82 | 85 |
86 | )} 87 | 88 |
89 |

90 | A faire 91 |
92 | {tasksByStatus.toDo} 93 |
94 |

95 | 96 | 97 |
98 | 99 | {toDoPercentage}% 100 | 101 |
102 |
103 | 104 |
105 |

106 | En cours 107 |
108 | {tasksByStatus.inProgress} 109 |
110 |

111 | 112 | 113 |
114 | 115 | {inProgressPercentage}% 116 | 117 |
118 |
119 | 120 |
121 |

122 | Terminée(s) 123 |
124 | {tasksByStatus.done} 125 |
126 |

127 | 128 | 129 |
130 | 131 | {progressPercentage}% 132 | 133 |
134 |
135 | 136 |
137 | 138 | {style && ( 139 | 140 |
141 | {totalTasks} 142 |
143 | Tâche 144 | 145 | 146 | 147 | )} 148 | 149 | {admin === 1 && ( 150 | 153 | )} 154 |
155 | 156 | 157 |
158 | ) 159 | } 160 | 161 | export default ProjectComponent -------------------------------------------------------------------------------- /app/components/TaskComponent.tsx: -------------------------------------------------------------------------------- 1 | import { Task } from '@/type' 2 | import React, { FC } from 'react' 3 | import UserInfo from './UserInfo' 4 | import Link from 'next/link' 5 | import { ArrowRight, Trash } from 'lucide-react' 6 | 7 | 8 | interface TaskProps { 9 | task: Task, 10 | index: number, 11 | email?: string, 12 | onDelete? : (id: string) => void 13 | } 14 | 15 | const TaskComponent: FC = ({ task, index, email , onDelete }) => { 16 | const canDelete = email == task.createdBy?.email 17 | 18 | const handleDeleteClick = () => { 19 | if(onDelete){ 20 | onDelete(task.id) 21 | } 22 | } 23 | 24 | return ( 25 | <> 26 | {index + 1} 27 | 28 |
29 |
34 | 35 | {task.status == "To Do" && 'A faire'} 36 | {task.status == "In Progress" && 'En cours'} 37 | {task.status == "Done" && 'Terminé'} 38 |
39 | 40 | {task.name.length > 100 ? `${task.name.slice(0, 100)}...` : task.name} 41 | 42 |
43 | 44 | 45 | 46 | 51 | 52 | 53 | 54 |
55 | {task.dueDate && new Date(task.dueDate).toLocaleDateString()} 56 |
57 | 58 | 59 | 60 |
61 | 62 | Plus 63 | 64 | 65 | {canDelete && ( 66 | 69 | )} 70 |
71 | 72 | 73 | ) 74 | } 75 | 76 | export default TaskComponent -------------------------------------------------------------------------------- /app/components/UserInfo.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react' 2 | import Image from 'next/image' 3 | 4 | interface UserInfoProps { 5 | role: string 6 | email: string | null 7 | name: string | null 8 | } 9 | 10 | const UserInfo: FC = ({ role, email, name }) => { 11 | return ( 12 |
13 |
14 |
15 | {'profile 21 |
22 |
23 |
24 | {role} 25 | {email || ""} 26 | {name || ""} 27 |
28 |
29 | ) 30 | } 31 | 32 | export default UserInfo -------------------------------------------------------------------------------- /app/components/Wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { FolderGit2 } from 'lucide-react' 2 | import React from 'react' 3 | import Navbar from './Navbar' 4 | import { ToastContainer } from 'react-toastify' 5 | import 'react-toastify/dist/ReactToastify.css' 6 | 7 | type WrapperProps = { 8 | children: React.ReactNode 9 | } 10 | 11 | const Wrapper = ({ children }: WrapperProps) => { 12 | return ( 13 |
14 | 15 |
16 | 25 | {children} 26 |
27 |
28 | ) 29 | } 30 | 31 | export default Wrapper -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadikou-faiz/taskflow/a860d8dea01692c9b2c8329c39bde3262cbc1060/app/favicon.ico -------------------------------------------------------------------------------- /app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadikou-faiz/taskflow/a860d8dea01692c9b2c8329c39bde3262cbc1060/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadikou-faiz/taskflow/a860d8dea01692c9b2c8329c39bde3262cbc1060/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/general-projects/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import React, { useEffect, useState } from 'react' 3 | import Wrapper from '../components/Wrapper' 4 | import { SquarePlus } from 'lucide-react' 5 | import { toast } from 'react-toastify' 6 | import { addUserToProject, getProjectsAssociatedWithUser } from '../actions' 7 | import { useUser } from '@clerk/nextjs' 8 | import { Project } from '@/type' 9 | import ProjectComponent from '../components/ProjectComponent' 10 | import EmptyState from '../components/EmptyState' 11 | 12 | const page = () => { 13 | const { user } = useUser() 14 | const email = user?.primaryEmailAddress?.emailAddress as string 15 | const [inviteCode, setInviteCode] = useState("") 16 | const [associatedProjects, setAssociatedProjects] = useState([]) 17 | 18 | const fetchProjects = async (email: string) => { 19 | try { 20 | const associated = await getProjectsAssociatedWithUser(email) 21 | setAssociatedProjects(associated) 22 | } catch (error) { 23 | toast.error("Erreur lors du chargement des projets:"); 24 | } 25 | } 26 | 27 | useEffect(() => { 28 | if (email) { 29 | fetchProjects(email) 30 | } 31 | }, [email]) 32 | 33 | const handleSubmit = async () => { 34 | try { 35 | if (inviteCode != "") { 36 | await addUserToProject(email, inviteCode) 37 | fetchProjects(email) 38 | setInviteCode("") 39 | toast.success('Vous pouvez maintenant collaboré sur ce projet'); 40 | } else { 41 | toast.error('Il manque le code du projet'); 42 | } 43 | } catch (error) { 44 | toast.error("Code invalide ou vous appartenez déjà au projet"); 45 | } 46 | } 47 | 48 | return ( 49 | 50 |
51 |
52 | setInviteCode(e.target.value)} 55 | type="text" 56 | placeholder="Code d'invitation" 57 | className='w-full p-2 input input-bordered' 58 | /> 59 |
60 | 63 |
64 | 65 |
66 | {associatedProjects.length > 0 ? ( 67 |
    68 | {associatedProjects.map((project) => ( 69 |
  • 70 | 71 |
  • 72 | ))} 73 |
74 | ) : ( 75 |
76 | 81 |
82 | )} 83 |
84 |
85 | ) 86 | } 87 | 88 | export default page -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .ql-container { 6 | background: #fff; 7 | border-radius: 0 0 4px 4px; 8 | } 9 | 10 | .ql-toolbar { 11 | background: #E5E6E6; 12 | border: 1px solid red; 13 | border-radius: 4px 4px 0 0; 14 | border-top-left-radius: 0.5em; 15 | border-top-right-radius: 0.5em; 16 | border-bottom: none; 17 | } 18 | 19 | .ql-editor { 20 | min-height: 200px; 21 | } 22 | 23 | .ql-editor::selection { 24 | color: #000; 25 | } 26 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | 3 | import "./globals.css"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | 20 | 22 | {children} 23 | 24 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/new-tasks/[projectId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { createTask, getProjectInfo, getProjectUsers } from '@/app/actions'; 3 | import AssignTask from '@/app/components/AssignTask'; 4 | import Wrapper from '@/app/components/Wrapper' 5 | import { Project } from '@/type'; 6 | import { useUser } from '@clerk/nextjs'; 7 | import { User } from '@prisma/client'; 8 | import Link from 'next/link'; 9 | import { useRouter } from 'next/navigation'; 10 | import React, { useEffect, useState } from 'react' 11 | import ReactQuill from 'react-quill-new'; 12 | import 'react-quill-new/dist/quill.snow.css'; 13 | import { toast } from 'react-toastify'; 14 | 15 | const page = ({ params }: { params: Promise<{ projectId: string }> }) => { 16 | 17 | const modules = { 18 | toolbar: [ 19 | [{ 'header': [1, 2, 3, false] }], 20 | ['bold', 'italic', 'underline', 'strike'], 21 | [{ 'font': [] }], 22 | [{ 'list': 'ordered' }, { 'list': 'bullet' }], 23 | [{ 'color': [] }, { 'background': [] }], 24 | ['blockquote', 'code-block'], 25 | ['link', 'image'], 26 | ['clean'] 27 | ] 28 | }; 29 | 30 | const { user } = useUser(); 31 | const email = user?.primaryEmailAddress?.emailAddress as string; 32 | const [projectId, setProjectId] = useState(""); 33 | const [project, setProject] = useState(null); 34 | const [usersProject, setUsersProject] = useState([]); 35 | const [selectedUser, setSelectedUser] = useState(null) 36 | const [dueDate, setDueDate] = useState(null) 37 | const [name, setName] = useState("") 38 | const [description, setDescription] = useState("") 39 | const rooter = useRouter() 40 | 41 | const fetchInfos = async (projectId: string) => { 42 | try { 43 | const project = await getProjectInfo(projectId, true) 44 | setProject(project) 45 | 46 | const associatedUsers = await getProjectUsers(projectId) 47 | setUsersProject(associatedUsers) 48 | 49 | } catch (error) { 50 | console.error('Erreur lors du chargement du projet:', error); 51 | } 52 | } 53 | 54 | useEffect(() => { 55 | const getId = async () => { 56 | const resolvedParams = await params; 57 | setProjectId(resolvedParams.projectId) 58 | fetchInfos(resolvedParams.projectId) 59 | } 60 | getId() 61 | 62 | }, [params]) 63 | 64 | const handleUserSelect = (user: User) => { 65 | setSelectedUser(user) 66 | } 67 | 68 | const handleSubmit = async () => { 69 | if (!name || !projectId || !selectedUser || !description || !dueDate) { 70 | toast.error('Veuillez remplir tous les champs obligatoires') 71 | return 72 | } 73 | try { 74 | await createTask(name, description, dueDate, projectId, email, selectedUser.email) 75 | rooter.push(`/project/${projectId}`) 76 | } catch (error) { 77 | toast.error("Une erreur est survenue lors de la création de la tâche." + error); 78 | } 79 | 80 | } 81 | 82 | return ( 83 | 84 |
85 |
86 |
    87 |
  • Retour
  • 88 |
  • 89 |
    {project?.name}
    90 |
  • 91 | 92 |
93 |
94 | 95 |
96 |
97 | 98 |
99 | 100 | A livré 101 | 102 | setDueDate(new Date(e.target.value))} 107 | /> 108 |
109 | 110 |
111 |
112 |
113 | setName(e.target.value)} 119 | /> 120 | 126 |
127 |
128 | 129 |
130 |
131 | 132 |
133 | 134 |
135 |
136 | ) 137 | } 138 | 139 | export default page -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Image from "next/image"; 4 | import Wrapper from "./components/Wrapper"; 5 | import { useEffect, useState } from "react"; 6 | import { FolderGit2 } from "lucide-react"; 7 | import { createProject, deleteProjectById, getProjectsCreatedByUser } from "./actions"; 8 | import { useUser } from "@clerk/nextjs"; 9 | import { toast } from "react-toastify"; 10 | import { Project } from "@/type"; 11 | import ProjectComponent from "./components/ProjectComponent"; 12 | import EmptyState from "./components/EmptyState"; 13 | 14 | export default function Home() { 15 | 16 | const { user } = useUser() 17 | const email = user?.primaryEmailAddress?.emailAddress as string 18 | const [name, setName] = useState("") 19 | const [descrition, setDescription] = useState("") 20 | const [projects, setProjects] = useState([]) 21 | 22 | const fetchProjects = async (email: string) => { 23 | try { 24 | const myproject = await getProjectsCreatedByUser(email) 25 | setProjects(myproject) 26 | console.log(myproject) 27 | } catch (error) { 28 | console.error('Erreur lors du chargement des projets:', error); 29 | } 30 | } 31 | 32 | useEffect(() => { 33 | if (email) { 34 | fetchProjects(email) 35 | } 36 | }, [email]) 37 | 38 | const deleteProject = async (projectId: string) => { 39 | try { 40 | await deleteProjectById(projectId) 41 | fetchProjects(email) 42 | toast.success('Project supprimé !') 43 | } catch (error) { 44 | throw new Error('Error deleting project: ' + error); 45 | } 46 | } 47 | 48 | const handleSubmit = async () => { 49 | try { 50 | const modal = document.getElementById('my_modal_3') as HTMLDialogElement 51 | const project = await createProject(name, descrition, email) 52 | if (modal) { 53 | modal.close() 54 | } 55 | setName(""), 56 | setDescription("") 57 | fetchProjects(email) 58 | toast.success("Projet Créé") 59 | } catch (error) { 60 | console.error('Error creating project:', error); 61 | } 62 | } 63 | 64 | return ( 65 | 66 |
67 | {/* You can open the modal using document.getElementById('ID').showModal() method */} 68 | 69 | 70 | 71 |
72 |
73 | {/* if there is a button in form, it will close the modal */} 74 | 75 |
76 |

Nouveau Projet

77 |

Décrivez votre projet simplement grâce à la description

78 |
79 | setName(e.target.value)} 84 | className="border border-base-300 input input-bordered w-full mb-4 placeholder:text-sm" 85 | required 86 | /> 87 | 95 | 98 |
99 |
100 |
101 | 102 |
103 | 104 | {projects.length > 0 ? ( 105 |
    106 | {projects.map((project) => ( 107 |
  • 108 | 109 |
  • 110 | ))} 111 |
112 | ) : ( 113 |
114 | 119 |
120 | )} 121 | 122 |
123 | 124 |
125 |
126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /app/project/[projectId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { deleteTaskById, getProjectInfo } from '@/app/actions'; 4 | import ProjectComponent from '@/app/components/ProjectComponent'; 5 | import UserInfo from '@/app/components/UserInfo'; 6 | import Wrapper from '@/app/components/Wrapper' 7 | import { Project } from '@/type'; 8 | import { useUser } from '@clerk/nextjs'; 9 | import { CircleCheckBig, CopyPlus, ListTodo, Loader, SlidersHorizontal, UserCheck } from 'lucide-react'; 10 | import Link from 'next/link'; 11 | import React, { useEffect, useState } from 'react' 12 | import EmptyState from '@/app/components/EmptyState' 13 | import TaskComponent from '@/app/components/TaskComponent'; 14 | import { toast } from 'react-toastify'; 15 | 16 | const page = ({ params }: { params: Promise<{ projectId: string }> }) => { 17 | 18 | const { user } = useUser(); 19 | const email = user?.primaryEmailAddress?.emailAddress; 20 | 21 | const [projectId, setProjectId] = useState(""); 22 | const [project, setProject] = useState(null); 23 | const [statusFilter, setStatusFilter] = useState(''); 24 | 25 | const [assignedFilter, setAssignedFilter] = useState(false); 26 | const [taskCounts, setTaskCounts] = useState({ todo: 0, inProgress: 0, done: 0, assigned: 0 }) 27 | 28 | const fetchInfos = async (projectId: string) => { 29 | try { 30 | const project = await getProjectInfo(projectId, true) 31 | setProject(project) 32 | 33 | } catch (error) { 34 | console.error('Erreur lors du chargement du projet:', error); 35 | } 36 | } 37 | 38 | useEffect(() => { 39 | const getId = async () => { 40 | const resolvedParams = await params; 41 | setProjectId(resolvedParams.projectId) 42 | fetchInfos(resolvedParams.projectId) 43 | 44 | } 45 | getId() 46 | }, [params]) 47 | 48 | useEffect(() => { 49 | if (project && project.tasks && email) { 50 | const counts = { 51 | todo: project.tasks.filter(task => task.status === "To Do").length, 52 | inProgress: project.tasks.filter(task => task.status == 'In Progress').length, 53 | done: project.tasks.filter(task => task.status == 'Done').length, 54 | assigned: project.tasks.filter(task => task?.user?.email == email).length, 55 | } 56 | setTaskCounts(counts) 57 | } 58 | }, [project]) 59 | 60 | 61 | const filteredTasks = project?.tasks?.filter(task => { 62 | const statusMatch = !statusFilter || task.status == statusFilter 63 | const assignedMatch = !assignedFilter || task?.user?.email == email 64 | return statusMatch && assignedMatch 65 | }) 66 | 67 | const deleteTask = async ( taskId : string) => { 68 | try { 69 | await deleteTaskById(taskId) 70 | fetchInfos(projectId) 71 | toast.success('Tache supprimée !') 72 | } catch (error) { 73 | toast.error("Error Task project") 74 | } 75 | } 76 | 77 | 78 | return ( 79 | 80 |
81 |
82 |
83 | 88 |
89 | 90 |
91 | {project && ( 92 | 93 | )} 94 |
95 |
96 | 97 |
98 |
99 |
100 |
101 | 106 | 107 | 113 | 114 | 120 | 121 |
122 |
123 | 129 | 130 | 136 |
137 |
138 | 139 | Nouvelle tâche 140 | 141 | 142 |
143 |
144 | 145 | {filteredTasks && filteredTasks.length > 0 ? ( 146 |
147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | {filteredTasks.map((task, index) => ( 159 | 160 | 161 | 162 | ))} 163 | 164 |
TitreAssigné àA livré leActions
165 | 166 |
167 | ) : ( 168 | 173 | )} 174 |
175 | 176 | 177 |
178 | 179 | 180 |
181 |
182 | ) 183 | } 184 | 185 | export default page -------------------------------------------------------------------------------- /app/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import AuthWrapper from '@/app/components/AuthWrapper' 2 | import { SignIn } from '@clerk/nextjs' 3 | 4 | export default function Page() { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } -------------------------------------------------------------------------------- /app/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import AuthWrapper from '@/app/components/AuthWrapper' 2 | import { SignUp } from '@clerk/nextjs' 3 | 4 | export default function Page() { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } -------------------------------------------------------------------------------- /app/task-details/[taskId]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { getProjectInfo, getTaskDetails, updateTaskStatus } from '@/app/actions'; 3 | import EmptyState from '@/app/components/EmptyState'; 4 | import UserInfo from '@/app/components/UserInfo'; 5 | import Wrapper from '@/app/components/Wrapper'; 6 | import { Project, Task } from '@/type'; 7 | import Link from 'next/link'; 8 | import React, { useEffect, useState } from 'react' 9 | import ReactQuill from 'react-quill-new'; 10 | import { toast } from 'react-toastify'; 11 | import 'react-quill-new/dist/quill.snow.css'; 12 | import { useUser } from '@clerk/nextjs'; 13 | 14 | const page = ({ params }: { params: Promise<{ taskId: string }> }) => { 15 | const { user } = useUser(); 16 | const email = user?.primaryEmailAddress?.emailAddress; 17 | 18 | const [task, setTask] = useState(null) 19 | const [taskId, setTaskId] = useState("") 20 | const [projectId, setProjectId] = useState(""); 21 | const [project, setProject] = useState(null); 22 | const [status, setStatus] = useState(""); 23 | const [realStatus, setRealStatus] = useState(""); 24 | const [solution, setSolution] = useState(""); 25 | 26 | const modules = { 27 | toolbar: [ 28 | [{ 'header': [1, 2, 3, false] }], 29 | ['bold', 'italic', 'underline', 'strike'], 30 | [{ 'font': [] }], 31 | [{ 'list': 'ordered' }, { 'list': 'bullet' }], 32 | [{ 'color': [] }, { 'background': [] }], 33 | ['blockquote', 'code-block'], 34 | ['link', 'image'], 35 | ['clean'] 36 | ] 37 | }; 38 | 39 | const fetchInfos = async (taskId: string) => { 40 | try { 41 | const task = await getTaskDetails(taskId) 42 | setTask(task) 43 | setStatus(task.status) 44 | setRealStatus(task.status) 45 | fetchProject(task.projectId) 46 | } catch (error) { 47 | toast.error("Erreur lors du chargement des détails de la tâche."); 48 | } 49 | } 50 | 51 | const fetchProject = async (projectId: string) => { 52 | try { 53 | const project = await getProjectInfo(projectId, false) 54 | setProject(project) 55 | } catch (error) { 56 | toast.error("Erreur lors du chargement du projet"); 57 | } 58 | } 59 | 60 | useEffect(() => { 61 | const getId = async () => { 62 | const resolvedParams = await params; 63 | setTaskId(resolvedParams.taskId) 64 | fetchInfos(resolvedParams.taskId) 65 | } 66 | getId() 67 | }, [params]) 68 | 69 | const changeStatus = async (taskId: string, newStatus: string) => { 70 | try { 71 | await updateTaskStatus(taskId, newStatus) 72 | fetchInfos(taskId) 73 | } catch (error) { 74 | toast.error("Erreur lors du changement de status") 75 | } 76 | } 77 | 78 | const handleStatusChange = (event: React.ChangeEvent) => { 79 | const newStatus = event.target.value; 80 | setStatus(newStatus) 81 | const modal = document.getElementById('my_modal_3') as HTMLDialogElement 82 | if (newStatus == "To Do" || newStatus == "In Progress") { 83 | changeStatus(taskId, newStatus) 84 | toast.success('Status changé') 85 | modal.close() 86 | } else { 87 | modal.showModal() 88 | } 89 | } 90 | 91 | const closeTask = async (newStatus: string) => { 92 | const modal = document.getElementById('my_modal_3') as HTMLDialogElement 93 | try { 94 | if (solution != "") { 95 | await updateTaskStatus(taskId, newStatus, solution) 96 | fetchInfos(taskId) 97 | if (modal) { 98 | modal.close() 99 | } 100 | toast.success('Tache cloturée') 101 | } else { 102 | toast.error('Il manque une solution') 103 | } 104 | 105 | } catch (error) { 106 | toast.error("Erreur lors du changement de status") 107 | } 108 | } 109 | 110 | useEffect(() => { 111 | const modal = document.getElementById('my_modal_3') as HTMLDialogElement 112 | const handleClose = () => { 113 | if (status === "Done" && status !== realStatus) { 114 | setStatus(realStatus) 115 | } 116 | } 117 | if (modal) { 118 | modal.addEventListener('close', handleClose) 119 | } 120 | return () => { 121 | if (modal) { 122 | modal.removeEventListener('close', handleClose) 123 | } 124 | } 125 | 126 | }, [status, realStatus]) 127 | 128 | return ( 129 | 130 | {task ? ( 131 |
132 |
133 |
134 |
    135 |
  • Retour
  • 136 |
  • {project?.name}
  • 137 |
138 |
139 |
140 | 145 |
146 |
147 | 148 |

{task.name}

149 | 150 |
151 | 152 | A livré le 153 |
{task?.dueDate?.toLocaleDateString()}
154 |
155 |
156 | 166 |
167 |
168 | 169 |
170 |
171 |
172 | 177 |
178 |
179 | {task.dueDate && ` 180 | ${Math.max(0, Math.ceil((new Date(task.dueDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24)))} jours restants 181 | `} 182 |
183 |
184 |
185 | 186 |
187 |
191 |
192 | 193 | {task?.solutionDescription && ( 194 |
195 |
196 | Solution 197 |
198 | 199 |
200 |
204 |
205 |
206 | )} 207 | 208 | 209 | 210 |
211 |
212 | {/* if there is a button in form, it will close the modal */} 213 | 214 |
215 |

C'est quoi la solutions ?

216 |

Décrivez ce que vous avez fait exactement

217 | 218 | 224 | 225 |
226 |
227 | 228 |
229 | ) : ( 230 | 235 | )} 236 | 237 | ) 238 | } 239 | 240 | export default page -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const prismaClientSingleton = () => { 4 | return new PrismaClient() 5 | } 6 | 7 | declare const globalThis: { 8 | prismaGlobal: ReturnType; 9 | } & typeof global; 10 | 11 | const prisma = globalThis.prismaGlobal ?? prismaClientSingleton() 12 | 13 | export default prisma 14 | 15 | if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' 2 | 3 | const isPublicRoute = createRouteMatcher(['/sign-in(.*)', '/sign-up(.*)']) 4 | 5 | export default clerkMiddleware(async (auth, request) => { 6 | if (!isPublicRoute(request)) { 7 | await auth.protect() 8 | } 9 | }) 10 | 11 | export const config = { 12 | matcher: [ 13 | // Skip Next.js internals and all static files, unless found in search params 14 | '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', 15 | // Always run for API routes 16 | '/(api|trpc)(.*)', 17 | ], 18 | } -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "taskflow", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@clerk/nextjs": "^6.3.4", 13 | "@prisma/client": "^5.22.0", 14 | "lucide-react": "^0.460.0", 15 | "next": "15.0.3", 16 | "react": "19.0.0-rc-66855b96-20241106", 17 | "react-dom": "19.0.0-rc-66855b96-20241106", 18 | "react-quill-new": "^3.3.3", 19 | "react-toastify": "^10.0.6" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20", 23 | "@types/react": "^18", 24 | "@types/react-dom": "^18", 25 | "daisyui": "^4.12.14", 26 | "eslint": "^8", 27 | "eslint-config-next": "15.0.3", 28 | "postcss": "^8", 29 | "prisma": "^5.22.0", 30 | "tailwindcss": "^3.4.1", 31 | "typescript": "^5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /prisma/dev.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadikou-faiz/taskflow/a860d8dea01692c9b2c8329c39bde3262cbc1060/prisma/dev.db -------------------------------------------------------------------------------- /prisma/migrations/20241117141302_add_models/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL PRIMARY KEY, 4 | "name" TEXT NOT NULL, 5 | "email" TEXT NOT NULL 6 | ); 7 | 8 | -- CreateTable 9 | CREATE TABLE "Project" ( 10 | "id" TEXT NOT NULL PRIMARY KEY, 11 | "name" TEXT NOT NULL, 12 | "description" TEXT, 13 | "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, 14 | "updatedAt" DATETIME NOT NULL, 15 | "inviteCode" TEXT NOT NULL, 16 | "createdById" TEXT NOT NULL, 17 | CONSTRAINT "Project_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 18 | ); 19 | 20 | -- CreateTable 21 | CREATE TABLE "Task" ( 22 | "id" TEXT NOT NULL PRIMARY KEY, 23 | "name" TEXT NOT NULL, 24 | "description" TEXT NOT NULL, 25 | "status" TEXT NOT NULL DEFAULT 'To Do', 26 | "dueDate" DATETIME, 27 | "projectId" TEXT NOT NULL, 28 | "userId" TEXT, 29 | "createdById" TEXT NOT NULL, 30 | "solutionDescription" TEXT, 31 | CONSTRAINT "Task_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE, 32 | CONSTRAINT "Task_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE, 33 | CONSTRAINT "Task_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE 34 | ); 35 | 36 | -- CreateTable 37 | CREATE TABLE "ProjectUser" ( 38 | "id" TEXT NOT NULL PRIMARY KEY, 39 | "userId" TEXT NOT NULL, 40 | "projectId" TEXT NOT NULL, 41 | CONSTRAINT "ProjectUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE, 42 | CONSTRAINT "ProjectUser_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project" ("id") ON DELETE CASCADE ON UPDATE CASCADE 43 | ); 44 | 45 | -- CreateIndex 46 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 47 | 48 | -- CreateIndex 49 | CREATE UNIQUE INDEX "Project_inviteCode_key" ON "Project"("inviteCode"); 50 | 51 | -- CreateIndex 52 | CREATE UNIQUE INDEX "ProjectUser_userId_projectId_key" ON "ProjectUser"("userId", "projectId"); 53 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "sqlite" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "sqlite" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model User { 14 | id String @id @default(uuid()) 15 | name String 16 | email String @unique 17 | tasks Task[] 18 | createdTasks Task[] @relation("CreatedTasks") 19 | projects Project[] @relation("UserProjects") 20 | userProjects ProjectUser[] 21 | } 22 | 23 | model Project { 24 | id String @id @default(uuid()) 25 | name String 26 | description String? 27 | createdAt DateTime @default(now()) 28 | updatedAt DateTime @updatedAt 29 | tasks Task[] 30 | inviteCode String @unique 31 | createdById String 32 | createdBy User @relation("UserProjects", fields: [createdById], references: [id]) 33 | users ProjectUser[] 34 | } 35 | 36 | model Task { 37 | id String @id @default(uuid()) 38 | name String 39 | description String 40 | status String @default("To Do") 41 | dueDate DateTime? 42 | projectId String 43 | project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) 44 | user User? @relation(fields: [userId], references: [id]) 45 | userId String? 46 | createdById String 47 | createdBy User @relation("CreatedTasks", fields: [createdById], references: [id]) 48 | solutionDescription String? 49 | } 50 | 51 | model ProjectUser { 52 | id String @id @default(uuid()) 53 | userId String 54 | projectId String 55 | user User @relation(fields: [userId], references: [id]) 56 | project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) 57 | @@unique([userId, projectId]) 58 | } 59 | 60 | -------------------------------------------------------------------------------- /public/empty-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadikou-faiz/taskflow/a860d8dea01692c9b2c8329c39bde3262cbc1060/public/empty-project.png -------------------------------------------------------------------------------- /public/empty-task.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadikou-faiz/taskflow/a860d8dea01692c9b2c8329c39bde3262cbc1060/public/empty-task.png -------------------------------------------------------------------------------- /public/profile.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadikou-faiz/taskflow/a860d8dea01692c9b2c8329c39bde3262cbc1060/public/profile.avif -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | }, 15 | }, 16 | }, 17 | plugins: [ 18 | require('daisyui'), 19 | ], 20 | daisyui: { 21 | themes: ["light", "dark", "cmyk"], 22 | }, 23 | } satisfies Config; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /type.ts: -------------------------------------------------------------------------------- 1 | import { Project as PrismaProject, Task as PrismaTask, User } from '@prisma/client'; 2 | 3 | // Fusion du type PrismaProject avec vos propriétés supplémentaires 4 | export type Project = PrismaProject & { 5 | totalTasks?: number; 6 | collaboratorsCount?: number; 7 | taskStats?: { 8 | toDo: number; 9 | inProgress: number; 10 | done: number; 11 | }; 12 | percentages?: { 13 | progressPercentage: number; 14 | inProgressPercentage: number; 15 | toDoPercentage: number; 16 | }; 17 | tasks?: Task[]; // Assurez-vous que la relation tasks est incluse 18 | users?: User[]; 19 | createdBy?: User, 20 | }; 21 | 22 | export type Task = PrismaTask & { 23 | user?: User | null; 24 | createdBy?: User | null ; 25 | } 26 | --------------------------------------------------------------------------------