diff --git a/app/api/boards/[id]/notes/route.ts b/app/api/boards/[id]/notes/route.ts
index 22c161a0f..8caa95854 100644
--- a/app/api/boards/[id]/notes/route.ts
+++ b/app/api/boards/[id]/notes/route.ts
@@ -23,6 +23,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
id: true,
isPublic: true,
organizationId: true,
+ createdBy: true,
notes: {
where: {
deletedAt: null, // Only include non-deleted notes
@@ -63,18 +64,25 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
- const user = await db.user.findUnique({
- where: { id: session.user.id },
- select: {
- organizationId: true,
+ const userInOrg = await db.user.findFirst({
+ where: {
+ id: session.user.id,
+ organizationId: board.organizationId,
},
});
- if (!user?.organizationId) {
- return NextResponse.json({ error: "No organization found" }, { status: 403 });
+ if (!userInOrg) {
+ return NextResponse.json({ error: "Access denied" }, { status: 403 });
}
- if (board.organizationId !== user.organizationId) {
+ const boardShare = await db.boardShare.findFirst({
+ where: {
+ boardId: boardId,
+ userId: session.user.id,
+ },
+ });
+
+ if (!boardShare && board.createdBy !== session.user.id) {
return NextResponse.json({ error: "Access denied" }, { status: 403 });
}
@@ -115,9 +123,21 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
}
const { color, checklistItems } = validatedBody;
- // Verify user has access to this board (same organization)
+ // Verify user has access to this board (either through organization membership or explicit board share)
const user = await db.user.findUnique({
- where: { id: session.user.id },
+ where: {
+ id: session.user.id,
+ OR: [
+ { organizationId: { not: null } },
+ {
+ boardShares: {
+ some: {
+ boardId: boardId,
+ },
+ },
+ },
+ ],
+ },
select: {
organizationId: true,
organization: {
@@ -130,8 +150,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
},
});
- if (!user?.organizationId) {
- return NextResponse.json({ error: "No organization found" }, { status: 403 });
+ if (!user) {
+ return NextResponse.json({ error: "Access denied" }, { status: 403 });
}
const board = await db.board.findUnique({
@@ -140,6 +160,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
id: true,
name: true,
organizationId: true,
+ createdBy: true,
sendSlackUpdates: true,
},
});
@@ -148,7 +169,25 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: "Board not found" }, { status: 404 });
}
- if (board.organizationId !== user.organizationId) {
+ const userInOrg = await db.user.findFirst({
+ where: {
+ id: session.user.id,
+ organizationId: board.organizationId,
+ },
+ });
+
+ if (!userInOrg) {
+ return NextResponse.json({ error: "Access denied" }, { status: 403 });
+ }
+
+ const boardShare = await db.boardShare.findFirst({
+ where: {
+ boardId: boardId,
+ userId: session.user.id,
+ },
+ });
+
+ if (!boardShare && board.createdBy !== session.user.id) {
return NextResponse.json({ error: "Access denied" }, { status: 403 });
}
diff --git a/app/api/boards/[id]/route.ts b/app/api/boards/[id]/route.ts
index 304a6c8f5..b1912cca5 100644
--- a/app/api/boards/[id]/route.ts
+++ b/app/api/boards/[id]/route.ts
@@ -35,7 +35,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
- // Check if user is member of the organization
+ // Check if user is in the organization (required for board sharing)
const userInOrg = await db.user.findFirst({
where: {
id: session.user.id,
@@ -47,6 +47,17 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: "Access denied" }, { status: 403 });
}
+ const boardShare = await db.boardShare.findFirst({
+ where: {
+ boardId: boardId,
+ userId: session.user.id,
+ },
+ });
+
+ if (!boardShare && board.createdBy !== session.user.id) {
+ return NextResponse.json({ error: "Access denied" }, { status: 403 });
+ }
+
// Return board data without sensitive organization member details
const { organization, ...boardData } = board;
@@ -105,15 +116,20 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: "Board not found" }, { status: 404 });
}
- // Check if user is member of the organization and get admin status
+ // Check if user is in the organization and has admin status (for editing/deleting boards)
+ // OR if user is the board creator
const currentUser = await db.user.findFirst({
where: {
id: session.user.id,
- organizationId: board.organizationId,
+ OR: [
+ { organizationId: board.organizationId },
+ { id: board.createdBy }, // Allow board creator to edit their own board
+ ],
},
select: {
id: true,
isAdmin: true,
+ organizationId: true,
},
});
@@ -121,11 +137,14 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({ error: "Access denied" }, { status: 403 });
}
+ // For admin checks, user must be organization admin OR board creator
+ const isOrgAdmin = currentUser.isAdmin || board.createdBy === session.user.id;
+
// For name/description/isPublic updates, check if user can edit this board (board creator or admin)
if (
(name !== undefined || description !== undefined || isPublic !== undefined) &&
board.createdBy !== session.user.id &&
- !currentUser.isAdmin
+ !isOrgAdmin
) {
return NextResponse.json(
{ error: "Only the board creator or admin can edit this board" },
@@ -196,15 +215,20 @@ export async function DELETE(
return NextResponse.json({ error: "Board not found" }, { status: 404 });
}
- // Check if user is member of the organization and get admin status
+ // Check if user is in the organization and has admin status (for editing/deleting boards)
+ // OR if user is the board creator
const currentUser = await db.user.findFirst({
where: {
id: session.user.id,
- organizationId: board.organizationId,
+ OR: [
+ { organizationId: board.organizationId },
+ { id: board.createdBy }, // Allow board creator to edit their own board
+ ],
},
select: {
id: true,
isAdmin: true,
+ organizationId: true,
},
});
@@ -212,8 +236,11 @@ export async function DELETE(
return NextResponse.json({ error: "Access denied" }, { status: 403 });
}
+ // For admin checks, user must be organization admin OR board creator
+ const isOrgAdmin = currentUser.isAdmin || board.createdBy === session.user.id;
+
// Check if user can delete this board (board creator or admin)
- if (board.createdBy !== session.user.id && !currentUser.isAdmin) {
+ if (board.createdBy !== session.user.id && !isOrgAdmin) {
return NextResponse.json(
{ error: "Only the board creator or admin can delete this board" },
{ status: 403 }
diff --git a/app/api/boards/[id]/share/route.ts b/app/api/boards/[id]/share/route.ts
new file mode 100644
index 000000000..a1b1550a8
--- /dev/null
+++ b/app/api/boards/[id]/share/route.ts
@@ -0,0 +1,236 @@
+import { auth } from "@/auth";
+import { db } from "@/lib/db";
+import { NextRequest, NextResponse } from "next/server";
+import { z } from "zod";
+
+// Helper function to update organization-level sharing status when board sharing changes
+async function updateOrganizationSharingStatus(organizationId: string) {
+ try {
+ // Get all boards in the organization
+ const boards = await db.board.findMany({
+ where: { organizationId },
+ select: { id: true },
+ });
+
+ if (boards.length === 0) return;
+
+ // Get all users in the organization
+ const users = await db.user.findMany({
+ where: { organizationId },
+ select: { id: true },
+ });
+
+ // Get current board shares for all boards in the organization
+ const allBoardShares = await db.boardShare.findMany({
+ where: {
+ boardId: { in: boards.map((b) => b.id) },
+ },
+ select: {
+ boardId: true,
+ userId: true,
+ },
+ });
+
+ // Group shares by user
+ const sharesByUser = new Map>();
+ allBoardShares.forEach((share) => {
+ if (!sharesByUser.has(share.userId)) {
+ sharesByUser.set(share.userId, new Set());
+ }
+ sharesByUser.get(share.userId)!.add(share.boardId);
+ });
+
+ // For each user, check if they should have "share all boards" status
+ for (const user of users) {
+ const sharedBoardIds = sharesByUser.get(user.id) || new Set();
+ const shouldShareAllBoards = boards.length > 0 && sharedBoardIds.size === boards.length;
+
+ // If user should share all boards but doesn't have shares for all boards, add them
+ if (shouldShareAllBoards) {
+ const missingBoardIds = boards.map((b) => b.id).filter((id) => !sharedBoardIds.has(id));
+
+ if (missingBoardIds.length > 0) {
+ await db.boardShare.createMany({
+ data: missingBoardIds.map((boardId) => ({
+ boardId,
+ userId: user.id,
+ })),
+ });
+ }
+ }
+ }
+ } catch (error) {
+ console.error("Error updating organization sharing status:", error);
+ }
+}
+
+// Get sharing status for a board
+export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+ try {
+ const session = await auth();
+
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const boardId = (await params).id;
+
+ // Check if board exists and user has access
+ const board = await db.board.findUnique({
+ where: { id: boardId },
+ include: { organization: true },
+ });
+
+ if (!board) {
+ return NextResponse.json({ error: "Board not found" }, { status: 404 });
+ }
+
+ // Check if user is member of the organization and get admin status
+ const currentUser = await db.user.findFirst({
+ where: {
+ id: session.user.id,
+ organizationId: board.organizationId,
+ },
+ select: {
+ id: true,
+ isAdmin: true,
+ },
+ });
+
+ if (!currentUser) {
+ return NextResponse.json({ error: "Access denied" }, { status: 403 });
+ }
+
+ // Get all organization members
+ const organizationMembers = await db.user.findMany({
+ where: { organizationId: board.organizationId },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ isAdmin: true,
+ },
+ orderBy: { name: "asc" },
+ });
+
+ // Get current board shares for this board
+ const boardShares = await db.boardShare.findMany({
+ where: { boardId },
+ select: { userId: true },
+ });
+
+ const sharedUserIds = new Set(boardShares.map((share) => share.userId));
+
+ // Combine member data with sharing status
+ const membersWithSharing = organizationMembers.map((member) => ({
+ id: member.id,
+ name: member.name,
+ email: member.email,
+ isAdmin: member.isAdmin,
+ isShared: sharedUserIds.has(member.id),
+ }));
+
+ return NextResponse.json({
+ members: membersWithSharing,
+ boardCreator: board.createdBy,
+ });
+ } catch (error) {
+ console.error("Error fetching board sharing:", error);
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
+ }
+}
+
+// Update sharing for specific users on this board
+export async function PUT(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
+ try {
+ const session = await auth();
+
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const boardId = (await params).id;
+ const body = await request.json();
+
+ const schema = z.object({
+ userIds: z.array(z.string()),
+ });
+
+ const validatedBody = schema.parse(body);
+ const { userIds } = validatedBody;
+
+ // Check if board exists and user has access
+ const board = await db.board.findUnique({
+ where: { id: boardId },
+ include: { organization: true },
+ });
+
+ if (!board) {
+ return NextResponse.json({ error: "Board not found" }, { status: 404 });
+ }
+
+ // Check if user is member of the organization and get admin status
+ const currentUser = await db.user.findFirst({
+ where: {
+ id: session.user.id,
+ organizationId: board.organizationId,
+ },
+ select: {
+ id: true,
+ isAdmin: true,
+ },
+ });
+
+ if (!currentUser) {
+ return NextResponse.json({ error: "Access denied" }, { status: 403 });
+ }
+
+ // Only admins can update board sharing
+ if (!currentUser.isAdmin) {
+ return NextResponse.json({ error: "Only admins can update board sharing" }, { status: 403 });
+ }
+
+ // Validate that all userIds are in the same organization
+ const validUsers = await db.user.findMany({
+ where: {
+ id: { in: userIds },
+ organizationId: board.organizationId,
+ },
+ select: { id: true },
+ });
+
+ if (validUsers.length !== userIds.length) {
+ return NextResponse.json(
+ { error: "Some users are not in the organization" },
+ { status: 400 }
+ );
+ }
+
+ // Update board shares - delete existing and create new ones
+ await db.$transaction([
+ db.boardShare.deleteMany({ where: { boardId } }),
+ ...userIds.map((userId) =>
+ db.boardShare.create({
+ data: {
+ boardId,
+ userId,
+ },
+ })
+ ),
+ ]);
+
+ // Check if this affects any user's "share all boards" status and update organization settings
+ await updateOrganizationSharingStatus(board.organizationId);
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: "Validation failed", details: error.errors },
+ { status: 400 }
+ );
+ }
+ console.error("Error updating board sharing:", error);
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
+ }
+}
diff --git a/app/api/boards/route.ts b/app/api/boards/route.ts
index 0423e7f34..ecc04385c 100644
--- a/app/api/boards/route.ts
+++ b/app/api/boards/route.ts
@@ -23,9 +23,21 @@ export async function GET() {
return NextResponse.json({ error: "No organization found" }, { status: 404 });
}
- // Get all boards for the organization
+ // Get boards the user has access to (public boards, explicitly shared boards, or boards created by the user)
const boards = await db.board.findMany({
- where: { organizationId: user.organizationId },
+ where: {
+ OR: [
+ { isPublic: true },
+ {
+ shares: {
+ some: {
+ userId: session.user.id,
+ },
+ },
+ },
+ { createdBy: session.user.id }, // Boards created by the user
+ ],
+ },
select: {
id: true,
name: true,
@@ -34,6 +46,12 @@ export async function GET() {
createdBy: true,
createdAt: true,
updatedAt: true,
+ organization: {
+ select: {
+ id: true,
+ name: true,
+ },
+ },
_count: {
select: {
notes: {
@@ -152,6 +170,14 @@ export async function POST(request: NextRequest) {
},
});
+ // Automatically create a board sharing record for the creator
+ await db.boardShare.create({
+ data: {
+ boardId: board.id,
+ userId: session.user.id,
+ },
+ });
+
return NextResponse.json({ board }, { status: 201 });
} catch (error) {
console.error("Error creating board:", error);
diff --git a/app/api/organization/share/route.ts b/app/api/organization/share/route.ts
new file mode 100644
index 000000000..797ded510
--- /dev/null
+++ b/app/api/organization/share/route.ts
@@ -0,0 +1,249 @@
+import { auth } from "@/auth";
+import { db } from "@/lib/db";
+import { NextRequest, NextResponse } from "next/server";
+import { z } from "zod";
+
+// Helper function to update board-level sharing status when organization sharing changes
+async function updateBoardSharingStatus(orgId: string) {
+ try {
+ // Get all boards in the organization
+ const boards = await db.board.findMany({
+ where: { organizationId: orgId },
+ select: { id: true },
+ });
+
+ if (boards.length === 0) return;
+ } catch (error) {
+ console.error("Error updating board sharing status:", error);
+ }
+}
+
+// Get sharing status for all organization members across all boards
+export async function GET() {
+ try {
+ const session = await auth();
+
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ // Get user with organization
+ const user = await db.user.findUnique({
+ where: { id: session.user.id },
+ select: {
+ organizationId: true,
+ isAdmin: true,
+ },
+ });
+
+ if (!user?.organizationId) {
+ return NextResponse.json({ error: "No organization found" }, { status: 404 });
+ }
+
+ // Only admins can view organization sharing
+ if (!user.isAdmin) {
+ return NextResponse.json(
+ { error: "Only admins can view organization sharing" },
+ { status: 403 }
+ );
+ }
+
+ // Get all organization members
+ const organizationMembers = await db.user.findMany({
+ where: { organizationId: user.organizationId },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ isAdmin: true,
+ },
+ orderBy: { name: "asc" },
+ });
+
+ // Get all boards in the organization
+ const boards = await db.board.findMany({
+ where: { organizationId: user.organizationId },
+ select: {
+ id: true,
+ name: true,
+ },
+ orderBy: { name: "asc" },
+ });
+
+ // Get all board shares for this organization
+ const boardShares = await db.boardShare.findMany({
+ where: {
+ board: {
+ organizationId: user.organizationId,
+ },
+ },
+ select: {
+ boardId: true,
+ userId: true,
+ },
+ });
+
+ // Create a map of board shares by user
+ const sharesByUser = new Map>();
+ boardShares.forEach((share) => {
+ if (!sharesByUser.has(share.userId)) {
+ sharesByUser.set(share.userId, new Set());
+ }
+ sharesByUser.get(share.userId)!.add(share.boardId);
+ });
+
+ // For each member, determine if they have all boards shared
+ const membersWithSharingStatus = organizationMembers.map((member) => {
+ const sharedBoardIds = sharesByUser.get(member.id) || new Set();
+ const allBoardsShared = boards.length > 0 && sharedBoardIds.size === boards.length;
+
+ return {
+ id: member.id,
+ name: member.name,
+ email: member.email,
+ isAdmin: member.isAdmin,
+ shareAllBoards: allBoardsShared,
+ sharedBoardIds: Array.from(sharedBoardIds),
+ totalBoards: boards.length,
+ };
+ });
+
+ return NextResponse.json({
+ members: membersWithSharingStatus,
+ boards: boards.map((board) => ({ id: board.id, name: board.name })),
+ });
+ } catch (error) {
+ console.error("Error fetching organization sharing:", error);
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
+ }
+}
+
+// Update sharing for specific users across all boards
+export async function PUT(request: NextRequest) {
+ try {
+ const session = await auth();
+
+ if (!session?.user?.id) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+ }
+
+ const body = await request.json();
+
+ const schema = z.object({
+ userSharing: z.array(
+ z.object({
+ userId: z.string(),
+ shareAllBoards: z.boolean(),
+ sharedBoardIds: z.array(z.string()).optional(),
+ })
+ ),
+ });
+
+ const validatedBody = schema.parse(body);
+ const { userSharing } = validatedBody;
+
+ // Get user with organization
+ const user = await db.user.findUnique({
+ where: { id: session.user.id },
+ select: {
+ organizationId: true,
+ isAdmin: true,
+ },
+ });
+
+ if (!user?.organizationId) {
+ return NextResponse.json({ error: "No organization found" }, { status: 404 });
+ }
+
+ const organizationId = user.organizationId;
+
+ // Only admins can update organization sharing
+ if (!user.isAdmin) {
+ return NextResponse.json(
+ { error: "Only admins can update organization sharing" },
+ { status: 403 }
+ );
+ }
+
+ // Get all boards in the organization
+ const boards = await db.board.findMany({
+ where: { organizationId },
+ select: { id: true },
+ });
+
+ const boardIds = boards.map((board) => board.id);
+
+ // Validate that all userIds are in the same organization
+ const validUsers = await db.user.findMany({
+ where: {
+ id: { in: userSharing.map((us) => us.userId) },
+ organizationId,
+ },
+ select: { id: true },
+ });
+
+ if (validUsers.length !== userSharing.length) {
+ return NextResponse.json(
+ { error: "Some users are not in the organization" },
+ { status: 400 }
+ );
+ }
+
+ // Update sharing for each user
+ for (const userShare of userSharing) {
+ if (userShare.shareAllBoards) {
+ // Share all boards with this user
+ await db.boardShare.deleteMany({
+ where: {
+ boardId: { in: boardIds },
+ userId: userShare.userId,
+ },
+ });
+
+ await db.boardShare.createMany({
+ data: boardIds.map((boardId) => ({
+ boardId,
+ userId: userShare.userId,
+ })),
+ });
+ } else if (userShare.sharedBoardIds && userShare.sharedBoardIds.length > 0) {
+ // Share specific boards with this user
+ await db.boardShare.deleteMany({
+ where: {
+ boardId: { in: boardIds },
+ userId: userShare.userId,
+ },
+ });
+
+ await db.boardShare.createMany({
+ data: userShare.sharedBoardIds.map((boardId) => ({
+ boardId,
+ userId: userShare.userId,
+ })),
+ });
+ } else {
+ // Share no boards with this user
+ await db.boardShare.deleteMany({
+ where: {
+ boardId: { in: boardIds },
+ userId: userShare.userId,
+ },
+ });
+ }
+ }
+
+ // Check if this affects any user's board-specific sharing and update accordingly
+ await updateBoardSharingStatus(organizationId);
+
+ return NextResponse.json({ success: true });
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return NextResponse.json(
+ { error: "Validation failed", details: error.errors },
+ { status: 400 }
+ );
+ }
+ console.error("Error updating organization sharing:", error);
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
+ }
+}
diff --git a/app/boards/[id]/page.tsx b/app/boards/[id]/page.tsx
index 6a637d532..07ea7b4a3 100644
--- a/app/boards/[id]/page.tsx
+++ b/app/boards/[id]/page.tsx
@@ -16,6 +16,7 @@ import {
XIcon,
StickyNote,
Plus,
+ Users,
} from "lucide-react";
import Link from "next/link";
import { BetaBadge } from "@/components/ui/beta-badge";
@@ -101,6 +102,13 @@ export default function BoardPage({ params }: { params: Promise<{ id: string }>
});
const [copiedPublicUrl, setCopiedPublicUrl] = useState(false);
const [deleteConfirmDialog, setDeleteConfirmDialog] = useState(false);
+ const [boardSharing, setBoardSharing] = useState<
+ { id: string; name: string; email: string; isShared: boolean }[]
+ >([]);
+ const [boardCreator, setBoardCreator] = useState(null);
+ const [memberSearchTerm, setMemberSearchTerm] = useState("");
+ const [updatingSharing, setUpdatingSharing] = useState(null);
+ const [boardSharingDialog, setBoardSharingDialog] = useState(false);
const boardRef = useRef(null);
const router = useRouter();
const searchParams = useSearchParams();
@@ -212,6 +220,12 @@ export default function BoardPage({ params }: { params: Promise<{ id: string }>
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [boardId]);
+ useEffect(() => {
+ if (boardSettingsDialog && boardId && boardId !== "all-notes" && boardId !== "archive") {
+ fetchBoardSharing();
+ }
+ }, [boardSettingsDialog, boardId]);
+
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedSearchTerm(searchTerm);
@@ -640,6 +654,79 @@ export default function BoardPage({ params }: { params: Promise<{ id: string }>
}
};
+ const fetchBoardSharing = async () => {
+ if (!boardId || boardId === "all-notes" || boardId === "archive") return;
+
+ try {
+ const response = await fetch(`/api/boards/${boardId}/share`);
+ if (response.ok) {
+ const data = await response.json();
+ setBoardSharing(data.members || []);
+ setBoardCreator(data.boardCreator || null);
+ }
+ } catch (error) {
+ console.error("Error fetching board sharing:", error);
+ }
+ };
+
+ const handleToggleMemberSharing = async (memberId: string, isShared: boolean) => {
+ if (!boardId || boardId === "all-notes" || boardId === "archive") return;
+
+ setUpdatingSharing(memberId);
+ try {
+ const currentSharedIds = boardSharing
+ .filter((member) => member.isShared)
+ .map((member) => member.id);
+
+ const newSharedIds = isShared
+ ? [...currentSharedIds, memberId]
+ : currentSharedIds.filter((id) => id !== memberId);
+
+ // Use individual update function that doesn't close the dialog
+ await handleUpdateBoardSharingIndividual(newSharedIds);
+ } catch (error) {
+ console.error("Error toggling member sharing:", error);
+ } finally {
+ setUpdatingSharing(null);
+ }
+ };
+
+ const handleUpdateBoardSharingIndividual = async (userIds: string[]) => {
+ if (!boardId || boardId === "all-notes" || boardId === "archive") return;
+
+ setUpdatingSharing("bulk");
+ try {
+ const response = await fetch(`/api/boards/${boardId}/share`, {
+ method: "PUT",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ userIds }),
+ });
+
+ if (response.ok) {
+ await fetchBoardSharing();
+ // Don't close dialog for individual toggles
+ } else {
+ const errorData = await response.json();
+ setErrorDialog({
+ open: true,
+ title: "Failed to update board sharing",
+ description: errorData.error || "Failed to update board sharing",
+ });
+ }
+ } catch (error) {
+ console.error("Error updating board sharing:", error);
+ setErrorDialog({
+ open: true,
+ title: "Failed to update board sharing",
+ description: "Failed to update board sharing",
+ });
+ } finally {
+ setUpdatingSharing(null);
+ }
+ };
+
const handleDeleteBoard = async () => {
try {
const response = await fetch(`/api/boards/${boardId}`, {
@@ -1196,6 +1283,56 @@ export default function BoardPage({ params }: { params: Promise<{ id: string }>
+ {/* Board Sharing Section */}
+
+
+
+
+
+
+
+ Control which team members can access this board
+
+
+
+ {/* Quick preview of shared members */}
+ {boardSharing.length > 0 && (
+
+
+ Currently shared with:
+
+
+ {boardSharing
+ .filter((member) => member.isShared)
+ .slice(0, 3)
+ .map((member) => (
+
+ {member.name || member.email}
+
+ ))}
+ {boardSharing.filter((member) => member.isShared).length > 3 && (
+
+ +{boardSharing.filter((member) => member.isShared).length - 3} more
+
+ )}
+
+
+ )}
+
+