From 508867ad13998e4b08e88d220e7a502257436797 Mon Sep 17 00:00:00 2001 From: Mayank Arya Date: Sun, 6 Sep 2026 00:43:59 +0530 Subject: [PATCH 1/5] Added View and Download Option --- admin/src/apis/courses.js | 18 +++++++ admin/src/pages/CourseDashboard.jsx | 53 +++++++++++++++++-- client/.env.example | 2 - .../contribution/contribution.model.js | 2 +- 4 files changed, 68 insertions(+), 7 deletions(-) delete mode 100644 client/.env.example diff --git a/admin/src/apis/courses.js b/admin/src/apis/courses.js index 2308f46c..886c3f92 100644 --- a/admin/src/apis/courses.js +++ b/admin/src/apis/courses.js @@ -246,3 +246,21 @@ export const deleteNode = async(type,id) => } } + +export const getFileDownloadUrl = async (fileId) => { + try { + const response = await fetch(`${API_BASE_URL}api/file/download/${fileId}`, { + headers: { + Authorization: "Bearer admin-coursehub-cc23-golang", + }, + credentials: "include", + }); + if (!response.ok) { + throw new Error("Failed to get download link"); + } + return await response.json(); // { url: "..." } + } catch (error) { + console.error("Error getting download link:", error); + throw error; + } +}; \ No newline at end of file diff --git a/admin/src/pages/CourseDashboard.jsx b/admin/src/pages/CourseDashboard.jsx index c45885c6..28745bea 100644 --- a/admin/src/pages/CourseDashboard.jsx +++ b/admin/src/pages/CourseDashboard.jsx @@ -1,6 +1,6 @@ import { useParams} from "react-router-dom"; import React, { useEffect, useState } from "react"; -import { fetchCourseDashboardData, deleteNode, handleContribution } from "@/apis/courses"; +import { fetchCourseDashboardData, deleteNode, handleContribution, getFileDownloadUrl } from "@/apis/courses"; import { FiFolder, FiFile, @@ -70,6 +70,7 @@ function LoadStructure({ node, onDelete, depth = 0 }) { ); } + export default function CourseDashboard() { const { code } = useParams(); @@ -154,6 +155,23 @@ export default function CourseDashboard() { const pendingContributions = data?.contributions?.filter(c => !c.approved) || []; + const handleDownload = async(fileId) => + { + try + { + const data = await getFileDownloadUrl(fileId); + if(data?.url) + { + window.location.href = data.url; + } + } + catch(error) + { + console.log(error); + alert("Failed to get download "); + } + }; + return (
@@ -232,10 +250,37 @@ export default function CourseDashboard() {
-
- {contribution.files?.map(f => f.name).join(", ") || contribution.contributionId} +
+ {contribution.files && contribution.files.length > 0 ? ( +
+ {contribution.files.map((file) => ( +
+ {file.name} + {file.webUrl && ( + + View + + )} + {file.downloadUrl && ( + + )} +
+ ))} +
+ ) : ( + contribution.contributionId + )}
-
Awaiting review
} /> + + + + } + />
diff --git a/admin/src/apis/courses.js b/admin/src/apis/courses.js index 886c3f92..cce741f6 100644 --- a/admin/src/apis/courses.js +++ b/admin/src/apis/courses.js @@ -263,4 +263,24 @@ export const getFileDownloadUrl = async (fileId) => { console.error("Error getting download link:", error); throw error; } -}; \ No newline at end of file +}; + + +export const fetchAllPendingContributions = async()=> +{ + try + { + const response = await fetch(`${API_BASE_URL}api/admin/contribution/pending`, { + headers: {Authorization: "Bearer admin-coursehub-cc23-golang"}, + credentials: "include", + }); + + if (!response.ok) { + throw new Error("Failed to get pending contributions"); + } + return await response.json(); + } catch (error) { + console.error("Error fetching pending contributions:", error); + throw error; + } +} diff --git a/admin/src/components/Sidebar.jsx b/admin/src/components/Sidebar.jsx index 40b06645..e0099f97 100644 --- a/admin/src/components/Sidebar.jsx +++ b/admin/src/components/Sidebar.jsx @@ -1,13 +1,15 @@ import React from "react"; import { Link, useLocation } from "react-router-dom"; -import { FaBook, FaUsers, FaLayerGroup, FaLink, FaUserGraduate, FaExclamationTriangle } from "react-icons/fa"; +import { FaBook, FaUsers, FaLayerGroup, FaLink, FaUserGraduate, FaExclamationTriangle, FaClock } from "react-icons/fa"; import { adminLogout } from "@/apis/auth"; +import { FcAlarmClock } from "react-icons/fc"; const navItems = [ { label: "Students", to: "/admin/students", icon: FaUserGraduate }, { label: "Courses", to: "/admin/courses", icon: FaBook }, { label: "Course Linking", to: "/admin/course-linking", icon: FaLink }, { label: "Courses Without BR", to: "/admin/courses-without-br", icon: FaExclamationTriangle }, + { label: "Pending Approvals", to: "/admin/pending-approvals", icon: FaClock }, ]; const Sidebar = () => { diff --git a/admin/src/pages/CourseDashboard.jsx b/admin/src/pages/CourseDashboard.jsx index 28745bea..be9b3ae7 100644 --- a/admin/src/pages/CourseDashboard.jsx +++ b/admin/src/pages/CourseDashboard.jsx @@ -284,14 +284,16 @@ export default function CourseDashboard() {
diff --git a/server/modules/admin/admin.routes.js b/server/modules/admin/admin.routes.js index f7b96d87..23a7199c 100644 --- a/server/modules/admin/admin.routes.js +++ b/server/modules/admin/admin.routes.js @@ -7,7 +7,7 @@ import multer from "multer"; import { adminLogin, adminLogout } from "./auth.controller.js"; import { uploadCourses, renameCourse, deleteCourse, linkLegacyCourse, bulkLinkCourses, syncCoursesCacheController } from "./adminDashboard.controller.js"; import {getCourseDashboardData, handleContribution, deleteNode} from "./adminDashboard.controller.js" - +import { getAllPendingContributions } from "./adminDashboard.controller.js"; const router = express.Router(); const upload = multer({ dest: "uploads/" }); @@ -44,6 +44,7 @@ router.post( isAdmin, catchAsync(adminController.createNewCourseFolders) ); +router.get("/contribution/pending", isAdmin, catchAsync(getAllPendingContributions)); router.post("/courses/upload", isAdmin, upload.single("file"), uploadCourses); router.post("/courses/bulk-link", isAdmin, upload.single("file"), bulkLinkCourses); diff --git a/server/modules/admin/adminDashboard.controller.js b/server/modules/admin/adminDashboard.controller.js index 981b7040..1415cc94 100644 --- a/server/modules/admin/adminDashboard.controller.js +++ b/server/modules/admin/adminDashboard.controller.js @@ -870,3 +870,17 @@ export async function syncCoursesCacheController(req, res, next) { next(new AppError(500, `Cache sync failed: ${err.message}`)); } } + + +export async function getAllPendingContributions(req, res, next) +{ + try + { + const contributions = await Contribution.find({ approved: false }).sort({ createdAt: -1 }).populate("files"); + res.json({pending:contributions}); + } + catch(error) + { + next(new AppError(500, `Failed to fetch pending contributions: ${error.message}`)); + } +} \ No newline at end of file From 09b782c6be4fce0b047d792e192f0377bbbbc0c4 Mon Sep 17 00:00:00 2001 From: Mayank Arya Date: Sun, 27 Sep 2026 18:41:11 +0530 Subject: [PATCH 5/5] UI for the pending contributions --- admin/src/pages/PendingContributions.jsx | 161 +++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 admin/src/pages/PendingContributions.jsx diff --git a/admin/src/pages/PendingContributions.jsx b/admin/src/pages/PendingContributions.jsx new file mode 100644 index 00000000..beb559d5 --- /dev/null +++ b/admin/src/pages/PendingContributions.jsx @@ -0,0 +1,161 @@ +import React, { useState, useEffect } from "react"; +import { + fetchAllPendingContributions, + handleContribution, + getFileDownloadUrl, +} from "@/apis/courses"; +import { toast } from "react-toastify"; +import { FiFile, FiCheck, FiX, FiEye, FiDownload, FiInbox } from "react-icons/fi"; + +export default function PendingApprovals() { + const [pending, setPending] = useState([]); + const [loading, setLoading] = useState(true); + const [processingId, setProcessingId] = useState(null); + + const loadPending = async () => { + try { + setLoading(true); + const response = await fetchAllPendingContributions(); + setPending(response.pending || []); + } catch (err) { + toast.error(err.message || "Failed to load pending approvals."); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadPending(); + }, []); + + const onAction = async (contributionId, action) => { + if (processingId) return; + try { + setProcessingId(contributionId); + await handleContribution(contributionId, action); + toast.success(action === "approve" ? "Contribution approved." : "Contribution rejected."); + // Drop the handled row without a full refetch. + setPending((prev) => prev.filter((c) => c.contributionId !== contributionId)); + } catch (err) { + toast.error(err.message || `Failed to ${action} contribution.`); + } finally { + setProcessingId(null); + } + }; + + const onDownload = async (fileId) => { + try { + const data = await getFileDownloadUrl(fileId); + if (data?.url) { + window.location.href = data.url; + } + } catch (err) { + toast.error("Failed to get download link."); + } + }; + + return ( +
+
+
+

Pending Approvals

+

+ All unverified files across every course, awaiting review. +

+
+ {!loading && ( + + {pending.length} pending + + )} +
+ +
+ {loading ? ( +

Loading pending approvals...

+ ) : pending.length === 0 ? ( +
+ +
You're all caught up
+
+ No files are waiting for approval. +
+
+ ) : ( +
+ {pending.map((contribution) => ( +
+ + + + +
+
+ {contribution.courseCode} +
+ {contribution.files && contribution.files.length > 0 ? ( +
+ {contribution.files.map((file) => ( +
+ + {file.name} + + {file.webUrl && ( + + View + + )} + {file.fileId && ( + + )} +
+ ))} +
+ ) : ( +
+ {contribution.contributionId} (no files) +
+ )} +
Awaiting review
+
+ +
+ + +
+
+ ))} +
+ )} +
+
+ ); +} \ No newline at end of file