From 45198c751d981acf0400aa8d2b8f6038dae2d302 Mon Sep 17 00:00:00 2001 From: Mayank Date: Sun, 21 Jun 2026 00:15:05 +0530 Subject: [PATCH 1/4] New Dashboard logic --- admin/src/App.jsx | 2 + admin/src/apis/courses.js | 66 +++++ admin/src/pages/CourseDashboard.jsx | 262 ++++++++++++++++++ admin/src/pages/Courses.jsx | 11 + server/modules/admin/admin.routes.js | 6 +- .../admin/adminDashboard.controller.js | 118 ++++++++ 6 files changed, 464 insertions(+), 1 deletion(-) create mode 100644 admin/src/pages/CourseDashboard.jsx diff --git a/admin/src/App.jsx b/admin/src/App.jsx index 36f99587..c92140c4 100644 --- a/admin/src/App.jsx +++ b/admin/src/App.jsx @@ -5,6 +5,7 @@ import Courses from "./pages/Courses"; import CourseLinking from "./pages/CourseLinking"; import PrivateRoute from "./router_utils/PrivateRoutes"; import Login from "./pages/Login"; +import CourseDashboard from "./pages/CourseDashboard"; function App() { return ( @@ -14,6 +15,7 @@ function App() {
+ }/> } /> { throw error; } }; + +export const fetchCourseDashboardData = async(code)=> +{ + try + { + const safeCode = code.toLowerCase().trim(); + const response = await fetch(`${API_BASE_URL}api/admin/course/${safeCode}/dashboard`, { + headers: {Authorization: "Bearer admin-coursehub-cc23-golang"}, + credentials: "include", + }); + + if (!response.ok) { + throw new Error("Failed to get dashboard data"); + } + return await response.json(); + } catch (error) { + console.error("Error fetching dashboard:", error); + throw error; + } +} + +export const handleContribution = async (contributionId, action) => { + try + { + const response = await fetch(`${API_BASE_URL}api/admin/contribution/action`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer admin-coursehub-cc23-golang", + }, + body: JSON.stringify({ contributionId, action }), + credentials: "include", + }); + if (!response.ok) + { + throw new Error(`Failed to ${action} contribution`); + } + return await response.json(); + } catch (error) { + throw error; + } +}; + +export const deleteNode = async(type,id) => +{ + try + { + const response = await fetch(`${API_BASE_URL}api/admin/node/${type}/${id}`, { + method : "DELETE", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer admin-coursehub-cc23-golang", + }, + }); + if(!response.ok) + { + throw new Error("Failed to delete Item"); + } + } + catch(error) + { + throw error; + } +} + diff --git a/admin/src/pages/CourseDashboard.jsx b/admin/src/pages/CourseDashboard.jsx new file mode 100644 index 00000000..ddda0e66 --- /dev/null +++ b/admin/src/pages/CourseDashboard.jsx @@ -0,0 +1,262 @@ +import { useParams, useNavigate } from "react-router-dom"; +import React, { useEffect, useState } from "react"; +import { fetchCourseDashboardData, deleteNode, handleContribution } from "@/apis/courses"; +import { + FiFolder, + FiFile, + FiTrash2, + FiChevronRight, + FiChevronDown, + FiUsers, + FiCheckCircle, + FiInbox, + FiCheck, + FiX, +} from "react-icons/fi"; + +function LoadStructure({ node, onDelete, depth = 0 }) { + const isFolder = node.childType === "Folder" || node.children; + const nodeType = isFolder ? "folder" : "file"; + + const [open, setOpen] = useState(true); + const hasChildren = isFolder && node.children && node.children.length > 0; + + return ( +
+
+ {isFolder ? ( + + ) : ( + + )} + + + {isFolder ? : } + + + {node.name} + + +
+ + {isFolder && open && + node.children?.map((child) => ( + + ))} +
+ ); +} + +export default function CourseDashboard() { + const { code } = useParams(); + const navigate = useNavigate(); + + const [loading, setLoading] = useState(true); + const [data, setData] = useState(null); + const [activeTab, setActiveTab] = useState("structure"); + + useEffect(() => { + loadData(); + }, [code]); + + const loadData = async () => { + try + { + const getDashboard = await fetchCourseDashboardData(code); + setData(getDashboard); + } + catch (error) + { + console.log(error); + } + finally + { + setLoading(false); + } + }; + + const handleContributionAction = async(contributionId, action) => + { + const isApprove = action == "approve"; + if(!window.confirm(`Are you sure you want to ${action} this contribution`)) + { + return; + } + + try + { + await handleContribution(contributionId,action); + alert(`Contribution ${isApprove ? 'Approved' : 'Rejected'} succesfully!`); + loadData(); + } + catch(error) + { + console.log(error); + alert("error"); + } + } + + const handleDelete = async (type, id, name) => + { + if (!window.confirm(`Are you SURE you want to permanently delete the ${type} "${name}"? This cannot be undone.`)) + { + return; + } + try + { + await deleteNode(type, id); + loadData(); + } + catch (error) + { + console.error(error); + alert(`Failed to delete the ${type}. Check the console.`); + } + }; + + if (loading) + { + return ( +
+ + Loading dashboard… +
+ ); + } + + const studentCount = data?.studentCount || 0; + const approvedContributions = data?.contributions?.filter(c => c.approved) || []; + const verifiedFilesCount = approvedContributions.reduce((total,contribution) => + { + return total + (contribution.files?.length || 0); + },0); + + const pendingContributions = data?.contributions?.filter(c => !c.approved) || []; + + return ( +
+
+

+ {data?.course?.code} + · {data?.course?.name} +

+
+ +
+
+
+ + + +
+
{studentCount}
+
Registered students
+
+
+
+ + + +
+
{verifiedFilesCount}
+
Verified contributions
+
+
+
+ +
+
+ Course structure +
+
+ {data?.course?.children?.length ? ( + data.course.children.map((child) => ( + + )) + ) : ( +
+ + + +
No folders or files yet
+
Course content will appear here once added.
+
+ )} +
+
+ +
+
+ Pending contributions + + {pendingContributions.length} + +
+
+ {pendingContributions.length === 0 ? ( +
+ + + +
You're all caught up
+
New submissions will show up here for review.
+
+ ) : ( + pendingContributions.map((contribution) => ( +
+ + + +
+
{contribution.contributionId}
+
Awaiting review
+
+
+ + +
+
+ )) + )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/admin/src/pages/Courses.jsx b/admin/src/pages/Courses.jsx index 472c5445..42d76364 100644 --- a/admin/src/pages/Courses.jsx +++ b/admin/src/pages/Courses.jsx @@ -27,8 +27,11 @@ import { Badge } from "@/components/ui/badge"; import { Modal, ModalHeader, ModalBody, ModalFooter } from "@/components/ui/modal"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { fetchCourses, updateCourseName, bulkSyncCourses, deleteCourse } from "@/apis/courses"; +import { useNavigate } from "react-router-dom"; +import { FaEye } from "react-icons/fa"; // Add FaEye to your react-icons import function Courses() { + const navigate = useNavigate(); const [courses, setCourses] = useState([]); const [search, setSearch] = useState(""); const [loading, setLoading] = useState(true); @@ -604,6 +607,14 @@ function Courses() {
{editingCode === course.code ? null : ( <> +