Auspicious14's picture
Upload 94 files
77610ec verified
Raw
History Blame Contribute Delete
20 kB
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 });
}
};