diff --git a/actions/contributors.tsx b/actions/contributors.tsx new file mode 100644 index 0000000..f45d9cd --- /dev/null +++ b/actions/contributors.tsx @@ -0,0 +1,90 @@ +"use server"; + +import { db } from "@/lib/prisma"; + +export async function deleteContributor(contributorId: string, authorId: string, projectId: string) { + try { + if (!contributorId || !authorId || !projectId) { + throw new Error("Missing required parameters"); + } + + const project = await db.project.findUnique({ + where: { id: projectId }, + include: { contributors: true } + }); + + if (!project) { + throw new Error("Project not found"); + } + + if (project.authorId !== authorId) { + throw new Error("Unauthorized: Only the project author can remove contributors"); + } + + const contributer = project.contributors.find(c => c.id === contributorId); + + if (!contributer) { + throw new Error("Contributor not found in this project"); + } + + await db.contributor.delete({ + where: { id: contributorId } + }); + + await db.application.delete({ + where: { + profileId_projectId: { + profileId: contributer.profileId, + projectId: projectId, + } + } + }); + + return { success: true, message: "Contributor removed successfully" }; + } catch (error) { + console.error('Error removing contributor:', error); + return { + success: false, + message: 'Failed to remove contributor', + }; + } +} + +export async function leaveProject(contributorId: string, userId: string, projectId: string) { + try { + if (!contributorId || !userId || !projectId) { + throw new Error("Missing required parameters"); + } + + const project = await db.project.findUnique({ + where: { id: projectId }, + include: { contributors: true } + }); + + if (!project) throw new Error("Project not found"); + const contributor = project.contributors.find(c => c.id === contributorId); + if (!contributor) throw new Error("Contributor not found in this project"); + if (contributor?.profileId !== userId) throw new Error("Unauthorized: You can only leave projects you are a contributor of"); + + await db.contributor.delete({ + where: { id: contributorId } + }); + + await db.application.delete({ + where: { + profileId_projectId: { + profileId: contributor.profileId, + projectId: projectId, + } + } + }); + + return { success: true, message: "Successfully left the project" }; + } catch (error) { + console.error('Error leaving project:', error); + return { + success: false, + message: 'Failed to leave project', + }; + } +} \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 7cd6faa..def88aa 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -61,7 +61,7 @@ model Profile { image String? section String branch String - college String @default("Not Set") + college String? year String skills String[] bio String? diff --git a/src/app/(user)/projects/[id]/_components/ContributorActions.tsx b/src/app/(user)/projects/[id]/_components/ContributorActions.tsx new file mode 100644 index 0000000..8407fe1 --- /dev/null +++ b/src/app/(user)/projects/[id]/_components/ContributorActions.tsx @@ -0,0 +1,132 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { UserMinus, LogOut, Loader2 } from "lucide-react"; +import { toast } from "sonner"; +import { deleteContributor, leaveProject } from "../../../../../../actions/contributors"; + +interface ContributorActionsProps { + contributorId: string; + contributorProfileId: string; + contributorName: string; + projectId: string; + currentUserProfileId?: string; + authorProfileId: string; +} + +export function ContributorActions({ + contributorId, + contributorProfileId, + contributorName, + projectId, + currentUserProfileId, + authorProfileId, +}: ContributorActionsProps) { + const [loading, setLoading] = useState(false); + const [showConfirm, setShowConfirm] = useState(false); + const router = useRouter(); + + const isAuthor = currentUserProfileId === authorProfileId; + const isCurrentUserContributor = currentUserProfileId === contributorProfileId; + + if (!isAuthor && !isCurrentUserContributor) { + return null; + } + + const handleRemoveContributor = async () => { + if (!currentUserProfileId) return; + + setLoading(true); + try { + const result = await deleteContributor(contributorId, currentUserProfileId, projectId); + + if (result.success) { + toast.success("Contributor removed successfully"); + router.refresh(); + } else { + toast.error(result.message || "Failed to remove contributor"); + } + } catch { + toast.error("An error occurred while removing the contributor"); + } finally { + setLoading(false); + setShowConfirm(false); + } + }; + + const handleLeaveProject = async () => { + if (!currentUserProfileId) return; + + setLoading(true); + try { + const result = await leaveProject(contributorId, currentUserProfileId, projectId); + + if (result.success) { + toast.success("Successfully left the project"); + router.refresh(); + } else { + toast.error(result.message || "Failed to leave project"); + } + } catch { + toast.error("An error occurred while leaving the project"); + } finally { + setLoading(false); + setShowConfirm(false); + } + }; + + if (showConfirm) { + return ( +
+ + {isAuthor ? "Remove?" : "Leave?"} + + + +
+ ); + } + + if (isAuthor) { + return ( + + ); + } + + if (isCurrentUserContributor) { + return ( + + ); + } + + return null; +} diff --git a/src/app/(user)/projects/[id]/_components/ContributorsSection.tsx b/src/app/(user)/projects/[id]/_components/ContributorsSection.tsx new file mode 100644 index 0000000..66faa87 --- /dev/null +++ b/src/app/(user)/projects/[id]/_components/ContributorsSection.tsx @@ -0,0 +1,75 @@ +"use client"; + +import Link from "next/link"; +import Image from "next/image"; +import { ContributorActions } from "./ContributorActions"; + +interface Contributor { + id: string; + contributorRecordId: string; + name: string; + avatar?: string; +} + +interface ContributorsSectionProps { + contributors: Contributor[]; + projectId: string; + currentUserProfileId?: string; + authorProfileId: string; +} + +export function ContributorsSection({ + contributors, + projectId, + currentUserProfileId, + authorProfileId, +}: ContributorsSectionProps) { + return ( +
+

+ Contributors ({contributors.length}) +

+
+ {contributors.map((contributor) => ( +
+ + {contributor.avatar ? ( + {contributor.name} + ) : ( +
+ {contributor.name.charAt(0)} +
+ )} +
+

+ {contributor.name} +

+

Contributor

+
+ + +
+ ))} +
+
+ ); +} diff --git a/src/app/(user)/projects/[id]/page.tsx b/src/app/(user)/projects/[id]/page.tsx index 3bd68fb..2929a57 100644 --- a/src/app/(user)/projects/[id]/page.tsx +++ b/src/app/(user)/projects/[id]/page.tsx @@ -10,6 +10,7 @@ import { db } from "@/lib/prisma"; import { ApplyButton } from "../_components/ApplyButton"; import LikeButton from "@/components/LikeButton"; import { SettingsDropdown } from "./_components/SettingsDropdown"; +import { ContributorsSection } from "./_components/ContributorsSection"; import { UserRoundCog } from "lucide-react"; interface ProjectPageProps { @@ -166,6 +167,7 @@ export default async function ProjectPage({ params }: ProjectPageProps) { endDate: apiProject.endDate, contributors: apiProject.contributors.map((c) => ({ id: c.user.id, + contributorRecordId: c.id, // The Contributor table record ID name: c.user.name, avatar: c.user.image || undefined, })), @@ -301,42 +303,12 @@ export default async function ProjectPage({ params }: ProjectPageProps) { {project.contributors.length > 0 && ( -
-

- Contributors ({project.contributors.length}) -

-
- {project.contributors.map((contributor) => ( - - {contributor.avatar ? ( - {contributor.name} - ) : ( -
- {contributor.name.charAt(0)} -
- )} -
-

- {contributor.name} -

-

- Contributor -

-
- - ))} -
-
+ )} diff --git a/src/app/api/projects/[projectId]/route.ts b/src/app/api/projects/[projectId]/route.ts index 837fc49..13c2a60 100644 --- a/src/app/api/projects/[projectId]/route.ts +++ b/src/app/api/projects/[projectId]/route.ts @@ -178,4 +178,5 @@ export async function DELETE( { status: 500 } ); } -} \ No newline at end of file +} +