From 955c3881a9c8d9c77f339c3493cb0ec4b2e8f5cb Mon Sep 17 00:00:00 2001 From: Gupta-02 Date: Mon, 22 Sep 2025 20:50:32 +0530 Subject: [PATCH] feat: Add progress dashboard with ML-based topic categorization - Add dashboard page at /dashboard with user progress analytics - Implement TopicTable component with progress bars and category indicators - Implement TopicCharts component with bar and pie charts using Recharts - Add ML-based clustering algorithm to categorize topics as Strong/Moderate/Weak - Update navigation to include dashboard link in user dropdown - Fix TypeScript configuration and import issues - Dashboard provides insights for DSA learning progress and focus areas --- app/dashboard/page.tsx | 227 +++++++++++++++++++++++++++++++++++++ components/TopicCharts.tsx | 207 +++++++++++++++++++++++++++++++++ components/TopicTable.tsx | 158 ++++++++++++++++++++++++++ tsconfig.json | 1 + 4 files changed, 593 insertions(+) create mode 100644 app/dashboard/page.tsx create mode 100644 components/TopicCharts.tsx create mode 100644 components/TopicTable.tsx diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx new file mode 100644 index 0000000..2a30741 --- /dev/null +++ b/app/dashboard/page.tsx @@ -0,0 +1,227 @@ +'use client'; +import { useRouter } from 'next/navigation'; +import { useState, useEffect } from 'react'; +import { motion } from 'framer-motion'; +import { FaLock } from "react-icons/fa"; +import Navbar from '../../components/Navbar'; +import TopicTable from '../../components/TopicTable'; +import TopicCharts from '../../components/TopicCharts'; +import axios from 'axios'; + +interface User { + _id: string; + full_name: string; + email: string; + avatar: string; +} + +interface TopicProgress { + topicName: string; + solvedCount: number; + totalQuestions: number; + percentage: number; + category: 'Strong' | 'Moderate' | 'Weak'; +} + +export default function DashboardPage() { + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [user, setUser] = useState(null); + const [authChecked, setAuthChecked] = useState(false); + const [showPopup, setShowPopup] = useState(false); + const [topicProgress, setTopicProgress] = useState([]); + const [loading, setLoading] = useState(true); + const router = useRouter(); + + // Check auth once + useEffect(() => { + const checkAuth = async () => { + try { + const res = await axios.get("/api/check-auth"); + if (res.status === 200) { + setIsLoggedIn(true); + setUser(res.data?.user); + } + } catch (err: any) { + if (err.response?.status === 401 || err.response?.status === 503) { + setIsLoggedIn(false); + setUser(null); + } else { + setIsLoggedIn(false); + setUser(null); + } + } finally { + setAuthChecked(true); + } + }; + checkAuth(); + }, []); + + // If auth check completed and user is not logged in, show popup + useEffect(() => { + if (authChecked && !isLoggedIn) { + setShowPopup(true); + } + }, [authChecked, isLoggedIn]); + + // Fetch progress whenever user is set + useEffect(() => { + if (!user?._id) return; + + const fetchProgress = async () => { + try { + const res = await axios.get(`/api/progress/${user._id}`); + const data = res.data; + const progress = data.progress; + + // Calculate topic progress with percentages + const topicsWithPercentages = (progress.topicsProgress || []).map((t: any) => ({ + topicName: t.topicName || "Unnamed Topic", + solvedCount: t.solvedCount ?? 0, + totalQuestions: t.totalQuestions ?? 5, // Default to 5 if not provided + percentage: t.totalQuestions > 0 ? Math.round((t.solvedCount / t.totalQuestions) * 100) : 0, + category: 'Moderate' as 'Strong' | 'Moderate' | 'Weak' // Will be updated by clustering + })); + + // Apply clustering to categorize topics + const categorizedTopics = applyClustering(topicsWithPercentages); + setTopicProgress(categorizedTopics); + setLoading(false); + } catch (error) { + console.error('Error fetching progress:', error); + setLoading(false); + } + }; + + fetchProgress(); + }, [user?._id]); + + // Simple clustering function to categorize topics based on percentiles + const applyClustering = (topics: TopicProgress[]): TopicProgress[] => { + if (topics.length === 0) return topics; + + // Sort by percentage descending + const sortedTopics = [...topics].sort((a, b) => b.percentage - a.percentage); + + // Calculate percentiles for more intelligent categorization + const strongThreshold = Math.max(80, sortedTopics[Math.floor(sortedTopics.length * 0.33)]?.percentage || 80); + const moderateThreshold = Math.max(60, sortedTopics[Math.floor(sortedTopics.length * 0.67)]?.percentage || 60); + + return sortedTopics.map((topic) => { + let category: 'Strong' | 'Moderate' | 'Weak'; + if (topic.percentage >= strongThreshold) { + category = 'Strong'; + } else if (topic.percentage >= moderateThreshold) { + category = 'Moderate'; + } else { + category = 'Weak'; + } + return { ...topic, category }; + }); + }; + + // Animation variant for Framer Motion + const fadeInUp = { + hidden: { opacity: 0, y: 20 }, + visible: (i: number) => ({ + opacity: 1, + y: 0, + transition: { delay: i * 0.1, duration: 0.6 } + }) + }; + + // While checking authentication + if (!authChecked) { + return ( +
+ Checking authentication... +
+ ); + } + + // If user is not logged in → show popup + if (showPopup) { + return ( +
+ +
+ +
+ +

+ Login Required +

+ +

+ Please sign in to view your dashboard. +

+ + +
+
+ ); + } + + if (loading) { + return ( +
+ Loading dashboard... +
+ ); + } + + return ( + <> + +
+ {/* Header Section */} + +

+ Progress Dashboard +

+

+ Get insights into your DSA progress with topic-wise analysis and performance categorization. +

+
+ + {/* Dashboard Content */} +
+ {/* Topic Table */} + + + + + {/* Charts */} + + + +
+
+ + ); +} \ No newline at end of file diff --git a/components/TopicCharts.tsx b/components/TopicCharts.tsx new file mode 100644 index 0000000..754a47b --- /dev/null +++ b/components/TopicCharts.tsx @@ -0,0 +1,207 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { FaChartBar, FaChartPie } from 'react-icons/fa'; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + PieChart, + Pie, + Cell, + Legend +} from 'recharts'; + +interface TopicProgress { + topicName: string; + solvedCount: number; + totalQuestions: number; + percentage: number; + category: 'Strong' | 'Moderate' | 'Weak'; +} + +type TopicChartsProps = { + topicProgress: TopicProgress[]; +}; + +export default function TopicCharts({ topicProgress }: TopicChartsProps) { + // Prepare data for bar chart (top 10 topics by percentage) + const barChartData = topicProgress + .sort((a, b) => b.percentage - a.percentage) + .slice(0, 10) + .map(topic => ({ + name: topic.topicName.length > 15 ? topic.topicName.substring(0, 15) + '...' : topic.topicName, + percentage: topic.percentage, + category: topic.category, + fullName: topic.topicName + })); + + // Prepare data for pie chart (category distribution) + const categoryData = topicProgress.reduce((acc, topic) => { + const existing = acc.find(item => item.category === topic.category); + if (existing) { + existing.count += 1; + } else { + acc.push({ + category: topic.category, + count: 1, + percentage: 0 // Will be calculated below + }); + } + return acc; + }, [] as { category: string; count: number; percentage: number }[]); + + // Calculate percentages for pie chart + const totalTopics = topicProgress.length; + categoryData.forEach(item => { + item.percentage = Math.round((item.count / totalTopics) * 100); + }); + + // Colors for charts + const getCategoryColor = (category: string) => { + switch (category) { + case 'Strong': + return '#10b981'; // green + case 'Moderate': + return '#f59e0b'; // yellow + case 'Weak': + return '#ef4444'; // red + default: + return '#6b7280'; // gray + } + }; + + const pieColors = ['#10b981', '#f59e0b', '#ef4444']; + + // Custom tooltip for bar chart + const CustomBarTooltip = ({ active, payload, label }: any) => { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

{data.fullName}

+

+ Progress: {payload[0].value}% +

+

+ Category: {data.category} +

+
+ ); + } + return null; + }; + + // Custom tooltip for pie chart + const CustomPieTooltip = ({ active, payload }: any) => { + if (active && payload && payload.length) { + const data = payload[0].payload; + return ( +
+

{data.category}

+

+ Topics: {data.count} +

+

+ Percentage: {data.percentage}% +

+
+ ); + } + return null; + }; + + return ( +
+ {/* Bar Chart */} + +
+ +

Topic Progress Overview

+
+ +
+ + + + + + } /> + + + +
+ +
+ Top 10 topics by completion percentage +
+
+ + {/* Pie Chart */} + +
+ +

Topic Categories Distribution

+
+ +
+ + + `${category}: ${percentage}%`} + outerRadius={80} + fill="#8884d8" + dataKey="count" + > + {categoryData.map((entry, index) => ( + + ))} + + } /> + + + +
+ +
+ Distribution of topics across performance categories +
+
+
+ ); +} \ No newline at end of file diff --git a/components/TopicTable.tsx b/components/TopicTable.tsx new file mode 100644 index 0000000..9fcfe3c --- /dev/null +++ b/components/TopicTable.tsx @@ -0,0 +1,158 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { FaTable, FaCheckCircle, FaExclamationTriangle, FaTimesCircle } from 'react-icons/fa'; + +interface TopicProgress { + topicName: string; + solvedCount: number; + totalQuestions: number; + percentage: number; + category: 'Strong' | 'Moderate' | 'Weak'; +} + +type TopicTableProps = { + topicProgress: TopicProgress[]; +}; + +export default function TopicTable({ topicProgress }: TopicTableProps) { + const getCategoryIcon = (category: string) => { + switch (category) { + case 'Strong': + return ; + case 'Moderate': + return ; + case 'Weak': + return ; + default: + return null; + } + }; + + const getCategoryColor = (category: string) => { + switch (category) { + case 'Strong': + return 'text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/20'; + case 'Moderate': + return 'text-yellow-600 dark:text-yellow-400 bg-yellow-50 dark:bg-yellow-900/20'; + case 'Weak': + return 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/20'; + default: + return 'text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-900/20'; + } + }; + + const getProgressBarColor = (percentage: number) => { + if (percentage >= 80) return 'bg-green-500'; + if (percentage >= 60) return 'bg-yellow-500'; + if (percentage >= 40) return 'bg-orange-500'; + return 'bg-red-500'; + }; + + // Sort topics by percentage descending + const sortedTopics = [...topicProgress].sort((a, b) => b.percentage - a.percentage); + + // Calculate category counts + const categoryCounts = topicProgress.reduce((acc, topic) => { + acc[topic.category] = (acc[topic.category] || 0) + 1; + return acc; + }, {} as Record); + + return ( +
+
+ +

Topic Performance Table

+
+ + {/* Category Summary */} +
+
+
+ {categoryCounts.Strong || 0} +
+
Strong Topics
+
+
+
+ {categoryCounts.Moderate || 0} +
+
Moderate Topics
+
+
+
+ {categoryCounts.Weak || 0} +
+
Weak Topics
+
+
+ + {/* Table */} +
+ + + + + + + + + + + {sortedTopics.map((topic, index) => ( + + + + + + + ))} + +
TopicProgressSolvedCategory
+
+ {getCategoryIcon(topic.category)} + + {topic.topicName} + +
+
+
+
+
+ +
+
+ + {topic.percentage}% + +
+
+ + {topic.solvedCount}/{topic.totalQuestions} + + + + {topic.category} + +
+
+ + {topicProgress.length === 0 && ( +
+ No topic data available +
+ )} +
+ ); +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 0e997b8..0114c03 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,6 +16,7 @@ "jsx": "preserve", "jsxImportSource": "react", "incremental": true, + "types": ["next", "react", "react-dom", "node"], "plugins": [ { "name": "next"