Skip to content

Commit 25d6224

Browse files
Merge pull request #44 from Ajiet-DevNation/feature
Allow author to remove contributors and contributors to leave project
2 parents 596d1fa + 50e96bf commit 25d6224

6 files changed

Lines changed: 308 additions & 38 deletions

File tree

actions/contributors.tsx

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
"use server";
2+
3+
import { db } from "@/lib/prisma";
4+
5+
export async function deleteContributor(contributorId: string, authorId: string, projectId: string) {
6+
try {
7+
if (!contributorId || !authorId || !projectId) {
8+
throw new Error("Missing required parameters");
9+
}
10+
11+
const project = await db.project.findUnique({
12+
where: { id: projectId },
13+
include: { contributors: true }
14+
});
15+
16+
if (!project) {
17+
throw new Error("Project not found");
18+
}
19+
20+
if (project.authorId !== authorId) {
21+
throw new Error("Unauthorized: Only the project author can remove contributors");
22+
}
23+
24+
const contributer = project.contributors.find(c => c.id === contributorId);
25+
26+
if (!contributer) {
27+
throw new Error("Contributor not found in this project");
28+
}
29+
30+
await db.contributor.delete({
31+
where: { id: contributorId }
32+
});
33+
34+
await db.application.delete({
35+
where: {
36+
profileId_projectId: {
37+
profileId: contributer.profileId,
38+
projectId: projectId,
39+
}
40+
}
41+
});
42+
43+
return { success: true, message: "Contributor removed successfully" };
44+
} catch (error) {
45+
console.error('Error removing contributor:', error);
46+
return {
47+
success: false,
48+
message: 'Failed to remove contributor',
49+
};
50+
}
51+
}
52+
53+
export async function leaveProject(contributorId: string, userId: string, projectId: string) {
54+
try {
55+
if (!contributorId || !userId || !projectId) {
56+
throw new Error("Missing required parameters");
57+
}
58+
59+
const project = await db.project.findUnique({
60+
where: { id: projectId },
61+
include: { contributors: true }
62+
});
63+
64+
if (!project) throw new Error("Project not found");
65+
const contributor = project.contributors.find(c => c.id === contributorId);
66+
if (!contributor) throw new Error("Contributor not found in this project");
67+
if (contributor?.profileId !== userId) throw new Error("Unauthorized: You can only leave projects you are a contributor of");
68+
69+
await db.contributor.delete({
70+
where: { id: contributorId }
71+
});
72+
73+
await db.application.delete({
74+
where: {
75+
profileId_projectId: {
76+
profileId: contributor.profileId,
77+
projectId: projectId,
78+
}
79+
}
80+
});
81+
82+
return { success: true, message: "Successfully left the project" };
83+
} catch (error) {
84+
console.error('Error leaving project:', error);
85+
return {
86+
success: false,
87+
message: 'Failed to leave project',
88+
};
89+
}
90+
}

prisma/schema.prisma

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ model Profile {
6161
image String?
6262
section String
6363
branch String
64-
college String @default("Not Set")
64+
college String?
6565
year String
6666
skills String[]
6767
bio String?
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
"use client";
2+
3+
import { useState } from "react";
4+
import { useRouter } from "next/navigation";
5+
import { UserMinus, LogOut, Loader2 } from "lucide-react";
6+
import { toast } from "sonner";
7+
import { deleteContributor, leaveProject } from "../../../../../../actions/contributors";
8+
9+
interface ContributorActionsProps {
10+
contributorId: string;
11+
contributorProfileId: string;
12+
contributorName: string;
13+
projectId: string;
14+
currentUserProfileId?: string;
15+
authorProfileId: string;
16+
}
17+
18+
export function ContributorActions({
19+
contributorId,
20+
contributorProfileId,
21+
contributorName,
22+
projectId,
23+
currentUserProfileId,
24+
authorProfileId,
25+
}: ContributorActionsProps) {
26+
const [loading, setLoading] = useState(false);
27+
const [showConfirm, setShowConfirm] = useState(false);
28+
const router = useRouter();
29+
30+
const isAuthor = currentUserProfileId === authorProfileId;
31+
const isCurrentUserContributor = currentUserProfileId === contributorProfileId;
32+
33+
if (!isAuthor && !isCurrentUserContributor) {
34+
return null;
35+
}
36+
37+
const handleRemoveContributor = async () => {
38+
if (!currentUserProfileId) return;
39+
40+
setLoading(true);
41+
try {
42+
const result = await deleteContributor(contributorId, currentUserProfileId, projectId);
43+
44+
if (result.success) {
45+
toast.success("Contributor removed successfully");
46+
router.refresh();
47+
} else {
48+
toast.error(result.message || "Failed to remove contributor");
49+
}
50+
} catch {
51+
toast.error("An error occurred while removing the contributor");
52+
} finally {
53+
setLoading(false);
54+
setShowConfirm(false);
55+
}
56+
};
57+
58+
const handleLeaveProject = async () => {
59+
if (!currentUserProfileId) return;
60+
61+
setLoading(true);
62+
try {
63+
const result = await leaveProject(contributorId, currentUserProfileId, projectId);
64+
65+
if (result.success) {
66+
toast.success("Successfully left the project");
67+
router.refresh();
68+
} else {
69+
toast.error(result.message || "Failed to leave project");
70+
}
71+
} catch {
72+
toast.error("An error occurred while leaving the project");
73+
} finally {
74+
setLoading(false);
75+
setShowConfirm(false);
76+
}
77+
};
78+
79+
if (showConfirm) {
80+
return (
81+
<div className="flex items-center gap-2 ml-auto">
82+
<span className="text-xs text-muted-foreground">
83+
{isAuthor ? "Remove?" : "Leave?"}
84+
</span>
85+
<button
86+
onClick={isAuthor ? handleRemoveContributor : handleLeaveProject}
87+
disabled={loading}
88+
className="px-2 py-1 text-xs font-medium rounded-md bg-red-500/10 text-red-600 dark:text-red-400 hover:bg-red-500/20 transition-colors disabled:opacity-50"
89+
>
90+
{loading ? (
91+
<Loader2 className="h-3 w-3 animate-spin" />
92+
) : (
93+
"Yes"
94+
)}
95+
</button>
96+
<button
97+
onClick={() => setShowConfirm(false)}
98+
disabled={loading}
99+
className="px-2 py-1 text-xs font-medium rounded-md bg-muted text-muted-foreground hover:bg-muted/80 transition-colors disabled:opacity-50"
100+
>
101+
No
102+
</button>
103+
</div>
104+
);
105+
}
106+
107+
if (isAuthor) {
108+
return (
109+
<button
110+
onClick={() => setShowConfirm(true)}
111+
className="ml-auto p-2 rounded-lg text-muted-foreground hover:text-red-500 hover:bg-red-500/10 transition-all group"
112+
title={`Remove ${contributorName} from project`}
113+
>
114+
<UserMinus className="h-4 w-4" />
115+
</button>
116+
);
117+
}
118+
119+
if (isCurrentUserContributor) {
120+
return (
121+
<button
122+
onClick={() => setShowConfirm(true)}
123+
className="ml-auto p-2 rounded-lg text-muted-foreground hover:text-orange-500 hover:bg-orange-500/10 transition-all group"
124+
title="Leave this project"
125+
>
126+
<LogOut className="h-4 w-4" />
127+
</button>
128+
);
129+
}
130+
131+
return null;
132+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"use client";
2+
3+
import Link from "next/link";
4+
import Image from "next/image";
5+
import { ContributorActions } from "./ContributorActions";
6+
7+
interface Contributor {
8+
id: string;
9+
contributorRecordId: string;
10+
name: string;
11+
avatar?: string;
12+
}
13+
14+
interface ContributorsSectionProps {
15+
contributors: Contributor[];
16+
projectId: string;
17+
currentUserProfileId?: string;
18+
authorProfileId: string;
19+
}
20+
21+
export function ContributorsSection({
22+
contributors,
23+
projectId,
24+
currentUserProfileId,
25+
authorProfileId,
26+
}: ContributorsSectionProps) {
27+
return (
28+
<section className="bg-card/60 backdrop-blur-sm border border-border rounded-xl p-8 shadow-xl fade-in-up delay-200">
29+
<h2 className="text-xl font-semibold text-foreground mb-6 pb-3 border-b border-border/50">
30+
Contributors ({contributors.length})
31+
</h2>
32+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
33+
{contributors.map((contributor) => (
34+
<div
35+
key={contributor.id}
36+
className="flex items-center gap-4 p-4 rounded-xl transition-all hover:bg-white/5 hover:shadow-md backdrop-blur-sm"
37+
>
38+
<Link
39+
href={`/profile/${contributor.id}`}
40+
className="flex items-center gap-4 flex-1 min-w-0"
41+
>
42+
{contributor.avatar ? (
43+
<Image
44+
src={contributor.avatar}
45+
alt={contributor.name}
46+
width={48}
47+
height={48}
48+
className="h-12 w-12 rounded-full bg-muted border-2 border-border shadow-sm"
49+
/>
50+
) : (
51+
<div className="h-12 w-12 rounded-full bg-muted border-2 border-border flex items-center justify-center text-muted-foreground font-semibold shadow-sm">
52+
{contributor.name.charAt(0)}
53+
</div>
54+
)}
55+
<div className="flex-1 min-w-0">
56+
<p className="text-sm font-semibold text-foreground truncate">
57+
{contributor.name}
58+
</p>
59+
<p className="text-xs text-muted-foreground">Contributor</p>
60+
</div>
61+
</Link>
62+
<ContributorActions
63+
contributorId={contributor.contributorRecordId}
64+
contributorProfileId={contributor.id}
65+
contributorName={contributor.name}
66+
projectId={projectId}
67+
currentUserProfileId={currentUserProfileId}
68+
authorProfileId={authorProfileId}
69+
/>
70+
</div>
71+
))}
72+
</div>
73+
</section>
74+
);
75+
}

src/app/(user)/projects/[id]/page.tsx

Lines changed: 8 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { db } from "@/lib/prisma";
1010
import { ApplyButton } from "../_components/ApplyButton";
1111
import LikeButton from "@/components/LikeButton";
1212
import { SettingsDropdown } from "./_components/SettingsDropdown";
13+
import { ContributorsSection } from "./_components/ContributorsSection";
1314
import { UserRoundCog } from "lucide-react";
1415

1516
interface ProjectPageProps {
@@ -166,6 +167,7 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
166167
endDate: apiProject.endDate,
167168
contributors: apiProject.contributors.map((c) => ({
168169
id: c.user.id,
170+
contributorRecordId: c.id, // The Contributor table record ID
169171
name: c.user.name,
170172
avatar: c.user.image || undefined,
171173
})),
@@ -301,42 +303,12 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
301303
</section>
302304

303305
{project.contributors.length > 0 && (
304-
<section className="bg-card/60 backdrop-blur-sm border border-border rounded-xl p-8 shadow-xl fade-in-up delay-200">
305-
<h2 className="text-xl font-semibold text-foreground mb-6 pb-3 border-b border-border/50">
306-
Contributors ({project.contributors.length})
307-
</h2>
308-
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
309-
{project.contributors.map((contributor) => (
310-
<Link
311-
key={contributor.id}
312-
href={`/profile/${contributor.id}`}
313-
className="flex items-center gap-4 p-4 rounded-xl transition-all hover:bg-white/5 hover:shadow-md backdrop-blur-sm"
314-
>
315-
{contributor.avatar ? (
316-
<Image
317-
src={contributor.avatar}
318-
alt={contributor.name}
319-
width={48}
320-
height={48}
321-
className="h-12 w-12 rounded-full bg-muted border-2 border-border shadow-sm"
322-
/>
323-
) : (
324-
<div className="h-12 w-12 rounded-full bg-muted border-2 border-border flex items-center justify-center text-muted-foreground font-semibold shadow-sm">
325-
{contributor.name.charAt(0)}
326-
</div>
327-
)}
328-
<div className="flex-1 min-w-0">
329-
<p className="text-sm font-semibold text-foreground truncate">
330-
{contributor.name}
331-
</p>
332-
<p className="text-xs text-muted-foreground">
333-
Contributor
334-
</p>
335-
</div>
336-
</Link>
337-
))}
338-
</div>
339-
</section>
306+
<ContributorsSection
307+
contributors={project.contributors}
308+
projectId={project.id}
309+
currentUserProfileId={currentUserProfile?.id}
310+
authorProfileId={apiProject.authorId}
311+
/>
340312
)}
341313
</div>
342314

src/app/api/projects/[projectId]/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,5 @@ export async function DELETE(
178178
{ status: 500 }
179179
);
180180
}
181-
}
181+
}
182+

0 commit comments

Comments
 (0)