Skip to content

feat(invite): ✨ [WIP] invite to project and board #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions app/[slug]/[board]/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { getServerSession } from "next-auth";
import { Users } from "lucide-react";
import { BoardUser } from "@prisma/client";

import { authOptions } from "@/lib/auth";
import { findBoardBySlug } from "@/helpers/boards/findBoardBySlug";
import { checkUserAccess } from "@/helpers/common/hasAccess";
import { BoardsList } from "@/components/boards/list";
import { formatBoardType } from "@/helpers/common/formatBoardType";
import { Badge } from "@/components/ui/badge";
import { BoardView } from "@/components/boards/view";
import { CreatePost } from "@/components/posts/create";
Expand All @@ -23,9 +24,10 @@ export default async function BoardLayout({
children: React.ReactNode;
params: { board: string; slug: string };
}) {
const board = (await findBoardBySlug(params.board)) as
| (Board & { projectId: string })
| null;
const board = (await findBoardBySlug({
slug: params.board,
projectSlug: params.slug,
})) as (Board & { projectId: string; boardUsers: BoardUser[] }) | null;
const session = await getServerSession(authOptions);

if (!board) {
Expand All @@ -48,23 +50,30 @@ export default async function BoardLayout({
<div className="mb-4 flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<div className="flex items-center space-x-4">
<h1 className="text-xl font-bold sm:text-2xl">{board.name}</h1>
<Badge variant="outline">
{formatBoardType(board.boardType as string)}
</Badge>
{session &&
board.boardUsers.some(
(user) =>
user.userId === session.user.id && user.role === "MEMBER"
) && (
<Badge variant="outline">
<Users aria-label="Shared Board" className="mr-1" size={13} />
Team
</Badge>
)}
</div>
<div className="flex flex-col items-start space-y-2 sm:flex-row sm:items-center sm:space-x-4 sm:space-y-0">
<div className="flex flex-col items-start space-y-2 sm:flex-row sm:items-center sm:space-x-2 sm:space-y-0">
<Input
disabled
className="w-full sm:w-auto"
placeholder="Search Posts (Coming Soon)"
/>
<BoardView />
{hasAccess && <BoardOptions />}
{hasAccess && <BoardOptions boardId={board.id} />}
{session ? (
<CreatePost
boardId={board.id as string}
projectId={board.projectId as string}
text="New Post"
text="New"
/>
) : null}
</div>
Expand Down
16 changes: 9 additions & 7 deletions app/[slug]/[board]/(main)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ export async function generateMetadata({
}: {
params: { board: string; slug: string };
}) {
const board = (await findBoardBySlug(params.board)) as
| (Board & { project: Project; projectId: string; id: string })
| null;
const board = (await findBoardBySlug({
slug: params.board,
projectSlug: params.slug,
})) as (Board & { project: Project; projectId: string; id: string }) | null;

return {
title: board?.name + " - " + board?.project?.name,
Expand All @@ -30,9 +31,10 @@ export default async function BoardPage({
params: { board: string; slug: string };
searchParams: { view?: string };
}) {
const board = (await findBoardBySlug(params.board)) as
| (Board & { project: Project; projectId: string; id: string })
| null;
const board = (await findBoardBySlug({
slug: params.board,
projectSlug: params.slug,
})) as (Board & { project: Project; projectId: string; id: string }) | null;
const session = await getServerSession(authOptions);
const view = searchParams.view || "list";

Expand All @@ -48,7 +50,7 @@ export default async function BoardPage({

return (
<PostsList
boardId={board.id}
boardId={board?.id as string}
cols={2}
currentUserId={session?.user?.id as string}
hasAccess={hasAccess}
Expand Down
40 changes: 31 additions & 9 deletions app/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getServerSession } from "next-auth";
import { Project } from "@prisma/client";
import { Project, ProjectUser } from "@prisma/client";
import { Users } from "lucide-react";

import { findProjectBySlug } from "@/helpers/projects/findProjectBySlug";
import { BoardsList } from "@/components/boards/list";
Expand All @@ -12,6 +13,7 @@ import { Recent } from "@/components/common/recent";
import { Input } from "@/components/ui/input";
import { Roadmap } from "@/components/common/roadmap";
import { BoardFilter } from "@/components/boards/filter";
import { Badge } from "@/components/ui/badge";

import NotFound from "./not-found";
import PrivateBoard from "./private";
Expand All @@ -33,7 +35,9 @@ export default async function ProjectPage({
}: {
params: { slug: string };
}) {
const project = (await findProjectBySlug(params.slug)) as Project | null;
const project = (await findProjectBySlug(params.slug)) as
| (Project & { projectUsers: ProjectUser[] })
| null;
const session = await getServerSession(authOptions);

if (!project) {
Expand All @@ -55,14 +59,33 @@ export default async function ProjectPage({
<header className="mb-8 flex flex-col items-center justify-between gap-4 sm:flex-row">
{session && hasAccess && (
<>
<Input
disabled
className="w-full sm:w-auto"
placeholder="Search boards... (Coming Soon)"
/>
<section className="flex items-center gap-2">
<h1 className="text-xl font-bold sm:text-2xl">
{project.name}
</h1>
{project.projectUsers.some(
(user) =>
user.userId === session.user.id && user.role === "MEMBER"
) && (
<Badge variant="outline">
<Users
aria-label="Shared Project"
className="mr-1"
size={13}
/>
Team
</Badge>
)}
</section>
<section className="flex w-full flex-wrap items-center justify-center gap-2 sm:w-auto sm:justify-end">
<Input
disabled
className="w-full sm:w-auto"
placeholder="Search boards... (Coming Soon)"
/>
<BoardFilter />
<ProjectOptions />
{hasAccess && <BoardView />}
<ProjectOptions data={project} />
{session.user.isInstanceAdmin && (
<CreateBoard projectId={project.id} />
)}
Expand All @@ -83,7 +106,6 @@ export default async function ProjectPage({
<div>
<span className="text-md mb-2 block sm:mb-0">Boards</span>
</div>
{hasAccess && <BoardView />}
</div>
<div className="mt-4">
<BoardsList
Expand Down
45 changes: 43 additions & 2 deletions app/api/boards/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,54 @@ export async function POST(request: Request) {

return NextResponse.json(
{ message: "Validation error", errors: fieldErrors },
{ status: 400 },
{ status: 400 }
);
}

return NextResponse.json(
{ message: "Failed to create board" },
{ status: 500 },
{ status: 500 }
);
}
}

export async function GET(request: Request) {
const user = await authenticate(request);

if (!user) {
return NextResponse.json({ message: "Unauthorized" }, { status: 401 });
}

try {
const projectsWithAccess = await db.project.findMany({
where: {
OR: [
{ projectUsers: { some: { userId: user.id } } },
{ boards: { some: { boardUsers: { some: { userId: user.id } } } } },
],
},
select: {
id: true,
name: true,
slug: true,
boards: {
where: {
boardUsers: { some: { userId: user.id } },
},
select: {
id: true,
name: true,
slug: true,
},
},
},
});

return NextResponse.json(projectsWithAccess, { status: 200 });
} catch (error) {
return NextResponse.json(
{ message: "Failed to fetch projects and boards" },
{ status: 500 }
);
}
}
5 changes: 0 additions & 5 deletions app/api/invite/accept/[id]/route.ts

This file was deleted.

39 changes: 0 additions & 39 deletions app/api/invite/route.ts

This file was deleted.

84 changes: 84 additions & 0 deletions app/api/invites/accept/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { InviteStatus, ProjectBoardRole, InviteType } from "@prisma/client";

import { authOptions } from "@/lib/auth";
import { db } from "@/lib/db";

export async function POST(
req: Request,
{ params }: { params: { id: string } }
) {
try {
const session = await getServerSession(authOptions);

if (!session?.user) {
return new NextResponse("Unauthorized", { status: 401 });
}

const invite = await db.invite.findUnique({
where: { id: params.id },
include: { project: true, board: true },
});

if (!invite) {
return new NextResponse("Invite not found", { status: 404 });
}

if (
invite.recipientId !== session.user.id &&
invite.recipientEmail !== session.user.email
) {
return new NextResponse("Unauthorized", { status: 401 });
}

if (invite.status !== InviteStatus.PENDING) {
return new NextResponse("Invite is no longer valid", { status: 400 });
}

// remove from invite table
await db.invite.delete({ where: { id: params.id } });

// Add user to projectUsers and boardUsers based on invite type
if (invite.type === InviteType.PROJECT && invite.projectId) {
await db.projectUser.create({
data: {
userId: session.user.id,
projectId: invite.projectId,
role: ProjectBoardRole.MEMBER,
},
});

// Fetch all boards associated with the project
const projectBoards = await db.board.findMany({
where: { projectId: invite.projectId },
});

// Add user to all boards of the project
await Promise.all(
projectBoards.map((board) =>
db.boardUser.create({
data: {
userId: session.user.id,
boardId: board.id,
role: ProjectBoardRole.MEMBER,
},
})
)
);
} else if (invite.type === InviteType.BOARD && invite.boardId) {
await db.boardUser.create({
data: {
userId: session.user.id,
boardId: invite.boardId,
role: ProjectBoardRole.MEMBER,
},
});
}

return NextResponse.json({ message: "Invite accepted successfully" });
} catch (error) {
console.error("Error accepting invite:", error);
return new NextResponse("Internal Server Error", { status: 500 });
}
}
Loading