diff --git a/app/api/projects/[id]/comments/route.ts b/app/api/projects/[id]/comments/route.ts index 8b6973f1e..263e9400c 100644 --- a/app/api/projects/[id]/comments/route.ts +++ b/app/api/projects/[id]/comments/route.ts @@ -1,3 +1,49 @@ +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth.config"; +export async function POST(request: Request) { + try { + const url = new URL(request.url); + const pathParts = url.pathname.split("/"); + const projectId = pathParts[pathParts.length - 2]; + const session = await getServerSession(authOptions); + + if (!session || !session.user || !session.user.id) { + return NextResponse.json( + { error: "You must be logged in to comment" }, + { status: 401 }, + ); + } + + const userId = session.user.id; + const { content, parentId } = await request.json(); + if (!content || typeof content !== "string" || content.trim().length === 0) { + return NextResponse.json({ error: "Comment content is required" }, { status: 400 }); + } + + // Create the comment + const comment = await prisma.comment.create({ + data: { + content: content.trim(), + project: { connect: { id: projectId } }, + user: { connect: { id: userId } }, + parent: parentId ? { connect: { id: parentId } } : undefined, + }, + include: { + user: { select: { id: true, name: true, image: true } }, + reactions: true, + _count: { select: { reactions: true, replies: true } }, + }, + }); + + return NextResponse.json(comment); + } catch (error) { + console.error("Error posting comment:", error); + return NextResponse.json( + { error: "Failed to post comment" }, + { status: 500 }, + ); + } +} import prisma from "@/lib/prisma"; import { type NextRequest, NextResponse } from "next/server"; diff --git a/app/api/projects/[id]/route.ts b/app/api/projects/[id]/route.ts index df83e715a..752102c82 100644 --- a/app/api/projects/[id]/route.ts +++ b/app/api/projects/[id]/route.ts @@ -18,52 +18,61 @@ type ProjectWithRelations = Project & { // Only use the request parameter and extract the ID from the URL export async function GET(request: Request) { - try { - // Extract the ID from the URL path - const url = new URL(request.url); - const pathParts = url.pathname.split("/"); - const id = pathParts[pathParts.length - 1]; // Get the last segment of the path + try { + // Extract the ID from the URL path + const url = new URL(request.url); + const pathParts = url.pathname.split("/"); + const id = pathParts[pathParts.length - 1]; // Get the last segment of the path - const project = (await prisma.project.findUnique({ - where: { id }, - include: { - user: { - select: { - id: true, - name: true, - image: true, - }, - }, - votes: true, - teamMembers: true, - _count: { - select: { - votes: true, - }, - }, - }, - })) as ProjectWithRelations | null; + const project = (await prisma.project.findUnique({ + where: { id }, + include: { + user: { + select: { + id: true, + name: true, + image: true, + }, + }, + votes: true, + teamMembers: true, + _count: { + select: { + votes: true, + }, + }, + }, + })) as ProjectWithRelations | null; - if (!project) { - return NextResponse.json({ error: "Project not found" }, { status: 404 }); - } + if (!project) { + return NextResponse.json({ error: "Project not found" }, { status: 404 }); + } - // Calculate team members count manually - const teamMembersCount = project.teamMembers.length; + // Calculate upvotes, downvotes, and vote percentage + const upvotes = project.votes.filter((v) => v.type === 'UPVOTE').length; + const downvotes = project.votes.filter((v) => v.type === 'DOWNVOTE').length; + const totalVotes = upvotes + downvotes; + const votePercentage = totalVotes > 0 ? Math.round((upvotes / totalVotes) * 100) : 0; - // Return the project with the manually calculated team members count - return NextResponse.json({ - ...project, - _count: { - ...project._count, - teamMembers: teamMembersCount, - }, - }); - } catch (error) { - console.error("Error fetching project:", error); - return NextResponse.json( - { error: "Internal Server Error" }, - { status: 500 }, - ); - } + // Calculate team members count manually + const teamMembersCount = project.teamMembers.length; + + // Return the project with the manually calculated team members count and voting info + return NextResponse.json({ + ...project, + _count: { + ...project._count, + teamMembers: teamMembersCount, + }, + upvotes, + downvotes, + votePercentage, + }); + } catch (error) { + console.error("Error fetching project:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } } diff --git a/app/api/projects/[id]/vote/route.ts b/app/api/projects/[id]/vote/route.ts index 0d8506aac..55db59030 100644 --- a/app/api/projects/[id]/vote/route.ts +++ b/app/api/projects/[id]/vote/route.ts @@ -1,178 +1,178 @@ import { authOptions } from "@/lib/auth.config"; import { - notifyNewVote, - notifyProjectValidated, - notifyVoteMilestone, + notifyNewVote, + notifyProjectValidated, + notifyVoteMilestone, } from "@/lib/notifications/project"; import { prisma } from "@/lib/prisma"; import { getServerSession } from "next-auth"; import { type NextRequest, NextResponse } from "next/server"; const VOTE_THRESHOLDS = [10, 25, 50, 100]; + try { + const url = new URL(request.url); + const pathParts = url.pathname.split("/"); + const id = pathParts[pathParts.length - 1]; + const session = await getServerSession(authOptions); + + if (!session || !session.user || !session.user.id) { + return NextResponse.json( + { error: "You must be logged in to vote" }, + { status: 401 }, + ); + } -export async function POST(request: Request) { - try { - const url = new URL(request.url); - const pathParts = url.pathname.split("/"); - const id = pathParts[pathParts.length - 1]; - const session = await getServerSession(authOptions); - - if (!session || !session.user || !session.user.id) { - return NextResponse.json( - { error: "You must be logged in to vote" }, - { status: 401 }, - ); - } + const userId = session.user.id; + const projectId = id; + const { type } = await request.json(); // type: 'UPVOTE' | 'DOWNVOTE' + if (!type || (type !== 'UPVOTE' && type !== 'DOWNVOTE')) { + return NextResponse.json({ error: 'Invalid vote type' }, { status: 400 }); + } - const userId = session.user.id; - const projectId = id; + // Check if project exists + const project = await prisma.project.findUnique({ + where: { id: projectId }, + include: { + user: { + select: { id: true, name: true }, + }, + }, + }); + + if (!project) { + return NextResponse.json({ error: "Project not found" }, { status: 404 }); + } - // Check if project exists - const project = await prisma.project.findUnique({ - where: { id: projectId }, - include: { - user: { - select: { id: true, name: true }, - }, - }, + // Check if user has already voted + const existingVote = await prisma.vote.findUnique({ + where: { + projectId_userId: { + projectId, + userId, + }, + }, + }); + + if (existingVote) { + // If the vote type is the same, remove the vote (toggle) + if (existingVote.type === type) { + await prisma.vote.delete({ + where: { id: existingVote.id }, }); - - if (!project) { - return NextResponse.json({ error: "Project not found" }, { status: 404 }); - } - - // Check if user has already voted - const existingVote = await prisma.vote.findUnique({ - where: { - projectId_userId: { - projectId, - userId, - }, - }, + } else { + // Otherwise, update the vote type + await prisma.vote.update({ + where: { id: existingVote.id }, + data: { type }, }); + } + } else { + // Create a new vote + await prisma.vote.create({ + data: { + project: { connect: { id: projectId } }, + user: { connect: { id: userId } }, + type, + }, + }); + } - if (existingVote) { - // User has already voted, so remove their vote - await prisma.vote.delete({ - where: { - id: existingVote.id, - }, - }); - - const count = await prisma.vote.count({ - where: { projectId }, - }); - - return NextResponse.json({ success: true, voted: false, count }); - } - - // Create a new vote - await prisma.vote.create({ - data: { - project: { - connect: { id: projectId }, - }, - user: { - connect: { id: userId }, - }, - }, - }); + // Get updated vote counts + const upvotes = await prisma.vote.count({ where: { projectId, type: 'UPVOTE' } }); + const downvotes = await prisma.vote.count({ where: { projectId, type: 'DOWNVOTE' } }); + const count = upvotes + downvotes; + + // Notify project creator about the new vote (only for upvotes) + if (type === 'UPVOTE' && project.user.id !== userId) { + const voter = await prisma.user.findUnique({ + where: { id: userId }, + select: { name: true }, + }); + + await notifyNewVote({ + projectId, + projectTitle: project.title, + userId: project.user.id, + voterName: voter?.name || "Someone", + voteCount: upvotes, + }); + } - // Get updated vote count - const count = await prisma.vote.count({ - where: { projectId }, + // Check if a vote threshold has been reached (upvotes only) + for (const threshold of VOTE_THRESHOLDS) { + if (upvotes === threshold) { + await notifyVoteMilestone({ + projectId, + projectTitle: project.title, + userId: project.user.id, + threshold, }); - // Notify project creator about the new vote - if (project.user.id !== userId) { - const voter = await prisma.user.findUnique({ - where: { id: userId }, - select: { name: true }, - }); - - await notifyNewVote({ - projectId, - projectTitle: project.title, - userId: project.user.id, - voterName: voter?.name || "Someone", - voteCount: count, - }); - } - - // Check if a vote threshold has been reached - for (const threshold of VOTE_THRESHOLDS) { - if (count === threshold) { - await notifyVoteMilestone({ - projectId, - projectTitle: project.title, - userId: project.user.id, - threshold, - }); - - // If the project reaches a significant vote count and is still pending validation, - // automatically validate it (optional feature) - if (threshold >= 50 && project.ideaValidation === "PENDING") { - await prisma.project.update({ - where: { id: projectId }, - data: { ideaValidation: "VALIDATED" }, - }); - - // Notify about automatic validation - await notifyProjectValidated({ - projectId, - projectTitle: project.title, - userId: project.user.id, - voteCount: threshold, - }); - } - } + // If the project reaches a significant upvote count and is still pending validation, + // automatically validate it (optional feature) + if (threshold >= 50 && project.ideaValidation === "PENDING") { + await prisma.project.update({ + where: { id: projectId }, + data: { ideaValidation: "VALIDATED" }, + }); + + // Notify about automatic validation + await notifyProjectValidated({ + projectId, + projectTitle: project.title, + userId: project.user.id, + voteCount: threshold, + }); } + } + } - return NextResponse.json({ success: true, voted: true, count }); - } catch (error) { - console.error("Error voting for project:", error); - return NextResponse.json( - { error: "Failed to register vote" }, - { status: 500 }, - ); + return NextResponse.json({ success: true, voted: true, upvotes, downvotes, count }); + } catch (error) { + console.error("Error voting for project:", error); + return NextResponse.json( + { error: "Failed to register vote" }, + { status: 500 }, + ); + } } } export async function GET(request: NextRequest) { - try { - const url = new URL(request.url); - const pathParts = url.pathname.split("/"); - const id = pathParts[pathParts.length - 1]; - const projectId = id; - - // Get vote count - const count = await prisma.vote.count({ - where: { projectId }, - }); - - // Check if current user has voted - const session = await getServerSession(authOptions); - let hasVoted = false; - - if (session?.user?.id) { - const vote = await prisma.vote.findUnique({ - where: { - projectId_userId: { - projectId, - userId: session.user.id, - }, - }, - }); - - hasVoted = !!vote; - } - - return NextResponse.json({ count, hasVoted }); - } catch (error) { - console.error("Error getting vote data:", error); - return NextResponse.json( - { error: "Failed to get vote data" }, - { status: 500 }, - ); + try { + const url = new URL(request.url); + const pathParts = url.pathname.split("/"); + const id = pathParts[pathParts.length - 1]; + const projectId = id; + + // Get upvote and downvote counts + const upvotes = await prisma.vote.count({ where: { projectId, type: 'UPVOTE' } }); + const downvotes = await prisma.vote.count({ where: { projectId, type: 'DOWNVOTE' } }); + const count = upvotes + downvotes; + const votePercentage = count > 0 ? Math.round((upvotes / count) * 100) : 0; + + // Check if current user has voted and what type + const session = await getServerSession(authOptions); + let userVoteType: 'UPVOTE' | 'DOWNVOTE' | null = null; + + if (session?.user?.id) { + const vote = await prisma.vote.findUnique({ + where: { + projectId_userId: { + projectId, + userId: session.user.id, + }, + }, + }); + userVoteType = vote?.type ?? null; } + + return NextResponse.json({ upvotes, downvotes, count, votePercentage, userVoteType }); + } catch (error) { + console.error("Error getting vote data:", error); + return NextResponse.json( + { error: "Failed to get vote data" }, + { status: 500 }, + ); + } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1da72fd42..e4967c567 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -148,12 +148,18 @@ model Vote { projectId String user User @relation(fields: [userId], references: [id], onDelete: Cascade) userId String + type VoteType @default(UPVOTE) @@unique([projectId, userId]) @@index([projectId]) @@index([userId]) } +enum VoteType { + UPVOTE + DOWNVOTE +} + enum Role { USER ADMIN