import { Request, Response } from "express"; import prisma from "../../prisma/client"; import { Difficulty, DifficultyMode, ProjectStatus } from "@prisma/client"; import { checkAuth } from "../../middlewares/auth"; import { upLoadFiles, uploadBuffer } from "../../middlewares/file"; import { generateSlug, parseIfString } from "../../utils/helper"; import { getGeminiResponse } from "../../utils/gemini"; export const createProject = async (req: Request, res: Response) => { const userId = (req as any).user?.id; let { title, description, difficultyLevel, technologies, categories, estimatedTime, learningObjectives, resourceLinks, starterRepoUrl, difficultyModes, coverImage, milestones, } = req.body; // Parse JSON strings from FormData technologies = parseIfString(technologies); categories = parseIfString(categories); learningObjectives = parseIfString(learningObjectives); resourceLinks = parseIfString(resourceLinks); difficultyModes = parseIfString(difficultyModes); milestones = parseIfString(milestones); try { if (!userId) return res.status(401).json({ success: false, message: "Unauthorized" }); const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user || (user.role !== "ADMIN" && user.role !== "CONTRIBUTOR")) { return res.status(403).json({ success: false, message: "Only Admins or Contributors can create projects", }); } let imageUrl = coverImage; if (req.file) { imageUrl = await uploadBuffer(req.file.buffer, req.file.originalname); } else if (coverImage && !coverImage.startsWith("http")) { imageUrl = await upLoadFiles(coverImage); } const project = await prisma.project.create({ data: { title, slug: generateSlug(title), description, difficultyLevel: difficultyLevel as Difficulty, technologies, categories, estimatedTime, learningObjectives, resourceLinks: resourceLinks || [], starterRepoUrl, difficultyModes: difficultyModes || ["GUIDED", "STANDARD", "HARDCORE"], createdBy: { connect: { id: userId } }, coverImage: imageUrl, milestones: { create: milestones?.map((m: any, index: number) => ({ milestoneNumber: index + 1, title: m.title, description: m.description, hints: Array.isArray(m.hints) ? { GUIDED: m.hints } : m.hints || {}, validationCriteria: m.validationCriteria, })), }, }, include: { milestones: true }, }); res.status(201).json({ success: true, data: project }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const updateProject = async (req: Request, res: Response) => { const { id } = req.params; const userId = (req as any).user?.id; let updateData = req.body; updateData.technologies = parseIfString(updateData.technologies); updateData.categories = parseIfString(updateData.categories); updateData.learningObjectives = parseIfString(updateData.learningObjectives); updateData.resourceLinks = parseIfString(updateData.resourceLinks); updateData.difficultyModes = parseIfString(updateData.difficultyModes); const milestonesData = parseIfString(updateData.milestones); delete updateData.milestones; try { const project = await prisma.project.findUnique({ where: { id }, include: { milestones: true, userProjects: true }, }); if (!project) { return res.status(404).json({ success: false, message: "Project not found", }); } const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user || (user.role !== "ADMIN" && project.createdById !== userId)) { return res.status(403).json({ success: false, message: "Unauthorized to update this project", }); } if (req.file) { updateData.coverImage = await uploadBuffer( req.file.buffer, req.file.originalname ); } else if ( updateData.coverImage && !updateData.coverImage.startsWith("http") ) { updateData.coverImage = await upLoadFiles(updateData.coverImage); } // Update slug if title changed if (updateData.title && updateData.title !== project.title) { updateData.slug = generateSlug(updateData.title); } // Clean up any undefined or empty string values Object.keys(updateData).forEach((key) => { if (updateData[key] === undefined || updateData[key] === "") { delete updateData[key]; } }); // Update project with milestones handled separately const updatedProject = await prisma.project.update({ where: { id }, data: { ...updateData, ...(milestonesData && { milestones: { deleteMany: {}, create: milestonesData .filter((m: any) => m.title && m.description) .map((m: any, index: number) => ({ milestoneNumber: index + 1, title: m.title, description: m.description, hints: typeof m.hints === "object" ? m.hints : {}, validationCriteria: typeof m.validationCriteria === "string" ? m.validationCriteria : JSON.stringify(m.validationCriteria || {}), })), }, }), }, include: { milestones: { orderBy: { milestoneNumber: "asc" }, }, createdBy: { select: { id: true, firstName: true, lastName: true, email: true, avatarUrl: true, }, }, userProjects: { where: { userId: user.id } }, }, }); // Build progressByMode let progressByMode: any = {}; let userProgress = null; if (updatedProject.userProjects && updatedProject.userProjects.length > 0) { for (const up of updatedProject.userProjects) { const completedMilestones = await prisma.userMilestone.findMany({ where: { userId: user.id, userProjectId: up.id, }, select: { milestoneId: true }, }); progressByMode[up.difficultyModeChosen] = { status: up.status, completedMilestones: completedMilestones.map((m) => m.milestoneId), userProjectId: up.id, startedAt: up.startedAt, completedAt: up.completedAt, }; } userProgress = updatedProject.userProjects[updatedProject.userProjects.length - 1]; } res.json({ success: true, data: { ...updatedProject, progressByMode, userProgress, }, }); } catch (error: any) { console.error("Update project error:", error); res.status(500).json({ success: false, message: error.message, }); } }; export const deleteProject = async (req: Request, res: Response) => { const { id } = req.params; const userId = (req as any).user?.id; try { const project = await prisma.project.findUnique({ where: { id } }); if (!project) return res .status(404) .json({ success: false, message: "Project not found" }); const user = await prisma.user.findUnique({ where: { id: userId } }); if (!user || user.role !== "ADMIN") { return res .status(403) .json({ success: false, message: "Only Admins can delete projects" }); } await prisma.project.delete({ where: { id } }); res.json({ success: true, message: "Project deleted successfully" }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const getProjects = async (req: Request, res: Response) => { const { difficulty, tech, category, title } = req.query; try { const where: any = {}; if (difficulty) where.difficultyLevel = difficulty as Difficulty; if (tech) where.technologies = { has: tech as string }; if (category) where.categories = { has: category as string }; if (title) where.title = { contains: title as string, mode: "insensitive" }; const projects = await prisma.project.findMany({ where, include: { createdBy: { select: { firstName: true, lastName: true }, }, }, orderBy: { createdAt: "desc" }, }); res.json({ success: true, data: projects }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const getProjectById = async (req: Request, res: Response) => { const { id } = req.params; try { const user = await checkAuth(req); const project = await prisma.project.findUnique({ where: { id }, include: { milestones: { orderBy: { milestoneNumber: "asc" } }, createdBy: { select: { id: true, firstName: true, lastName: true, avatarUrl: true, }, }, userProjects: user ? { where: { userId: user.id } } : false, }, }); if (!project) return res .status(404) .json({ success: false, message: "Project not found" }); // If user is authenticated, fetch their progress and completed milestones for each mode let progressByMode: any = {}; let userProgress = null; if (user && project.userProjects && project.userProjects.length > 0) { for (const up of project.userProjects) { const completedMilestones = await prisma.userMilestone.findMany({ where: { userId: user.id, userProjectId: up.id, }, select: { milestoneId: true }, }); progressByMode[up.difficultyModeChosen] = { status: up.status, completedMilestones: completedMilestones.map((m) => m.milestoneId), userProjectId: up.id, startedAt: up.startedAt, completedAt: up.completedAt, }; } // Set the most recent userProject as the default userProgress userProgress = project.userProjects[project.userProjects.length - 1]; } res.json({ success: true, data: { ...project, progressByMode, userProgress, }, }); } catch (error: any) { console.error("Get project error:", error); res.status(500).json({ success: false, message: error.message }); } }; // User Progress Endpoints export const startProject = async (req: Request, res: Response) => { const { id: projectId } = req.params; const userId = (req as any).user?.id; const { difficultyModeChosen } = req.body; try { const mode = (difficultyModeChosen as DifficultyMode) || "STANDARD"; const existingProgress = await prisma.userProject.findUnique({ where: { userId_projectId_difficultyModeChosen: { userId, projectId, difficultyModeChosen: mode, }, }, }); if (existingProgress) { return res.status(400).json({ success: false, message: "Project already started in this mode", }); } const progress = await prisma.userProject.create({ data: { userId, projectId, difficultyModeChosen: mode, status: "IN_PROGRESS", }, }); res.status(201).json({ success: true, data: progress }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const updateProgress = async (req: Request, res: Response) => { const { id: projectId } = req.params; const userId = (req as any).user?.id; const { status, repoUrl, difficultyMode } = req.body; try { const progress = await prisma.userProject.update({ where: { userId_projectId_difficultyModeChosen: { userId, projectId, difficultyModeChosen: difficultyMode as DifficultyMode, }, }, data: { status: status as ProjectStatus, repoUrl }, }); res.json({ success: true, data: progress }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const completeProject = async (req: Request, res: Response) => { const { id: projectId } = req.params; const userId = (req as any).user?.id; const { repoUrl, difficultyMode } = req.body; try { const progress = await prisma.userProject.update({ where: { userId_projectId_difficultyModeChosen: { userId, projectId, difficultyModeChosen: difficultyMode as DifficultyMode, }, }, data: { status: "COMPLETED", completedAt: new Date(), repoUrl, }, }); // Award XP (simple example) await prisma.user.update({ where: { id: userId }, data: { xp: { increment: 100 } }, }); // Update project completion rate const totalStarted = await prisma.userProject.count({ where: { projectId }, }); const totalCompleted = await prisma.userProject.count({ where: { projectId, status: "COMPLETED" }, }); await prisma.project.update({ where: { id: projectId }, data: { completionRate: (totalCompleted / totalStarted) * 100, submissionCount: { increment: 1 }, }, }); res.json({ success: true, data: progress }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const getUserProgress = async (req: Request, res: Response) => { const userId = (req as any).user?.id; try { const progress = await prisma.userProject.findMany({ where: { userId }, include: { project: { include: { milestones: { orderBy: { milestoneNumber: "asc" }, }, createdBy: { select: { firstName: true, lastName: true, avatarUrl: true }, }, }, }, // Correct relation name from schema.prisma milestoneProgress: { select: { milestoneId: true, completedAt: true }, }, }, }); res.json({ success: true, data: progress }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; // Submissions export const submitProject = async (req: Request, res: Response) => { const { id: projectId } = req.params; const userId = (req as any).user?.id; const { repoUrl } = req.body; try { const submission = await prisma.submission.create({ data: { userId, projectId, repoUrl, }, }); res.status(201).json({ success: true, data: submission }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const completeMilestone = async (req: Request, res: Response) => { const { id: projectId, milestoneId } = req.params; const userId = (req as any).user?.id; const { difficultyMode } = req.body; try { const userProject = await prisma.userProject.findUnique({ where: { userId_projectId_difficultyModeChosen: { userId, projectId, difficultyModeChosen: difficultyMode as DifficultyMode, }, }, }); if (!userProject) { return res.status(400).json({ success: false, message: "Project not started by user in this mode", }); } const existingMilestone = await prisma.userMilestone.findUnique({ where: { userId_milestoneId_userProjectId: { userId, milestoneId, userProjectId: userProject.id, }, }, }); if (existingMilestone) { return res.status(400).json({ success: false, message: "Milestone already completed in this mode", }); } // 1. Create the milestone completion record const milestone = await prisma.userMilestone.create({ data: { userId, milestoneId, userProjectId: userProject.id, }, }); // 2. CHECK FOR PROJECT COMPLETION const totalMilestones = await prisma.projectMilestone.count({ where: { projectId }, }); const completedCount = await prisma.userMilestone.count({ where: { userId, userProjectId: userProject.id, }, }); // 3. AUTO-COMPLETE PROJECT if all milestones are done if (completedCount === totalMilestones) { await prisma.userProject.update({ where: { id: userProject.id }, data: { status: "COMPLETED", completedAt: new Date(), }, }); await prisma.user.update({ where: { id: userId }, data: { xp: { increment: 50 } }, }); } res.status(201).json({ success: true, data: milestone }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const getFeaturedProjects = async (req: Request, res: Response) => { try { const featuredProjects = await prisma.project.findMany({ take: 4, orderBy: { submissionCount: "desc" }, include: { createdBy: { select: { firstName: true, lastName: true }, }, }, }); res.json({ success: true, data: featuredProjects }); } catch (error: any) { res.status(500).json({ success: false, message: error.message }); } }; export const submitCode = async (req: Request, res: Response) => { const userId = (req as any).user?.id; const { projectId, milestoneId, code, language } = req.body; try { // Fetch milestone details for validation criteria const milestone = await prisma.projectMilestone.findUnique({ where: { id: milestoneId }, include: { project: true }, }); if (!milestone) { return res.status(404).json({ success: false, message: "Milestone not found", }); } // AI-powered code review const reviewPrompt = ` You are a code reviewer for a learning platform. Review this code submission: Project: ${milestone.project.title} Milestone: ${milestone.title} Description: ${milestone.description} Validation Criteria: ${milestone.validationCriteria || "General best practices"} Code: \`\`\`${language} ${code} \`\`\` Provide: 1. Does this code meet the milestone requirements? (YES/NO) 2. Code quality score (0-100) 3. Specific feedback on what's good 4. Areas for improvement 5. Security concerns (if any) Format as JSON: { "meetsRequirements": boolean, "score": number, "strengths": string[], "improvements": string[], "securityIssues": string[] } `; const reviewResult = await getGeminiResponse(reviewPrompt); // Parse AI response let parsedReview; try { // Extract JSON from potential markdown code blocks const jsonMatch = reviewResult.match(/```json\n?([\s\S]*?)\n?```/); const jsonStr = jsonMatch ? jsonMatch[1] : reviewResult; parsedReview = JSON.parse(jsonStr); } catch (e) { parsedReview = { meetsRequirements: false, score: 0, strengths: [], improvements: ["Unable to parse AI review"], securityIssues: [], }; } // Save code submission const submission = await prisma.submission.create({ data: { userId, projectId, repoUrl: `code-submission-${Date.now()}`, // Placeholder feedback: JSON.stringify(parsedReview), score: parsedReview.score, aiReviewData: parsedReview, }, }); res.json({ success: true, data: { submission, review: parsedReview, }, }); } catch (error: any) { console.error("Code submission error:", error); res.status(500).json({ success: false, message: error.message }); } };