- {navigationItems.map((item, index) => (
-
-
+
+ {/* Desktop nav items */}
+
+ {navigationItems.map((item, index) => (
+
+
item.hasDropdown && toggleDropdown(item.name)}
+ >
+ {getIcon(item.icon)}
+ {item.name}
+ {item.hasDropdown && (
+
+
+
+ )}
+
- {item.hasDropdown && activeDropdown === item.name && (
-
-
-
Option 1
-
Option 2
-
Option 3
+ {item.hasDropdown && activeDropdown === item.name && (
+
+
+ Option 1
+ Option 2
+ Option 3
+
-
- )}
-
- ))}
+ )}
+
+ ))}
+
);
diff --git a/src/components/EductionPortal/StudentDashboard/NavigationBar.module.css b/src/components/EductionPortal/StudentDashboard/NavigationBar.module.css
index 8f611d20dd..9a33f7590d 100644
--- a/src/components/EductionPortal/StudentDashboard/NavigationBar.module.css
+++ b/src/components/EductionPortal/StudentDashboard/NavigationBar.module.css
@@ -3,6 +3,7 @@
background-color: #3b82f6;
padding: 0.75rem 0;
box-shadow: 0 1px 3px rgb(0 0 0 / 10%);
+ position: relative;
}
.navContainer {
@@ -11,8 +12,16 @@
padding: 0 1rem;
display: flex;
align-items: center;
- gap: 2rem;
- overflow-x: auto;
+ gap: 0.25rem;
+}
+
+/* Desktop nav items wrapper */
+.navItems {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ flex-wrap: nowrap;
+ flex: 1;
}
.navItem {
@@ -89,19 +98,74 @@
color: #1f2937;
}
+/* Hamburger button — hidden on desktop */
+.hamburger {
+ display: none;
+ align-items: center;
+ justify-content: center;
+ width: 40px;
+ height: 40px;
+ background: transparent;
+ border: none;
+ border-radius: 6px;
+ color: #fff;
+ cursor: pointer;
+ flex-shrink: 0;
+ transition: background-color 0.2s ease;
+}
+
+.hamburger:hover {
+ background-color: rgb(255 255 255 / 10%);
+}
+
/* Responsive Design */
@media (width <= 768px) {
+ .hamburger {
+ display: flex;
+ }
+
+
.navContainer {
- gap: 1rem;
- padding: 0 0.5rem;
+ flex-wrap: wrap;
+ gap: 0;
+ padding: 0 0.75rem;
+ }
+
+ /* Hide desktop nav items on mobile; show only when open */
+ .navItems {
+ display: none;
+ flex-direction: column;
+ align-items: flex-start;
+ width: 100%;
+ padding: 0.5rem 0;
+ gap: 0;
+ }
+
+ .navItems.navItemsOpen {
+ display: flex;
+ }
+
+ .navItem {
+ width: 100%;
}
.navButton {
- padding: 0.5rem;
+ width: 100%;
+ justify-content: flex-start;
+ padding: 0.6rem 0.5rem;
+ border-radius: 4px;
}
- .navText {
- display: none;
+ .dropdown {
+ position: static;
+ margin-top: 0;
+ }
+
+ .dropdownContent {
+ border-radius: 4px;
+ margin-left: 1.5rem;
+ min-width: auto;
+ width: calc(100% - 1.5rem);
}
}
diff --git a/src/components/EductionPortal/StudentDashboard/StudentDashboard.jsx b/src/components/EductionPortal/StudentDashboard/StudentDashboard.jsx
index 554251aae3..39636401e4 100644
--- a/src/components/EductionPortal/StudentDashboard/StudentDashboard.jsx
+++ b/src/components/EductionPortal/StudentDashboard/StudentDashboard.jsx
@@ -1,7 +1,6 @@
-import React, { useState, useEffect } from 'react';
-import { Container, Row, Col, Card, Button } from 'reactstrap';
+import React, { useState, useEffect, useMemo, useCallback } from 'react';
+import { Container, Button } from 'reactstrap';
import { useSelector, useDispatch } from 'react-redux';
-import { toast } from 'react-toastify';
import styles from './StudentDashboard.module.css';
import TaskCardView from './TaskCardView';
import TaskListView from './TaskListView';
@@ -11,8 +10,19 @@ import { fetchStudentTasks, markStudentTaskAsDone } from '~/actions/studentTasks
import { fetchIntermediateTasks, markIntermediateTaskAsDone } from '~/actions/intermediateTasks';
import HoursLogPanel from '../StudentTasks/HoursLogPanel';
+const ACTIVE_STATUSES = new Set(['assigned', 'in_progress']);
+const PENDING_STATUSES = new Set(['pending_review', 'submitted']);
+const COMPLETED_STATUSES = new Set(['completed', 'graded']);
+
+const FILTER_TABS = [
+ { key: 'active', label: 'Active' },
+ { key: 'pending', label: 'Pending Review' },
+ { key: 'completed', label: 'Completed' },
+];
+
const StudentDashboard = () => {
const [viewMode, setViewMode] = useState('card'); // 'card' or 'list'
+ const [activeFilter, setActiveFilter] = useState('active');
const [summaryData, setSummaryData] = useState({
totalTimeLogged: '0h 0min',
thisWeek: '0h 0min',
@@ -24,41 +34,67 @@ const StudentDashboard = () => {
const [activeLogTask, setActiveLogTask] = useState(null);
const dispatch = useDispatch();
- const authUser = useSelector(state => state.auth.user);
const { taskItems: tasks, fetching: loading, error } = useSelector(state => state.studentTasks);
const darkMode = useSelector(state => state.theme.darkMode);
+ // Derived filtered task list — no unnecessary rendering of graded/completed tasks by default
+ const filteredTasks = useMemo(() => {
+ if (!tasks) return [];
+ switch (activeFilter) {
+ case 'active':
+ return tasks.filter(t => ACTIVE_STATUSES.has(t.status));
+ case 'pending':
+ return tasks.filter(t => PENDING_STATUSES.has(t.status));
+ case 'completed':
+ return tasks.filter(t => COMPLETED_STATUSES.has(t.status));
+ default:
+ return tasks;
+ }
+ }, [tasks, activeFilter]);
+
+ // Tab counts for display
+ const tabCounts = useMemo(() => {
+ if (!tasks) return { active: 0, pending: 0, completed: 0 };
+ return {
+ active: tasks.filter(t => ACTIVE_STATUSES.has(t.status)).length,
+ pending: tasks.filter(t => PENDING_STATUSES.has(t.status)).length,
+ completed: tasks.filter(t => COMPLETED_STATUSES.has(t.status)).length,
+ };
+ }, [tasks]);
+
// Fetch tasks from API
useEffect(() => {
dispatch(fetchStudentTasks());
}, [dispatch]);
- // Fetch intermediate tasks for all parent tasks
+ // Fetch intermediate tasks only for currently visible (filtered) tasks
useEffect(() => {
- const fetchAllIntermediateTasks = async () => {
- if (tasks && tasks.length > 0) {
- const intermediateTasksData = {};
-
- // Fetch intermediate tasks for each parent task
- for (const task of tasks) {
- try {
- const subTasks = await dispatch(fetchIntermediateTasks(task.id));
- if (subTasks && subTasks.length > 0) {
- intermediateTasksData[task.id] = subTasks;
- }
- } catch {
- // Non-critical: skip if intermediate tasks unavailable for this task
- }
- }
+ const fetchVisibleIntermediateTasks = async () => {
+ if (!filteredTasks || filteredTasks.length === 0) return;
- setIntermediateTasks(intermediateTasksData);
+ const intermediateTasksData = { ...intermediateTasks };
+ let changed = false;
+
+ for (const task of filteredTasks) {
+ if (intermediateTasksData[task.id] !== undefined) continue; // already fetched
+ try {
+ const subTasks = await dispatch(fetchIntermediateTasks(task.id));
+ intermediateTasksData[task.id] = subTasks || [];
+ changed = true;
+ } catch {
+ intermediateTasksData[task.id] = [];
+ changed = true;
+ }
}
+
+ if (changed) setIntermediateTasks(intermediateTasksData);
};
- fetchAllIntermediateTasks();
- }, [tasks, dispatch]);
+ fetchVisibleIntermediateTasks();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [filteredTasks, dispatch]);
- // Calculate summary data when tasks change
+ // Calculate summary data when tasks change (uses ALL tasks for accurate totals)
useEffect(() => {
if (tasks && tasks.length > 0) {
calculateSummaryData(tasks);
@@ -67,17 +103,18 @@ const StudentDashboard = () => {
// Calculate summary data from tasks
const calculateSummaryData = tasksData => {
+ const formatTime = hrs => {
+ const wholeHours = Math.floor(hrs);
+ const minutes = Math.round((hrs - wholeHours) * 60);
+ return `${wholeHours}h ${minutes}min`;
+ };
+
const totalHours = tasksData.reduce((sum, task) => sum + (task.logged_hours || 0), 0);
const thisWeekHours = tasksData.reduce((sum, task) => {
- // Check if task was logged this week (simplified logic)
const taskDate = new Date(task.last_logged_date || task.created_at);
const weekAgo = new Date();
weekAgo.setDate(weekAgo.getDate() - 7);
-
- if (taskDate >= weekAgo) {
- return sum + (task.logged_hours || 0);
- }
- return sum;
+ return taskDate >= weekAgo ? sum + (task.logged_hours || 0) : sum;
}, 0);
const activeCourses = new Set(tasksData.map(task => task.course_id || task.course_name)).size;
@@ -91,55 +128,42 @@ const StudentDashboard = () => {
});
};
- // Format time in hours and minutes
- const formatTime = hours => {
- const wholeHours = Math.floor(hours);
- const minutes = Math.round((hours - wholeHours) * 60);
- return `${wholeHours}h ${minutes}min`;
- };
-
// Handle log time
const handleLogTime = task => {
setActiveLogTask(task);
};
// Handle mark as done
- const handleMarkAsDone = async taskId => {
- dispatch(markStudentTaskAsDone(taskId));
- };
+ const handleMarkAsDone = useCallback(
+ taskId => {
+ dispatch(markStudentTaskAsDone(taskId));
+ },
+ [dispatch],
+ );
// Handle mark intermediate task as done
- const handleMarkIntermediateAsDone = async (intermediateTaskId, parentTaskId) => {
- try {
- await dispatch(markIntermediateTaskAsDone(intermediateTaskId));
- // Refresh intermediate tasks for this parent
- const tasks = await dispatch(fetchIntermediateTasks(parentTaskId));
- setIntermediateTasks(prev => ({ ...prev, [parentTaskId]: tasks || [] }));
- } catch (error) {
- // Error is handled in the action
- }
- };
+ const handleMarkIntermediateAsDone = useCallback(
+ async (intermediateTaskId, parentTaskId) => {
+ try {
+ await dispatch(markIntermediateTaskAsDone(intermediateTaskId));
+ const subTasks = await dispatch(fetchIntermediateTasks(parentTaskId));
+ setIntermediateTasks(prev => ({ ...prev, [parentTaskId]: subTasks || [] }));
+ } catch {
+ // Error is handled in the action
+ }
+ },
+ [dispatch],
+ );
// Toggle expand/collapse intermediate tasks
- const toggleIntermediateTasks = async taskId => {
- const isExpanded = expandedTasks[taskId];
-
- // Just toggle the expanded state (tasks are already loaded)
- setExpandedTasks(prev => ({
- ...prev,
- [taskId]: !isExpanded,
- }));
- };
-
- // Toggle view mode
- const toggleViewMode = () => {
- setViewMode(prev => (prev === 'card' ? 'list' : 'card'));
- };
+ const toggleIntermediateTasks = useCallback(taskId => {
+ setExpandedTasks(prev => ({ ...prev, [taskId]: !prev[taskId] }));
+ }, []);
if (loading) {
return (
-
+
Loading your dashboard...
);
@@ -156,6 +180,36 @@ const StudentDashboard = () => {
);
}
+ const emptyMessages = {
+ active: 'No active tasks right now.',
+ pending: 'No tasks pending review.',
+ completed: 'No completed tasks yet.',
+ };
+
+ const sharedTaskProps = {
+ tasks: filteredTasks,
+ onMarkAsDone: handleMarkAsDone,
+ onLogTime: handleLogTime,
+ intermediateTasks,
+ expandedTasks,
+ onToggleIntermediateTasks: toggleIntermediateTasks,
+ onMarkIntermediateAsDone: handleMarkIntermediateAsDone,
+ darkMode,
+ };
+
+ let taskContent;
+ if (filteredTasks.length === 0) {
+ taskContent = (
+
+
{emptyMessages[activeFilter]}
+
+ );
+ } else if (viewMode === 'card') {
+ taskContent =
;
+ } else {
+ taskContent =
;
+ }
+
return (
@@ -174,10 +228,29 @@ const StudentDashboard = () => {
{/* Summary Cards */}
- {/* Recent Time Logs Section */}
+ {/* Tasks Section */}
+ {/* Section header row */}
-
Recent Time Logs
+ {/* Filter tabs */}
+
+ {FILTER_TABS.map(tab => (
+ setActiveFilter(tab.key)}
+ >
+ {tab.label}
+ {tabCounts[tab.key]}
+
+ ))}
+
+
+ {/* View toggle */}
{
{/* Task Views */}
- {viewMode === 'card' ? (
-
- ) : (
-
- )}
+ {taskContent}
diff --git a/src/components/EductionPortal/StudentDashboard/StudentDashboard.module.css b/src/components/EductionPortal/StudentDashboard/StudentDashboard.module.css
index 4052265dbc..c814f2b154 100644
--- a/src/components/EductionPortal/StudentDashboard/StudentDashboard.module.css
+++ b/src/components/EductionPortal/StudentDashboard/StudentDashboard.module.css
@@ -40,7 +40,9 @@
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 2rem;
+ flex-wrap: wrap;
+ gap: 1rem;
+ margin-bottom: 1.5rem;
}
.sectionTitle {
@@ -50,6 +52,74 @@
margin: 0;
}
+/* Filter tabs */
+.filterTabs {
+ display: flex;
+ gap: 0.25rem;
+ background-color: #f3f4f6;
+ border-radius: 8px;
+ padding: 4px;
+ flex-wrap: wrap;
+}
+
+.filterTab {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 0.4rem 0.9rem;
+ background: transparent;
+ border: none;
+ border-radius: 6px;
+ font-size: 0.875rem;
+ font-weight: 500;
+ color: #6b7280;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ white-space: nowrap;
+}
+
+.filterTab:hover {
+ background-color: #e5e7eb;
+ color: #374151;
+}
+
+.filterTabActive {
+ background-color: #fff;
+ color: #1a1a1a;
+ box-shadow: 0 1px 3px rgb(0 0 0 / 10%);
+}
+
+.filterCount {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 20px;
+ height: 20px;
+ padding: 0 5px;
+ background-color: #e5e7eb;
+ border-radius: 10px;
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: #374151;
+}
+
+.filterTabActive .filterCount {
+ background-color: #3b82f6;
+ color: #fff;
+}
+
+/* Empty state for filtered tabs */
+.emptyState {
+ text-align: center;
+ padding: 3rem 1rem;
+ color: #6b7280;
+ font-size: 1rem;
+}
+
+.emptyState p {
+ margin: 0;
+}
+
.viewToggle {
display: flex;
gap: 0.5rem;
@@ -131,24 +201,56 @@
/* Responsive Design */
@media (width <= 768px) {
.mainContainer {
- padding: 1rem 0.5rem;
+ padding: 1rem 0.75rem;
}
.title {
- font-size: 2rem;
+ font-size: 1.75rem;
+ }
+
+ .subtitle {
+ font-size: 1rem;
+ }
+
+ .header {
+ margin-bottom: 2rem;
}
.sectionHeader {
flex-direction: column;
- gap: 1rem;
align-items: flex-start;
}
+ .filterTabs {
+ width: 100%;
+ }
+
+ .filterTab {
+ flex: 1;
+ justify-content: center;
+ padding: 0.4rem 0.5rem;
+ font-size: 0.8rem;
+ }
+
.viewToggle {
align-self: flex-end;
}
}
+@media (width <= 480px) {
+ .mainContainer {
+ padding: 0.75rem 0.5rem;
+ }
+
+ .title {
+ font-size: 1.5rem;
+ }
+
+ .filterTab {
+ font-size: 0.75rem;
+ }
+}
+
/* Dark Mode Styles - Using global dark-mode class for consistency across builds */
:global(.dark-mode) .dashboard {
background-color: #1a1a1a;
@@ -166,6 +268,38 @@
color: #fff;
}
+:global(.dark-mode) .filterTabs {
+ background-color: #2d2d2d;
+}
+
+:global(.dark-mode) .filterTab {
+ color: #9ca3af;
+}
+
+:global(.dark-mode) .filterTab:hover {
+ background-color: #363636;
+ color: #d1d5db;
+}
+
+:global(.dark-mode) .filterTabActive {
+ background-color: #404040;
+ color: #fff;
+}
+
+:global(.dark-mode) .filterCount {
+ background-color: #4b5563;
+ color: #d1d5db;
+}
+
+:global(.dark-mode) .filterTabActive .filterCount {
+ background-color: #2563eb;
+ color: #fff;
+}
+
+:global(.dark-mode) .emptyState {
+ color: #9ca3af;
+}
+
:global(.dark-mode) .viewToggle {
background-color: #2d2d2d;
box-shadow: 0 1px 3px rgb(0 0 0 / 30%);
diff --git a/src/components/EductionPortal/StudentDashboard/TaskCard.module.css b/src/components/EductionPortal/StudentDashboard/TaskCard.module.css
index cf35302eea..928c566774 100644
--- a/src/components/EductionPortal/StudentDashboard/TaskCard.module.css
+++ b/src/components/EductionPortal/StudentDashboard/TaskCard.module.css
@@ -362,17 +362,17 @@
/* Responsive Design */
@media (width <= 768px) {
.taskCard {
- padding: 1.25rem;
- min-height: 260px;
+ padding: 1rem;
+ min-height: unset;
}
.actionButtons {
- flex-direction: column;
+ flex-wrap: wrap;
gap: 0.5rem;
}
.clockButton {
- width: 100%;
+ width: 40px;
}
.intermediateTaskItem {
@@ -384,6 +384,25 @@
}
}
+@media (width <= 480px) {
+ .taskCard {
+ padding: 0.875rem;
+ border-radius: 8px;
+ }
+
+ .taskTitle {
+ font-size: 1rem;
+ }
+
+ .actionButtons {
+ flex-direction: column;
+ }
+
+ .clockButton {
+ width: 100%;
+ }
+}
+
/* Dark Mode Styles - Using global dark-mode class for consistency across builds */
:global(.dark-mode) .taskCard {
background-color: #2d2d2d;
@@ -456,6 +475,7 @@
color: #d1d5db;
}
+/* stylelint-disable-next-line no-descending-specificity */
:global(.dark-mode) .markDoneButton {
background-color: #2563eb;
color: #fff;
@@ -544,6 +564,7 @@
color: #fbbf24;
}
+/* stylelint-disable-next-line no-descending-specificity */
:global(.dark-mode) .markIntermediateDoneButton {
background-color: #2d2d2d;
border-color: #404040;
diff --git a/src/components/EductionPortal/StudentDashboard/TaskCardView.module.css b/src/components/EductionPortal/StudentDashboard/TaskCardView.module.css
index 9fdbe180ed..189bdc9918 100644
--- a/src/components/EductionPortal/StudentDashboard/TaskCardView.module.css
+++ b/src/components/EductionPortal/StudentDashboard/TaskCardView.module.css
@@ -1,8 +1,8 @@
/* stylelint-disable */
.cardView {
display: grid;
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
- gap: 1.5rem;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 1.25rem;
}
.emptyState {
@@ -20,7 +20,7 @@
@media (width <= 768px) {
.cardView {
grid-template-columns: 1fr;
- gap: 1rem;
+ gap: 0.875rem;
}
}
diff --git a/src/components/EductionPortal/StudentDashboard/TaskListItem.module.css b/src/components/EductionPortal/StudentDashboard/TaskListItem.module.css
index 9e9552fa7f..ec4c8d4fe4 100644
--- a/src/components/EductionPortal/StudentDashboard/TaskListItem.module.css
+++ b/src/components/EductionPortal/StudentDashboard/TaskListItem.module.css
@@ -352,12 +352,14 @@
.taskListItem {
flex-direction: column;
align-items: flex-start;
- gap: 1rem;
+ gap: 0.875rem;
+ padding: 1rem;
}
.actionIcons {
- flex-direction: row;
+ flex-flow: row wrap;
align-self: flex-end;
+ gap: 0.5rem;
}
.intermediateTaskItem {
@@ -369,6 +371,17 @@
}
}
+@media (width <= 480px) {
+ .taskListItem {
+ padding: 0.875rem;
+ border-radius: 8px;
+ }
+
+ .taskTitle {
+ font-size: 1rem;
+ }
+}
+
/* Dark Mode Styles - Using global dark-mode class for consistency across builds */
:global(.dark-mode) .taskListItem {
background-color: #2d2d2d;
@@ -441,6 +454,7 @@
color: #d1d5db;
}
+/* stylelint-disable-next-line no-descending-specificity */
:global(.dark-mode) .markDoneButton {
background-color: #2563eb;
color: #fff;
@@ -531,6 +545,7 @@
color: #fbbf24;
}
+/* stylelint-disable-next-line no-descending-specificity */
:global(.dark-mode) .markIntermediateDoneButton {
background-color: #2d2d2d;
border-color: #404040;
diff --git a/src/routes.jsx b/src/routes.jsx
index f2fc47c514..4e99936964 100644
--- a/src/routes.jsx
+++ b/src/routes.jsx
@@ -1027,6 +1027,8 @@ export default (
path="/communityportal/reports/EventNoShowChart"
component={EventNoShowChart}
/>
+
+