{
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 0000000..f9eff4b
--- /dev/null
+++ b/admin/src/pages/CourseDashboard.jsx
@@ -0,0 +1,262 @@
+import { useParams} 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 [loading, setLoading] = useState(true);
+ const [data, setData] = useState(null);
+
+ 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.files?.map(f => f.name).join(", ") || 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 472c544..42d7636 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 : (
<>
+