feat: initial commit of CampusFlow student portal with theme configuration
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
node_modules/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Student Dashboard
|
||||||
|
|
||||||
|
A responsive Student Dashboard built using React JS and Tailwind CSS.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Dashboard Overview
|
||||||
|
- Student Profile
|
||||||
|
- My Courses
|
||||||
|
- Task Board
|
||||||
|
- Academic Progress Tracking
|
||||||
|
- Responsive Design
|
||||||
|
|
||||||
|
## Technologies Used
|
||||||
|
|
||||||
|
- React JS
|
||||||
|
- Tailwind CSS
|
||||||
|
- JavaScript
|
||||||
|
- Vite
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Arshiya Indikar
|
||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Student Dashboard</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "Student Dashboard",
|
||||||
|
"description": "A responsive and elegant student dashboard to track courses, assignments, attendance, performance, and schedule.",
|
||||||
|
"requestFramePermissions": [],
|
||||||
|
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
||||||
|
}
|
||||||
Generated
+4347
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "react-example",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --port=3000 --host=0.0.0.0",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"clean": "rm -rf dist server.js",
|
||||||
|
"lint": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@google/genai": "^2.4.0",
|
||||||
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
|
"@vitejs/plugin-react": "^5.0.4",
|
||||||
|
"lucide-react": "^0.546.0",
|
||||||
|
"react": "^19.0.1",
|
||||||
|
"react-dom": "^19.0.1",
|
||||||
|
"vite": "^6.2.3",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"motion": "^12.23.24"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.14.0",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"esbuild": "^0.25.0",
|
||||||
|
"tailwindcss": "^4.1.14",
|
||||||
|
"tsx": "^4.21.0",
|
||||||
|
"typescript": "~5.8.2",
|
||||||
|
"vite": "^6.2.3",
|
||||||
|
"@types/express": "^4.17.21"
|
||||||
|
}
|
||||||
|
}
|
||||||
+357
@@ -0,0 +1,357 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
import {
|
||||||
|
Menu,
|
||||||
|
Bell,
|
||||||
|
Clock,
|
||||||
|
User,
|
||||||
|
Search,
|
||||||
|
BookOpen,
|
||||||
|
CalendarDays,
|
||||||
|
LogOut,
|
||||||
|
ChevronDown,
|
||||||
|
Compass,
|
||||||
|
FileCheck
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
// Subcomponents
|
||||||
|
import Sidebar from './components/Sidebar';
|
||||||
|
import DashboardHome from './components/DashboardHome';
|
||||||
|
import CourseList from './components/CourseList';
|
||||||
|
import AssignmentTracker from './components/AssignmentTracker';
|
||||||
|
import AttendanceCalendar from './components/AttendanceCalendar';
|
||||||
|
import GradeCalculator from './components/GradeCalculator';
|
||||||
|
import ProfileView from './components/ProfileView';
|
||||||
|
|
||||||
|
// Seed Data
|
||||||
|
import {
|
||||||
|
INITIAL_PROFILE,
|
||||||
|
INITIAL_COURSES,
|
||||||
|
INITIAL_ASSIGNMENTS,
|
||||||
|
INITIAL_ANNOUNCEMENTS,
|
||||||
|
INITIAL_ATTENDANCE
|
||||||
|
} from './data/mockStudentData';
|
||||||
|
|
||||||
|
// Theme Configurations
|
||||||
|
const COLOR_THEMES = {
|
||||||
|
indigo: {
|
||||||
|
primary: 'bg-indigo-600',
|
||||||
|
hover: 'hover:bg-indigo-700',
|
||||||
|
text: 'text-indigo-600',
|
||||||
|
bg: 'bg-indigo-50',
|
||||||
|
border: 'border-indigo-100',
|
||||||
|
name: 'Royal Indigo',
|
||||||
|
accent: 'from-indigo-500 to-indigo-600'
|
||||||
|
},
|
||||||
|
emerald: {
|
||||||
|
primary: 'bg-emerald-600',
|
||||||
|
hover: 'hover:bg-emerald-700',
|
||||||
|
text: 'text-emerald-600',
|
||||||
|
bg: 'bg-emerald-50',
|
||||||
|
border: 'border-emerald-100',
|
||||||
|
name: 'Teal Emerald',
|
||||||
|
accent: 'from-emerald-500 to-emerald-605'
|
||||||
|
},
|
||||||
|
violet: {
|
||||||
|
primary: 'bg-violet-600',
|
||||||
|
hover: 'hover:bg-violet-700',
|
||||||
|
text: 'text-violet-600',
|
||||||
|
bg: 'bg-violet-50',
|
||||||
|
border: 'border-violet-100',
|
||||||
|
name: 'Neon Violet',
|
||||||
|
accent: 'from-violet-500 to-violet-600'
|
||||||
|
},
|
||||||
|
amber: {
|
||||||
|
primary: 'bg-amber-500',
|
||||||
|
hover: 'hover:bg-amber-600',
|
||||||
|
text: 'text-amber-600',
|
||||||
|
bg: 'bg-amber-50',
|
||||||
|
border: 'border-amber-100',
|
||||||
|
name: 'Bright Amber',
|
||||||
|
accent: 'from-amber-550 to-amber-500'
|
||||||
|
},
|
||||||
|
rose: {
|
||||||
|
primary: 'bg-rose-500',
|
||||||
|
hover: 'hover:bg-rose-650',
|
||||||
|
text: 'text-rose-600',
|
||||||
|
bg: 'bg-rose-50',
|
||||||
|
border: 'border-rose-100',
|
||||||
|
name: 'Elegant Rose',
|
||||||
|
accent: 'from-rose-500 to-rose-600'
|
||||||
|
},
|
||||||
|
slate: {
|
||||||
|
primary: 'bg-slate-800',
|
||||||
|
hover: 'hover:bg-slate-900',
|
||||||
|
text: 'text-slate-800',
|
||||||
|
bg: 'bg-slate-100',
|
||||||
|
border: 'border-slate-205',
|
||||||
|
name: 'Minimal Slate',
|
||||||
|
accent: 'from-slate-700 to-slate-808'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
|
||||||
|
// Storage hooks or Seed fallbacks
|
||||||
|
const [profile, setProfile] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('university_student_profile');
|
||||||
|
return saved ? JSON.parse(saved) : INITIAL_PROFILE;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [courses, setCourses] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('university_student_courses');
|
||||||
|
return saved ? JSON.parse(saved) : INITIAL_COURSES;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [assignments, setAssignments] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('university_student_assignments');
|
||||||
|
return saved ? JSON.parse(saved) : INITIAL_ASSIGNMENTS;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [announcements] = useState(INITIAL_ANNOUNCEMENTS);
|
||||||
|
|
||||||
|
const [attendance, setAttendance] = useState(() => {
|
||||||
|
const saved = localStorage.getItem('university_student_attendance');
|
||||||
|
return saved ? JSON.parse(saved) : INITIAL_ATTENDANCE;
|
||||||
|
});
|
||||||
|
|
||||||
|
// State Navigation controls
|
||||||
|
const [activeTab, setActiveTab] = useState('dashboard');
|
||||||
|
const [isMobileSidebarOpen, setIsMobileSidebarOpen] = useState(false);
|
||||||
|
const [selectedColorKey, setSelectedColorKey] = useState(() => {
|
||||||
|
return localStorage.getItem('university_theme_choice') || 'indigo';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show live Notification bells dropdown count
|
||||||
|
const [showNotificationList, setShowNotificationList] = useState(false);
|
||||||
|
const notifications = [
|
||||||
|
{ id: '1', msg: 'Exam Schedule is active', time: '1 hr ago', read: false },
|
||||||
|
{ id: '2', msg: 'Sarah Thomas scheduled fullstack assignment', time: '5 hrs ago', read: false },
|
||||||
|
{ id: '3', msg: 'New resource added in Tech Wing', time: 'Yesterday', read: true }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Sync state mutations to LocalStorage
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('university_student_profile', JSON.stringify(profile));
|
||||||
|
}, [profile]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('university_student_courses', JSON.stringify(courses));
|
||||||
|
}, [courses]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('university_student_assignments', JSON.stringify(assignments));
|
||||||
|
}, [assignments]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('university_student_attendance', JSON.stringify(attendance));
|
||||||
|
}, [attendance]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('university_theme_choice', selectedColorKey);
|
||||||
|
}, [selectedColorKey]);
|
||||||
|
|
||||||
|
// Handle fast status toggling on dashboard urgent tasks list
|
||||||
|
const handleToggleAssignmentStatus = (id) => {
|
||||||
|
const updated = assignments.map(as => {
|
||||||
|
if (as.id === id) {
|
||||||
|
const nextStatus = as.status === 'Completed' ? 'Pending' : 'Completed';
|
||||||
|
return { ...as, status: nextStatus };
|
||||||
|
}
|
||||||
|
return as;
|
||||||
|
});
|
||||||
|
setAssignments(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
const themeColor = COLOR_THEMES[selectedColorKey] || COLOR_THEMES.indigo;
|
||||||
|
|
||||||
|
// Header Nav element values
|
||||||
|
const currentLocalTime = new Date().toLocaleDateString('en', {
|
||||||
|
weekday: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-slate-50 flex" id="student-portal-app">
|
||||||
|
|
||||||
|
{/* Drawer & Bar Side Pane */}
|
||||||
|
<Sidebar
|
||||||
|
activeTab={activeTab}
|
||||||
|
setActiveTab={setActiveTab}
|
||||||
|
isOpen={isMobileSidebarOpen}
|
||||||
|
setIsOpen={setIsMobileSidebarOpen}
|
||||||
|
profile={profile}
|
||||||
|
themeColor={themeColor}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Main Content Box Canvas */}
|
||||||
|
<div className="flex-1 md:pl-64 flex flex-col min-w-0">
|
||||||
|
|
||||||
|
{/* Top Navbar Header */}
|
||||||
|
<header id="main-desktop-navbar" className="bg-white border-b border-slate-101 shadow-xs h-16 px-4 md:px-8 flex items-center justify-between sticky top-0 z-30">
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Hamburger for mobile panel */}
|
||||||
|
<button
|
||||||
|
id="hamburger-sidebar-trigger"
|
||||||
|
onClick={() => setIsMobileSidebarOpen(true)}
|
||||||
|
className="md:hidden p-1.5 text-slate-500 hover:bg-slate-100 rounded-lg"
|
||||||
|
aria-label="Open Navigation menu"
|
||||||
|
>
|
||||||
|
<Menu className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Timetable clock indicator */}
|
||||||
|
<p className="text-xs text-slate-550 text-slate-500 font-bold hidden md:flex items-center gap-1.5">
|
||||||
|
<Clock className={`h-4 w-4 ${themeColor.text}`} />
|
||||||
|
<span>{currentLocalTime} (UTC Standard)</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Gitea Connected pulse indicator */}
|
||||||
|
<div className={`flex items-center space-x-2 ${themeColor.bg} px-3 py-1 rounded-full border ${themeColor.border} shadow-xxs`}>
|
||||||
|
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></span>
|
||||||
|
<span className={`text-[11px] font-semibold ${themeColor.text}`}>Gitea Connected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="navbar-profile-interactions" className="flex items-center gap-4">
|
||||||
|
|
||||||
|
{/* Direct Notifications bell */}
|
||||||
|
<div className="relative">
|
||||||
|
<button
|
||||||
|
id="bell-dropdown-trigger"
|
||||||
|
onClick={() => setShowNotificationList(!showNotificationList)}
|
||||||
|
className="p-1.5 hover:bg-slate-50 rounded-xl relative cursor-pointer text-slate-500"
|
||||||
|
>
|
||||||
|
<Bell className="h-5 w-5" />
|
||||||
|
<span className="absolute top-1 right-1 h-2 w-2 bg-rose-500 rounded-full"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{showNotificationList && (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 z-40" onClick={() => setShowNotificationList(false)}></div>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 10 }}
|
||||||
|
className="absolute right-0 mt-2.5 w-64 bg-white rounded-xl border border-slate-100 shadow-xl z-50 p-2 space-y-1.5"
|
||||||
|
>
|
||||||
|
<h4 className="text-[10px] uppercase font-mono tracking-wider text-slate-450 text-slate-400 p-1 bg-slate-50 rounded">
|
||||||
|
Recent Alerts
|
||||||
|
</h4>
|
||||||
|
{notifications.map(n => (
|
||||||
|
<div key={n.id} className="text-xs p-2 rounded-lg hover:bg-slate-50 border-b border-slate-50 last:border-0">
|
||||||
|
<p className="font-semibold text-slate-800">{n.msg}</p>
|
||||||
|
<span className="text-[10px] text-slate-400 font-mono block mt-0.5">{n.time}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-6 w-px bg-slate-200"></div>
|
||||||
|
|
||||||
|
{/* Quick Profile widget links */}
|
||||||
|
<button
|
||||||
|
id="top-profile-badge-link"
|
||||||
|
onClick={() => setActiveTab('profile')}
|
||||||
|
className="flex items-center gap-2 px-1.5 py-1 hover:bg-slate-50 rounded-xl transition text-left"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={profile.avatarUrl}
|
||||||
|
alt={profile.name}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
className="h-8 w-8 rounded-full object-cover border border-slate-200"
|
||||||
|
/>
|
||||||
|
<div className="hidden lg:block min-w-0">
|
||||||
|
<p className="text-xs font-semibold text-slate-700 truncate max-w-[100px]" id="top-profile-name">
|
||||||
|
{profile.name.split(' ')[0]}
|
||||||
|
</p>
|
||||||
|
<span className="text-[9px] text-slate-400 font-mono">Sophomore</span>
|
||||||
|
</div>
|
||||||
|
<ChevronDown className="h-3.5 w-3.5 text-slate-405 text-slate-400 hidden lg:block" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Main tabs layout selector panel wrapping dynamic panels with an transitions panel */}
|
||||||
|
<main id="main-scrollable-canvas" className="flex-1 overflow-y-auto px-4 md:px-8 py-6 max-w-7xl w-full mx-auto pb-16">
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.div
|
||||||
|
key={activeTab}
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.18 }}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{activeTab === 'dashboard' && (
|
||||||
|
<DashboardHome
|
||||||
|
courses={courses}
|
||||||
|
assignments={assignments}
|
||||||
|
announcements={announcements}
|
||||||
|
attendance={attendance}
|
||||||
|
onToggleAssignmentStatus={handleToggleAssignmentStatus}
|
||||||
|
setActiveTab={setActiveTab}
|
||||||
|
themeColor={themeColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'courses' && (
|
||||||
|
<CourseList
|
||||||
|
courses={courses}
|
||||||
|
setCourses={setCourses}
|
||||||
|
themeColor={themeColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'assignments' && (
|
||||||
|
<AssignmentTracker
|
||||||
|
assignments={assignments}
|
||||||
|
setAssignments={setAssignments}
|
||||||
|
courses={courses}
|
||||||
|
themeColor={themeColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'attendance' && (
|
||||||
|
<AttendanceCalendar
|
||||||
|
attendance={attendance}
|
||||||
|
setAttendance={setAttendance}
|
||||||
|
courses={courses}
|
||||||
|
setCourses={setCourses}
|
||||||
|
themeColor={themeColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'grades' && (
|
||||||
|
<GradeCalculator
|
||||||
|
courses={courses}
|
||||||
|
themeColor={themeColor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'profile' && (
|
||||||
|
<ProfileView
|
||||||
|
profile={profile}
|
||||||
|
setProfile={setProfile}
|
||||||
|
selectedColorKey={selectedColorKey}
|
||||||
|
setSelectedColorKey={setSelectedColorKey}
|
||||||
|
themeColor={themeColor}
|
||||||
|
colorOptions={COLOR_THEMES}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,423 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
import {
|
||||||
|
Plus,
|
||||||
|
Search,
|
||||||
|
Trash2,
|
||||||
|
Clock,
|
||||||
|
CheckCircle,
|
||||||
|
X,
|
||||||
|
ArrowRight,
|
||||||
|
ArrowLeft,
|
||||||
|
FolderOpen
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export default function AssignmentTracker({ assignments, setAssignments, courses, themeColor }) {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [selectedCourseId, setSelectedCourseId] = useState('All');
|
||||||
|
const [selectedPriority, setSelectedPriority] = useState('All');
|
||||||
|
|
||||||
|
// Custom dialog simulation with modal
|
||||||
|
const [showAddForm, setShowAddForm] = useState(false);
|
||||||
|
const [newTitle, setNewTitle] = useState('');
|
||||||
|
const [newDescription, setNewDescription] = useState('');
|
||||||
|
const [newCourseId, setNewCourseId] = useState(courses[0]?.id || '');
|
||||||
|
const [newPriority, setNewPriority] = useState('Medium');
|
||||||
|
const [newDueDate, setNewDueDate] = useState('2026-06-15');
|
||||||
|
|
||||||
|
const handleMoveStatus = (id, currentStatus, direction) => {
|
||||||
|
const statusOrder = ['Pending', 'In_Progress', 'Completed'];
|
||||||
|
const currentIndex = statusOrder.indexOf(currentStatus);
|
||||||
|
|
||||||
|
let nextIndex = currentIndex;
|
||||||
|
if (direction === 'forward' && currentIndex < 2) {
|
||||||
|
nextIndex = currentIndex + 1;
|
||||||
|
} else if (direction === 'backward' && currentIndex > 0) {
|
||||||
|
nextIndex = currentIndex - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextIndex !== currentIndex) {
|
||||||
|
const updated = assignments.map(a => {
|
||||||
|
if (a.id === id) {
|
||||||
|
return { ...a, status: statusOrder[nextIndex] };
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
});
|
||||||
|
setAssignments(updated);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteAssignment = (id) => {
|
||||||
|
if (confirm('Are you sure you want to delete this task?')) {
|
||||||
|
setAssignments(assignments.filter(a => a.id !== id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateAssignment = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newTitle.trim()) return;
|
||||||
|
|
||||||
|
const courseObj = courses.find(c => c.id === newCourseId);
|
||||||
|
|
||||||
|
const newAss = {
|
||||||
|
id: 'ass-' + Date.now().toString(),
|
||||||
|
title: newTitle,
|
||||||
|
courseId: newCourseId,
|
||||||
|
courseName: courseObj ? courseObj.name : 'General Task',
|
||||||
|
description: newDescription,
|
||||||
|
dueDate: newDueDate,
|
||||||
|
priority: newPriority,
|
||||||
|
status: 'Pending'
|
||||||
|
};
|
||||||
|
|
||||||
|
setAssignments([newAss, ...assignments]);
|
||||||
|
|
||||||
|
// Reset form states
|
||||||
|
setNewTitle('');
|
||||||
|
setNewDescription('');
|
||||||
|
setShowAddForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter calculations
|
||||||
|
const filteredAssignments = assignments.filter((item) => {
|
||||||
|
const matchesSearch = item.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
item.description.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
const matchesPriority = selectedPriority === 'All' ? true : item.priority === selectedPriority;
|
||||||
|
const matchesCourse = selectedCourseId === 'All' ? true : item.courseId === selectedCourseId;
|
||||||
|
return matchesSearch && matchesPriority && matchesCourse;
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ id: 'Pending', title: 'To Do', colorClass: 'border-rose-500', bgClass: 'bg-rose-50/20', textClass: 'text-rose-600' },
|
||||||
|
{ id: 'In_Progress', title: 'In Progress', colorClass: 'border-amber-500', bgClass: 'bg-amber-50/20', textClass: 'text-amber-600' },
|
||||||
|
{ id: 'Completed', title: 'Completed', colorClass: 'border-emerald-500', bgClass: 'bg-emerald-50/20', textClass: 'text-emerald-600' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="assignment-tracker-view" className="space-y-6">
|
||||||
|
|
||||||
|
{/* Overview stats & filter controls header */}
|
||||||
|
<div className="bg-white p-5 rounded-2xl border border-slate-100 shadow-sm space-y-4">
|
||||||
|
|
||||||
|
{/* Statistics highlights bar */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-50 pb-4">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-slate-800">Task Board</h2>
|
||||||
|
<p className="text-xs text-slate-500 mt-1">Check progress & prioritize assignments dynamically</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="add-assignment-modal-trigger"
|
||||||
|
onClick={() => setShowAddForm(true)}
|
||||||
|
className={`cursor-pointer px-4 py-2 rounded-xl text-xs font-semibold ${themeColor.primary} ${themeColor.hover} text-white flex items-center gap-1.5 shadow-sm transition-all`}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add New Task
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dynamic Live Filtering Controls */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||||
|
|
||||||
|
{/* Search bar */}
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-3.5 top-3 h-4 w-4 text-slate-400" />
|
||||||
|
<input
|
||||||
|
id="assignment-search-input"
|
||||||
|
type="text"
|
||||||
|
placeholder="Search assignments..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-800 pl-10 pr-4 py-2.5 rounded-xl border border-slate-200 outline-none bg-slate-50 focus:border-indigo-400 focus:bg-white transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Module course ID filter */}
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
id="course-id-filter-select"
|
||||||
|
value={selectedCourseId}
|
||||||
|
onChange={(e) => setSelectedCourseId(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-705 px-3 py-2.5 rounded-xl border border-slate-200 bg-slate-50 outline-none focus:border-indigo-400 focus:bg-white transition-all font-semibold"
|
||||||
|
>
|
||||||
|
<option value="All">All Courses</option>
|
||||||
|
{courses.map(c => (
|
||||||
|
<option key={c.id} value={c.id}>{c.code} - {c.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Priority flag filter */}
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
id="priority-filter-select"
|
||||||
|
value={selectedPriority}
|
||||||
|
onChange={(e) => setSelectedPriority(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-750 px-3 py-2.5 rounded-xl border border-slate-200 bg-slate-50 outline-none focus:border-indigo-405 focus:bg-white transition-all font-semibold"
|
||||||
|
>
|
||||||
|
<option value="All">All Priorities</option>
|
||||||
|
<option value="High">🔴 High priority</option>
|
||||||
|
<option value="Medium">🟡 Medium priority</option>
|
||||||
|
<option value="Low">🟢 Low priority</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Counter label indicator */}
|
||||||
|
<div className="bg-slate-50 p-2.5 rounded-xl border border-slate-200/50 flex items-center justify-between font-mono text-[11px] text-slate-500 font-bold px-4">
|
||||||
|
<span>Filtered:</span>
|
||||||
|
<span className={`${themeColor.text}`}>{filteredAssignments.length} Assignments</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Kanban Board block */}
|
||||||
|
<div id="kanban-swimlanes-grid" className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
|
||||||
|
|
||||||
|
{columns.map((col) => {
|
||||||
|
const colItems = filteredAssignments.filter(item => item.status === col.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={col.id}
|
||||||
|
id={`kanban-column-${col.id}`}
|
||||||
|
className="bg-slate-50 rounded-2xl p-4 border border-slate-100 flex flex-col min-h-[500px]"
|
||||||
|
>
|
||||||
|
{/* Lane Heading Header */}
|
||||||
|
<div className="flex items-center justify-between mb-4 pb-2 border-b border-slate-200/50">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className={`block h-2.5 w-2.5 rounded-full ${
|
||||||
|
col.id === 'Pending' ? 'bg-rose-500' :
|
||||||
|
col.id === 'In_Progress' ? 'bg-amber-500' :
|
||||||
|
'bg-emerald-500'
|
||||||
|
}`}></span>
|
||||||
|
<h3 className="font-bold text-slate-800 text-sm tracking-wide">{col.title}</h3>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] font-bold text-slate-400 bg-slate-200/70 px-2 py-0.5 rounded-full">
|
||||||
|
{colItems.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Lane Items collection */}
|
||||||
|
<div className="space-y-3.5 flex-1 overflow-y-auto max-h-[600px] pr-1">
|
||||||
|
{colItems.map((item, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={item.id}
|
||||||
|
id={`assignment-kanban-card-${item.id}`}
|
||||||
|
initial={{ scale: 0.95, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ delay: idx * 0.04 }}
|
||||||
|
className="bg-white rounded-xl p-4 border border-slate-100 shadow-sm hover:shadow transition-shadow space-y-3 flex flex-col relative"
|
||||||
|
>
|
||||||
|
{/* Header tags inside cards */}
|
||||||
|
<div className="flex justify-between items-start gap-1">
|
||||||
|
<span className="text-[9px] font-bold text-slate-400 uppercase font-mono tracking-tight bg-slate-50 px-1.5 py-0.5 rounded truncate max-w-[120px]">
|
||||||
|
{item.courseName}
|
||||||
|
</span>
|
||||||
|
<span className={`text-[9px] font-bold uppercase rounded px-1.5 py-0.2 shrink-0 ${
|
||||||
|
item.priority === 'High' ? 'bg-rose-50 text-rose-600 font-bold' :
|
||||||
|
item.priority === 'Medium' ? 'bg-amber-50 text-amber-600' :
|
||||||
|
'bg-slate-50 text-slate-600'
|
||||||
|
}`}>
|
||||||
|
{item.priority}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title and Description */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<h4 className="font-bold text-slate-800 text-sm leading-tight">{item.title}</h4>
|
||||||
|
<p className="text-xs text-slate-500 line-clamp-2 leading-relaxed h-8">
|
||||||
|
{item.description || 'No description provided.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeline detail */}
|
||||||
|
<div className="flex items-center gap-1.5 text-[11px] text-slate-400 font-mono font-medium pt-2 border-t border-slate-50/80">
|
||||||
|
<Clock className="h-3.5 w-3.5 inline text-slate-400 shrink-0" />
|
||||||
|
<span>Due: {item.dueDate}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Column controls buttons */}
|
||||||
|
<div className="flex items-center justify-between pt-1 gap-1 flex-wrap">
|
||||||
|
|
||||||
|
{/* Left control button */}
|
||||||
|
{col.id !== 'Pending' ? (
|
||||||
|
<button
|
||||||
|
id={`move-left-btn-${item.id}`}
|
||||||
|
onClick={() => handleMoveStatus(item.id, item.status, 'backward')}
|
||||||
|
aria-label="Move Task Back"
|
||||||
|
className="p-1 px-1.5 bg-slate-50 text-slate-500 rounded hover:bg-slate-100 hover:text-slate-700 transition"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="w-1.5"></span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Delete icon */}
|
||||||
|
<button
|
||||||
|
id={`delete-task-btn-${item.id}`}
|
||||||
|
onClick={() => handleDeleteAssignment(item.id)}
|
||||||
|
className="p-1 text-slate-300 hover:text-rose-500 transition-colors cursor-pointer"
|
||||||
|
title="Delete Task"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Right control button */}
|
||||||
|
{col.id !== 'Completed' ? (
|
||||||
|
<button
|
||||||
|
id={`move-right-btn-${item.id}`}
|
||||||
|
onClick={() => handleMoveStatus(item.id, item.status, 'forward')}
|
||||||
|
aria-label="Move Task Forward"
|
||||||
|
className="p-1 px-1.5 bg-slate-50 text-slate-500 rounded hover:bg-slate-100 hover:text-slate-700 transition"
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="p-1 bg-emerald-50 text-emerald-600 rounded">
|
||||||
|
<CheckCircle className="h-3.5 w-3.5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{colItems.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-16 text-center text-slate-400 border border-dashed border-slate-200 rounded-xl bg-white/40">
|
||||||
|
<FolderOpen className="h-8 w-8 text-slate-300" />
|
||||||
|
<p className="text-xs font-medium mt-2">No tasks in this stage.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Floating Add Assignment Modal/Form overlay */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showAddForm && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-xs" id="add-task-modal">
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0.95, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
exit={{ scale: 0.95, opacity: 0 }}
|
||||||
|
className="bg-white rounded-2xl max-w-md w-full shadow-2xl overflow-hidden border border-slate-100"
|
||||||
|
>
|
||||||
|
{/* Modal Head */}
|
||||||
|
<div className="px-6 py-4 bg-slate-900 text-white flex justify-between items-center">
|
||||||
|
<h3 className="font-bold text-base">Add New Assignment</h3>
|
||||||
|
<button
|
||||||
|
id="close-add-task-modal"
|
||||||
|
onClick={() => setShowAddForm(false)}
|
||||||
|
className="p-1.5 text-slate-400 hover:text-white rounded-lg hover:bg-slate-800"
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal Body */}
|
||||||
|
<form onSubmit={handleCreateAssignment} className="p-6 space-y-4">
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 uppercase mb-1">Assignment Title</label>
|
||||||
|
<input
|
||||||
|
id="new-task-title-input"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="E.g. Full-Stack Dashboard build"
|
||||||
|
value={newTitle}
|
||||||
|
onChange={(e) => setNewTitle(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-850 px-3.5 py-2.5 rounded-xl border border-slate-200 focus:outline-none focus:border-indigo-400 bg-slate-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Course Map Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 uppercase mb-1">Assign to Course Module</label>
|
||||||
|
<select
|
||||||
|
id="new-task-course-select"
|
||||||
|
value={newCourseId}
|
||||||
|
onChange={(e) => setNewCourseId(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-800 px-3 py-2.5 rounded-xl border border-slate-200 focus:outline-none bg-slate-50 font-medium"
|
||||||
|
>
|
||||||
|
{courses.map(c => (
|
||||||
|
<option key={c.id} value={c.id}>{c.code} - {c.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Priority Selection */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 uppercase mb-1">Task Priority Flag</label>
|
||||||
|
<select
|
||||||
|
id="new-task-priority-select"
|
||||||
|
value={newPriority}
|
||||||
|
onChange={(e) => setNewPriority(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-800 px-3 py-2.5 rounded-xl border border-slate-205 focus:outline-none bg-slate-50 font-medium"
|
||||||
|
>
|
||||||
|
<option value="High">🔴 High Priority</option>
|
||||||
|
<option value="Medium">🟡 Medium Priority</option>
|
||||||
|
<option value="Low">🟢 Low Priority</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Due Date */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 uppercase mb-1">Deadline Date</label>
|
||||||
|
<input
|
||||||
|
id="new-task-duedate-input"
|
||||||
|
type="date"
|
||||||
|
required
|
||||||
|
value={newDueDate}
|
||||||
|
onChange={(e) => setNewDueDate(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-805 px-3.5 py-2.5 rounded-xl border border-slate-200 focus:outline-none bg-slate-50 font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description Text area */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 uppercase mb-1">Detailed Description (Optional)</label>
|
||||||
|
<textarea
|
||||||
|
id="new-task-desc-input"
|
||||||
|
rows={3}
|
||||||
|
placeholder="Enter project/assignment requirements..."
|
||||||
|
value={newDescription}
|
||||||
|
onChange={(e) => setNewDescription(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-800 px-3.5 py-2.5 rounded-xl border border-slate-200 focus:outline-none focus:border-indigo-400 bg-slate-50 resize-none animate-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submission Actions */}
|
||||||
|
<div className="pt-2 flex justify-end gap-3">
|
||||||
|
<button
|
||||||
|
id="cancel-add-task"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowAddForm(false)}
|
||||||
|
className="px-4 py-2.5 rounded-xl text-xs font-semibold bg-slate-100 text-slate-600 hover:bg-slate-200 transition-colors"
|
||||||
|
>
|
||||||
|
Discard
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="submit-add-task-btn"
|
||||||
|
type="submit"
|
||||||
|
className={`px-5 py-2.5 rounded-xl text-xs font-semibold text-white ${themeColor.primary} ${themeColor.hover} shadow-sm transition-all`}
|
||||||
|
>
|
||||||
|
Create Task
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,394 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
import {
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Inbox,
|
||||||
|
X,
|
||||||
|
Check,
|
||||||
|
FileSpreadsheet,
|
||||||
|
AlertCircle
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export default function AttendanceCalendar({ attendance, setAttendance, courses, themeColor }) {
|
||||||
|
// Calendar View month configurations
|
||||||
|
const [currentDate, setCurrentDate] = useState(new Date(2026, 5, 1)); // June 2026
|
||||||
|
const [selectedDate, setSelectedDate] = useState('2026-06-01');
|
||||||
|
|
||||||
|
// Custom log attendance simulator
|
||||||
|
const [showLogForm, setShowLogForm] = useState(false);
|
||||||
|
const [logCourseId, setLogCourseId] = useState(courses[0]?.id || '');
|
||||||
|
const [logStatus, setLogStatus] = useState('Present');
|
||||||
|
|
||||||
|
// Help calculate courses attendance rate dynamically
|
||||||
|
const recalculateCoursesAttendance = (updatedAttendance) => {
|
||||||
|
// Basic calculator mimicking background university sync
|
||||||
|
courses.forEach(c => {
|
||||||
|
const courseLogs = updatedAttendance.filter(item => item.courseId === c.id);
|
||||||
|
if (courseLogs.length > 0) {
|
||||||
|
const pres = courseLogs.filter(l => l.status === 'Present').length;
|
||||||
|
const lates = courseLogs.filter(l => l.status === 'Late').length;
|
||||||
|
c.attendanceRate = Math.round(((pres + lates * 0.5) / courseLogs.length) * 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentYear = currentDate.getFullYear();
|
||||||
|
const currentMonth = currentDate.getMonth();
|
||||||
|
|
||||||
|
const handlePrevMonth = () => {
|
||||||
|
setCurrentDate(new Date(currentYear, currentMonth - 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextMonth = () => {
|
||||||
|
setCurrentDate(new Date(currentYear, currentMonth + 1, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Days in month generators
|
||||||
|
const daysInMonth = new Date(currentYear, currentMonth + 1, 0).getDate();
|
||||||
|
const firstDayIndex = new Date(currentYear, currentMonth, 1).getDay();
|
||||||
|
|
||||||
|
// Helper date values formatting
|
||||||
|
const formatDateString = (dayNum) => {
|
||||||
|
const mm = String(currentMonth + 1).padStart(2, '0');
|
||||||
|
const dd = String(dayNum).padStart(2, '0');
|
||||||
|
return `${currentYear}-${mm}-${dd}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const monthNames = [
|
||||||
|
'January', 'February', 'March', 'April', 'May', 'June',
|
||||||
|
'July', 'August', 'September', 'October', 'November', 'December'
|
||||||
|
];
|
||||||
|
|
||||||
|
const daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
|
|
||||||
|
// Handle Mark Attendance state
|
||||||
|
const handleMarkAttendance = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const courseObj = courses.find(c => c.id === logCourseId);
|
||||||
|
if (!courseObj) return;
|
||||||
|
|
||||||
|
// Delete any existing log for this date & course
|
||||||
|
const cleaned = attendance.filter(
|
||||||
|
item => !(item.date === selectedDate && item.courseId === logCourseId)
|
||||||
|
);
|
||||||
|
|
||||||
|
const newLog = {
|
||||||
|
date: selectedDate,
|
||||||
|
status: logStatus,
|
||||||
|
courseId: logCourseId,
|
||||||
|
courseName: courseObj.name
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextAttendance = [newLog, ...cleaned];
|
||||||
|
setAttendance(nextAttendance);
|
||||||
|
recalculateCoursesAttendance(nextAttendance);
|
||||||
|
setShowLogForm(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteLog = (date, courseId) => {
|
||||||
|
const nextAttendance = attendance.filter(
|
||||||
|
item => !(item.date === date && item.courseId === courseId)
|
||||||
|
);
|
||||||
|
setAttendance(nextAttendance);
|
||||||
|
recalculateCoursesAttendance(nextAttendance);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Aggregates computed statistics
|
||||||
|
const totalLogsCount = attendance.length;
|
||||||
|
const presentCount = attendance.filter(l => l.status === 'Present').length;
|
||||||
|
const lateCount = attendance.filter(l => l.status === 'Late').length;
|
||||||
|
const absentCount = attendance.filter(l => l.status === 'Absent').length;
|
||||||
|
const netAttendancePercentage = totalLogsCount > 0
|
||||||
|
? Math.round(((presentCount + lateCount * 0.5) / totalLogsCount) * 100)
|
||||||
|
: 100;
|
||||||
|
|
||||||
|
// Selected Date events list
|
||||||
|
const selectedDateLogs = attendance.filter(l => l.date === selectedDate);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="attendance-tracker-view" className="space-y-6">
|
||||||
|
|
||||||
|
{/* Overview Aggregates header */}
|
||||||
|
<div className="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-50 pb-5">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-slate-800">Attendance Log Desk</h2>
|
||||||
|
<p className="text-xs text-slate-500 mt-1">Simulate daily class audits and mark course records</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2 bg-slate-100 p-1 rounded-xl">
|
||||||
|
<span className="text-xs font-semibold px-3 py-1 bg-white text-slate-800 shadow-xs rounded-lg">
|
||||||
|
Overall: {netAttendancePercentage}% Present
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Counter items grids */}
|
||||||
|
<div className="grid grid-cols-3 sm:grid-cols-4 gap-4 mt-5">
|
||||||
|
<div className="bg-emerald-50 rounded-xl p-3.5 text-center">
|
||||||
|
<span className="text-[10px] text-emerald-600 font-bold block uppercase tracking-wider">Present</span>
|
||||||
|
<span className="text-xl md:text-2xl font-black text-emerald-800 mt-1 block">{presentCount} Days</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-amber-50 rounded-xl p-3.5 text-center">
|
||||||
|
<span className="text-[10px] text-amber-600 font-bold block uppercase tracking-wider">Late Arrivals</span>
|
||||||
|
<span className="text-xl md:text-2xl font-black text-amber-800 mt-1 block">{lateCount} Days</span>
|
||||||
|
</div>
|
||||||
|
<div className="bg-rose-50 rounded-xl p-3.5 text-center">
|
||||||
|
<span className="text-[10px] text-rose-600 font-bold block uppercase tracking-wider">Absent Leaves</span>
|
||||||
|
<span className="text-xl md:text-2xl font-black text-rose-800 mt-1 block">{absentCount} Days</span>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-3 sm:col-span-1 bg-slate-50 rounded-xl p-3.5 text-center flex flex-col justify-center">
|
||||||
|
<span className="text-[10px] text-slate-400 font-bold block uppercase tracking-wider">Active Credits</span>
|
||||||
|
<span className="text-sm font-bold text-slate-700 mt-1 block">17 Hrs Scheduled</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid: Calendars block vs Logs breakdown */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
|
||||||
|
{/* Interactive Custom Calendar Grid (Left Columns) */}
|
||||||
|
<div id="calendar-grid-container" className="lg:col-span-2 bg-white rounded-2xl p-5 border border-slate-100 shadow-sm flex flex-col justify-between">
|
||||||
|
|
||||||
|
{/* Header switchers */}
|
||||||
|
<div className="flex items-center justify-between pb-4 border-b border-slate-50">
|
||||||
|
<h3 className="font-bold text-slate-800 text-sm">
|
||||||
|
{monthNames[currentMonth]} {currentYear}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<button
|
||||||
|
id="prev-month-btn"
|
||||||
|
onClick={handlePrevMonth}
|
||||||
|
className="p-1 px-1.5 bg-slate-50 rounded-lg hover:bg-slate-100 text-slate-600 border border-slate-200"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="next-month-btn"
|
||||||
|
onClick={handleNextMonth}
|
||||||
|
className="p-1 px-1.5 bg-slate-50 rounded-lg hover:bg-slate-100 text-slate-600 border border-slate-200"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Days week lists */}
|
||||||
|
<div className="grid grid-cols-7 gap-1 text-center py-2 border-b border-slate-50 font-medium text-[11px] text-slate-400 font-mono uppercase bg-slate-50/50 rounded-lg my-2">
|
||||||
|
{daysOfWeek.map(d => (
|
||||||
|
<span key={d}>{d}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid Blocks */}
|
||||||
|
<div className="grid grid-cols-7 gap-1.5" id="calendar-blocks-collection">
|
||||||
|
{/* Fill empty dates offset from firstday */}
|
||||||
|
{Array.from({ length: firstDayIndex }).map((_, i) => (
|
||||||
|
<div key={`empty-${i}`} className="aspect-square bg-slate-50/30 rounded-lg border border-dashed border-slate-100/40"></div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Days entries */}
|
||||||
|
{Array.from({ length: daysInMonth }).map((_, i) => {
|
||||||
|
const dayNum = i + 1;
|
||||||
|
const dateString = formatDateString(dayNum);
|
||||||
|
const isSelected = selectedDate === dateString;
|
||||||
|
|
||||||
|
// Find logs statuses for this day
|
||||||
|
const dayLogs = attendance.filter(l => l.date === dateString);
|
||||||
|
let statusDot = null;
|
||||||
|
if (dayLogs.length > 0) {
|
||||||
|
const containsAbsent = dayLogs.some(l => l.status === 'Absent');
|
||||||
|
const containsLate = dayLogs.some(l => l.status === 'Late');
|
||||||
|
|
||||||
|
if (containsAbsent) {
|
||||||
|
statusDot = 'bg-rose-500';
|
||||||
|
} else if (containsLate) {
|
||||||
|
statusDot = 'bg-amber-500';
|
||||||
|
} else {
|
||||||
|
statusDot = 'bg-emerald-500';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={dayNum}
|
||||||
|
id={`calendar-day-btn-${dayNum}`}
|
||||||
|
onClick={() => setSelectedDate(dateString)}
|
||||||
|
className={`relative aspect-square rounded-xl border flex flex-col items-center justify-center p-1.5 transition-all text-xs cursor-pointer font-semibold ${
|
||||||
|
isSelected
|
||||||
|
? 'border-indigo-600 bg-indigo-50 text-indigo-700 shadow-xs'
|
||||||
|
: 'border-slate-100 hover:bg-slate-50 text-slate-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{dayNum}</span>
|
||||||
|
{/* Status visual alert bar/dot */}
|
||||||
|
{statusDot && (
|
||||||
|
<span className={`block h-1.5 w-1.5 rounded-full ${statusDot} mt-1`} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar Color explanation legends */}
|
||||||
|
<div className="flex items-center gap-3.5 pt-4 border-t border-slate-100 mt-4 text-[10px] text-slate-500 font-medium">
|
||||||
|
<span className="flex items-center gap-1"><span className="h-2 w-2 rounded-full bg-emerald-500 block"></span> Present</span>
|
||||||
|
<span className="flex items-center gap-1"><span className="h-2 w-2 rounded-full bg-amber-500 block"></span> Late arrival</span>
|
||||||
|
<span className="flex items-center gap-1"><span className="h-2 w-2 rounded-full bg-rose-500 block"></span> Absent cut</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Selected Date log viewer (Column 1) */}
|
||||||
|
<div id="date-logs-summary-container" className="bg-white rounded-2xl p-5 border border-slate-100 shadow-sm flex flex-col justify-between min-h-[400px]">
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-50 pb-3">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-slate-800 text-sm">Attendance Logs</h3>
|
||||||
|
<p className="text-[10px] text-slate-400 font-mono font-medium">{selectedDate}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="open-attendance-add-form"
|
||||||
|
onClick={() => setShowLogForm(true)}
|
||||||
|
className={`p-1.5 rounded-lg ${themeColor.bg} ${themeColor.text} hover:scale-105 transition-transform flex items-center gap-1.5 font-bold text-[10px]`}
|
||||||
|
>
|
||||||
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
Mark log
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Attendance logs list for selectedDate */}
|
||||||
|
<div className="space-y-3 max-h-72 overflow-y-auto pr-1">
|
||||||
|
{selectedDateLogs.map((log) => (
|
||||||
|
<div
|
||||||
|
key={log.courseId}
|
||||||
|
id={`attendance-log-row-${log.courseId}`}
|
||||||
|
className="p-3 bg-slate-50 rounded-xl border border-slate-100/50 flex justify-between items-center bg-slate-50/50"
|
||||||
|
>
|
||||||
|
<div className="min-w-0 pr-2">
|
||||||
|
<p className="font-bold text-slate-800 text-xs truncate" title={log.courseName}>
|
||||||
|
{log.courseName}
|
||||||
|
</p>
|
||||||
|
<span className="text-[9px] text-slate-400 font-bold uppercase">{log.courseId}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
<span className={`text-[10px] font-bold uppercase px-2 py-0.5 rounded-md ${
|
||||||
|
log.status === 'Present' ? 'bg-emerald-50 text-emerald-600' :
|
||||||
|
log.status === 'Late' ? 'bg-amber-50 text-amber-600' :
|
||||||
|
'bg-rose-50 text-rose-600'
|
||||||
|
}`}>
|
||||||
|
{log.status}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id={`delete-attendance-log-${log.courseId}-${selectedDate}`}
|
||||||
|
onClick={() => handleDeleteLog(selectedDate, log.courseId)}
|
||||||
|
className="p-1 hover:text-rose-500 text-slate-300 transition-colors"
|
||||||
|
title="Delete log"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{selectedDateLogs.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-10 text-center space-y-2">
|
||||||
|
<div className="bg-slate-50 p-2.5 rounded-full text-slate-400">
|
||||||
|
<Inbox className="h-7 w-7" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-bold text-slate-700 text-[11px] uppercase">No logs recorded</h4>
|
||||||
|
<p className="text-xs text-slate-400 max-w-[150px] mx-auto">No classes logged on this date.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive Modal-Form embedded vertically/Drawer */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{showLogForm && (
|
||||||
|
<motion.div
|
||||||
|
id="log-attendance-embedded-form"
|
||||||
|
initial={{ opacity: 0, height: 0 }}
|
||||||
|
animate={{ opacity: 1, height: 'auto' }}
|
||||||
|
exit={{ opacity: 0, height: 0 }}
|
||||||
|
className="bg-slate-50 p-4 rounded-xl border border-slate-200 mt-4 space-y-3 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-100 pb-2">
|
||||||
|
<span className="font-bold text-xs text-slate-700">Audit daily class logs</span>
|
||||||
|
<button
|
||||||
|
id="close-attendance-add-form"
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowLogForm(false)}
|
||||||
|
className="p-0.5 text-slate-400 hover:text-slate-600"
|
||||||
|
>
|
||||||
|
<X className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleMarkAttendance} className="space-y-3.5 text-xs">
|
||||||
|
{/* Select course */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] font-bold text-slate-400 uppercase mb-1">Select Lecture</label>
|
||||||
|
<select
|
||||||
|
id="mark-attendance-course-select"
|
||||||
|
required
|
||||||
|
value={logCourseId}
|
||||||
|
onChange={(e) => setLogCourseId(e.target.value)}
|
||||||
|
className="w-full p-2 border border-slate-200 bg-white rounded-lg cursor-pointer font-sans outline-none font-semibold text-slate-705"
|
||||||
|
>
|
||||||
|
{courses.map(c => (
|
||||||
|
<option key={c.id} value={c.id}>{c.code} - {c.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status Option selections */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] font-bold text-slate-400 uppercase mb-1">Mark Status Code</label>
|
||||||
|
<div className="flex gap-2" id="attendance-status-mark-radios">
|
||||||
|
{['Present', 'Late', 'Absent'].map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt}
|
||||||
|
id={`mark-status-btn-${opt.toLowerCase()}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setLogStatus(opt)}
|
||||||
|
className={`flex-1 py-1.5 border rounded-lg font-bold transition-all text-center ${
|
||||||
|
logStatus === opt
|
||||||
|
? opt === 'Present' ? 'bg-emerald-50 border-emerald-500 text-emerald-600 scale-102 font-extrabold' :
|
||||||
|
opt === 'Late' ? 'bg-amber-50 border-amber-500 text-amber-600 scale-102 font-extrabold' :
|
||||||
|
'bg-rose-50 border-rose-500 text-rose-600 scale-102 font-extrabold'
|
||||||
|
: 'border-slate-200 hover:bg-slate-100 bg-white text-slate-600'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{opt}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="submit-attendance-log-btn"
|
||||||
|
type="submit"
|
||||||
|
className={`w-full py-2 ${themeColor.primary} ${themeColor.hover} text-white text-xs font-bold rounded-lg transition shadow-sm`}
|
||||||
|
>
|
||||||
|
Confirm & Save log
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
import {
|
||||||
|
BookOpen,
|
||||||
|
User,
|
||||||
|
Mail,
|
||||||
|
MapPin,
|
||||||
|
Calendar,
|
||||||
|
GraduationCap,
|
||||||
|
CheckCircle,
|
||||||
|
ArrowRight,
|
||||||
|
TrendingUp,
|
||||||
|
Send,
|
||||||
|
Sparkles,
|
||||||
|
Inbox
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export default function CourseList({ courses, setCourses, themeColor }) {
|
||||||
|
const [selectedCourse, setSelectedCourse] = useState(null);
|
||||||
|
|
||||||
|
// Custom dialog simulation with teachers
|
||||||
|
const [emailSubject, setEmailSubject] = useState('');
|
||||||
|
const [emailBody, setEmailBody] = useState('');
|
||||||
|
const [sentMails, setSentMails] = useState([]);
|
||||||
|
const [isSending, setIsSending] = useState(false);
|
||||||
|
const [professorResponse, setProfessorResponse] = useState(null);
|
||||||
|
|
||||||
|
const handleSendEmail = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!selectedCourse || !emailBody.trim() || !emailSubject.trim()) return;
|
||||||
|
|
||||||
|
setIsSending(true);
|
||||||
|
const newMail = {
|
||||||
|
id: Math.random().toString(),
|
||||||
|
subject: emailSubject,
|
||||||
|
body: emailBody,
|
||||||
|
to: selectedCourse.instructor,
|
||||||
|
date: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + ', Today'
|
||||||
|
};
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setSentMails([newMail, ...sentMails]);
|
||||||
|
setIsSending(false);
|
||||||
|
setEmailBody('');
|
||||||
|
setEmailSubject('');
|
||||||
|
|
||||||
|
// Auto reply mock AI answer from professor
|
||||||
|
const nameParts = newMail.to.split(' ');
|
||||||
|
|
||||||
|
const responses = [
|
||||||
|
`Dear Hamza,\n\nThank you for reaching out. I have reviewed your request regarding "${newMail.subject}". Please find me during office hours, or we will discuss this in our next class on ${selectedCourse.schedule.split(' - ')[0]}.\n\nBest regards,\n${selectedCourse.instructor}`,
|
||||||
|
`Hi Hamza,\n\nI appreciate you keeping me informed. I've noted down your concern. Please make sure to download the slides from the student server and review the recommended readings.\n\nSincerely,\n${selectedCourse.instructor}`,
|
||||||
|
`Hello,\n\nRegarding "${newMail.subject}" — I've noted this in my records. Yes, you may submit the assignment up to 24 hours late with a 15% penalty. Avoid repeated delays.\n\nRegards,\n${selectedCourse.instructor}`
|
||||||
|
];
|
||||||
|
|
||||||
|
setProfessorResponse(responses[Math.floor(Math.random() * responses.length)]);
|
||||||
|
}, 1200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleProgressChange = (courseId, delta) => {
|
||||||
|
const updated = courses.map(c => {
|
||||||
|
if (c.id === courseId) {
|
||||||
|
const nextProgress = Math.max(0, Math.min(100, c.progress + delta));
|
||||||
|
return { ...c, progress: nextProgress };
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
setCourses(updated);
|
||||||
|
if (selectedCourse && selectedCourse.id === courseId) {
|
||||||
|
setSelectedCourse({ ...selectedCourse, progress: Math.max(0, Math.min(100, selectedCourse.progress + delta)) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="courses-view" className="space-y-6">
|
||||||
|
|
||||||
|
{/* Upper header statistics bar */}
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-white p-6 rounded-2xl border border-slate-100 shadow-sm">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-slate-800">Academic Courses</h2>
|
||||||
|
<p className="text-slate-500 text-xs mt-1">Syllabus progression & professor consultation panel</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-[10px] text-slate-400 uppercase font-bold block">Active Modules</span>
|
||||||
|
<span className="text-lg font-bold text-slate-800">{courses.length} Courses</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-8 w-px bg-slate-200"></div>
|
||||||
|
<div className="text-center">
|
||||||
|
<span className="text-[10px] text-slate-400 uppercase font-bold block">Credits Allocated</span>
|
||||||
|
<span className="text-lg font-bold text-slate-800">
|
||||||
|
{courses.reduce((sum, c) => sum + c.credits, 0)} Credits
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid of registered courses */}
|
||||||
|
<div id="course-cards-container" className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
|
||||||
|
{courses.map((course) => (
|
||||||
|
<div
|
||||||
|
key={course.id}
|
||||||
|
id={`course-card-${course.id}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCourse(course);
|
||||||
|
setProfessorResponse(null);
|
||||||
|
}}
|
||||||
|
className="group cursor-pointer bg-white rounded-2xl border border-slate-100 p-5 shadow-sm hover:shadow-md hover:border-slate-200 transition-all duration-300 relative overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Soft accent glow on hover */}
|
||||||
|
<div className={`absolute top-0 right-0 h-24 w-24 rounded-full bg-gradient-to-br ${themeColor.accent} opacity-0 group-hover:opacity-10 transition-opacity blur-xl`}></div>
|
||||||
|
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded ${themeColor.bg} ${themeColor.text}`}>
|
||||||
|
{course.code}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm font-bold text-slate-600 font-mono">
|
||||||
|
Grade: {course.grade}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className="font-bold text-slate-800 mt-3 group-hover:text-indigo-650 transition-colors line-clamp-1">
|
||||||
|
{course.name}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-2.5">
|
||||||
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||||
|
<User className="h-3.5 w-3.5" />
|
||||||
|
<span className="font-medium">{course.instructor}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||||
|
<MapPin className="h-3.5 w-3.5" />
|
||||||
|
<span>Room: {course.room}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||||
|
<Calendar className="h-3.5 w-3.5" />
|
||||||
|
<span>Schedule: {course.schedule}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Syllabus completion slider indicators */}
|
||||||
|
<div className="mt-5 space-y-2">
|
||||||
|
<div className="flex justify-between text-[11px] font-bold text-slate-500">
|
||||||
|
<span>Syllabus Completed</span>
|
||||||
|
<span>{course.progress}%</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full transition-all duration-500 ${themeColor.primary}`}
|
||||||
|
style={{ width: `${course.progress}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Course Consultation Overlay Dialog Drawer panel */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{selectedCourse && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-end p-0 md:p-4 bg-slate-900/60 backdrop-blur-xs" id="course-details-drawer">
|
||||||
|
{/* Backdrop Close Click area */}
|
||||||
|
<div className="absolute inset-0" onClick={() => setSelectedCourse(null)}></div>
|
||||||
|
|
||||||
|
<div className="relative h-full md:h-auto md:max-h-[92vh] max-w-lg w-full bg-white md:rounded-2xl shadow-2xl flex flex-col overflow-hidden border border-slate-100 z-10">
|
||||||
|
{/* Header block with solid title text */}
|
||||||
|
<div className="px-6 py-5 bg-slate-900 text-white flex justify-between items-center shrink-0">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<span className={`text-[10px] font-extrabold uppercase px-2 py-0.5 rounded ${themeColor.bg} ${themeColor.text}`}>
|
||||||
|
{selectedCourse.code}
|
||||||
|
</span>
|
||||||
|
<h3 className="font-bold text-lg">{selectedCourse.name}</h3>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="close-course-details-btn"
|
||||||
|
onClick={() => setSelectedCourse(null)}
|
||||||
|
className="p-1 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 font-bold"
|
||||||
|
>
|
||||||
|
X
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal body scroll panel */}
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 py-5 space-y-6">
|
||||||
|
{/* Course parameters and scores */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 bg-slate-50 p-4 rounded-xl">
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] text-slate-400 uppercase font-bold">Professor</span>
|
||||||
|
<p className="font-semibold text-slate-800 text-sm mt-0.5">{selectedCourse.instructor}</p>
|
||||||
|
<p className="text-xs text-slate-400 font-mono">{selectedCourse.instructorEmail}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] text-slate-400 uppercase font-bold">Location</span>
|
||||||
|
<p className="font-semibold text-slate-800 text-sm mt-0.5">{selectedCourse.room}</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<span className="text-[10px] text-slate-400 uppercase font-bold">Attendance</span>
|
||||||
|
<p className="font-bold text-emerald-600 text-sm mt-0.5">{selectedCourse.attendanceRate}% Present</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2">
|
||||||
|
<span className="text-[10px] text-slate-400 uppercase font-bold">Lec Schedule</span>
|
||||||
|
<p className="font-medium text-slate-700 text-xs mt-1">{selectedCourse.schedule}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive Syllabus Completion Increment Module */}
|
||||||
|
<div className="space-y-3.5 border-b border-slate-100 pb-5">
|
||||||
|
<h4 className="font-bold text-slate-800 text-sm flex items-center justify-between">
|
||||||
|
<span>Modify Syllabus Progress</span>
|
||||||
|
<span className="text-xs text-slate-50 px-2 py-0.5 bg-slate-100 rounded font-semibold text-slate-700">
|
||||||
|
{selectedCourse.progress}% Completed
|
||||||
|
</span>
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
id={`decrease-progress-${selectedCourse.id}`}
|
||||||
|
onClick={() => handleProgressChange(selectedCourse.id, -5)}
|
||||||
|
className="px-3.5 py-1.5 bg-slate-50 text-slate-600 hover:bg-slate-100 font-semibold rounded-lg text-xs"
|
||||||
|
>
|
||||||
|
- 5% Complete
|
||||||
|
</button>
|
||||||
|
<div className="flex-1 h-2 bg-slate-100 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-indigo-500 rounded-full transition-all"
|
||||||
|
style={{ width: `${selectedCourse.progress}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id={`increase-progress-${selectedCourse.id}`}
|
||||||
|
onClick={() => handleProgressChange(selectedCourse.id, 5)}
|
||||||
|
className="px-3.5 py-1.5 bg-indigo-50 text-indigo-600 hover:bg-indigo-100 font-semibold rounded-lg text-xs"
|
||||||
|
>
|
||||||
|
+ 5% Complete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email Consultation Simulation */}
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-1.5 text-slate-800 font-bold text-xs uppercase tracking-wider">
|
||||||
|
<Sparkles className="h-4 w-4 text-indigo-500 shrink-0" />
|
||||||
|
<span>Simulate Professor Consultation email</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 leading-relaxed">
|
||||||
|
You can draft and send an email message to Professor <strong className="text-slate-700">{selectedCourse.instructor}</strong> regarding leave updates, extensions, or grade double-checks:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form onSubmit={handleSendEmail} className="space-y-3 bg-indigo-55/40 p-4 rounded-xl border border-indigo-50">
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] font-bold text-slate-400 uppercase mb-1">Email Subject</label>
|
||||||
|
<input
|
||||||
|
id="email-subject-input"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="Request for Quiz extension, Medical leave..."
|
||||||
|
value={emailSubject}
|
||||||
|
onChange={(e) => setEmailSubject(e.target.value)}
|
||||||
|
className="w-full text-slate-800 text-xs px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:border-indigo-400"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-[10px] font-bold text-slate-400 uppercase mb-1">Email Content Body</label>
|
||||||
|
<textarea
|
||||||
|
id="email-body-input"
|
||||||
|
rows={3}
|
||||||
|
required
|
||||||
|
placeholder="Respected Professor, I would like to request..."
|
||||||
|
value={emailBody}
|
||||||
|
onChange={(e) => setEmailBody(e.target.value)}
|
||||||
|
className="w-full text-slate-800 text-xs px-3 py-2 rounded-lg border border-slate-200 bg-white focus:outline-none focus:border-indigo-400 resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="submit-email-btn"
|
||||||
|
type="submit"
|
||||||
|
disabled={isSending}
|
||||||
|
className={`w-full py-2 ${themeColor.primary} ${themeColor.hover} text-white font-semibold text-xs rounded-xl flex items-center justify-center gap-2 shadow-sm transition-all`}
|
||||||
|
>
|
||||||
|
{isSending ? (
|
||||||
|
<span>Sending Email...</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send className="h-3.5 w-3.5" />
|
||||||
|
<span>Send Simulated Email</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* AI Response Preview */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{professorResponse && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="bg-emerald-50 rounded-xl p-4 border border-emerald-100 space-y-2.5"
|
||||||
|
>
|
||||||
|
<span className="text-[10px] font-bold text-emerald-600 block uppercase tracking-wider flex items-center gap-1">
|
||||||
|
<Inbox className="h-3 w-3" />
|
||||||
|
New Response Received
|
||||||
|
</span>
|
||||||
|
<div className="text-xs text-rose-950 font-medium whitespace-pre-wrap leading-relaxed">
|
||||||
|
{professorResponse}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email history log */}
|
||||||
|
<div className="space-y-3 pt-4 border-t border-slate-100">
|
||||||
|
<h4 className="font-bold text-slate-700 text-xs uppercase tracking-wider">Mail Box Sent Logs</h4>
|
||||||
|
{sentMails.length === 0 ? (
|
||||||
|
<p className="text-xs text-slate-400 italic">No sent logs for this session yet.</p>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2 max-h-48 overflow-y-auto pr-1">
|
||||||
|
{sentMails.map((mail) => (
|
||||||
|
<div key={mail.id} className="p-3 bg-slate-50 rounded-lg text-xs space-y-1">
|
||||||
|
<div className="flex justify-between items-center text-[10px] text-slate-400">
|
||||||
|
<span>To: {mail.to}</span>
|
||||||
|
<span>{mail.date}</span>
|
||||||
|
</div>
|
||||||
|
<p className="font-bold text-slate-700">{mail.subject}</p>
|
||||||
|
<p className="text-slate-500 line-clamp-2">{mail.body}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,678 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { motion } from 'motion/react';
|
||||||
|
import {
|
||||||
|
Award,
|
||||||
|
Clock,
|
||||||
|
Calendar,
|
||||||
|
AlertTriangle,
|
||||||
|
Flame,
|
||||||
|
CheckCircle2,
|
||||||
|
BookOpen,
|
||||||
|
ArrowUpRight,
|
||||||
|
TrendingUp,
|
||||||
|
CirclePlay,
|
||||||
|
CalendarCheck,
|
||||||
|
ChevronRight,
|
||||||
|
CheckCircle,
|
||||||
|
Clock3
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export default function DashboardHome({
|
||||||
|
courses,
|
||||||
|
assignments,
|
||||||
|
announcements,
|
||||||
|
attendance,
|
||||||
|
onToggleAssignmentStatus,
|
||||||
|
setActiveTab,
|
||||||
|
themeColor
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const [activeAnnCategory, setActiveAnnCategory] = useState('All');
|
||||||
|
|
||||||
|
// Custom states for English steps and Git logs simulation
|
||||||
|
const [completedSteps, setCompletedSteps] = useState([1]);
|
||||||
|
const [gitLogs, setGitLogs] = useState([
|
||||||
|
{ id: 'git-3', text: 'Pushed 4 commits to main branch', type: 'commit', time: '12 mins ago' },
|
||||||
|
{ id: 'git-2', text: 'Merged pull request #12 (Feature/attendance-tracker)', type: 'merge', time: '2 hours ago' },
|
||||||
|
{ id: 'git-1', text: 'Connected repo with origin Gitea remote URL', type: 'setup', time: 'Yesterday' }
|
||||||
|
]);
|
||||||
|
const [newCommit, setNewCommit] = useState('');
|
||||||
|
|
||||||
|
const toggleStep = (stepNum) => {
|
||||||
|
if (completedSteps.includes(stepNum)) {
|
||||||
|
setCompletedSteps(completedSteps.filter(s => s !== stepNum));
|
||||||
|
} else {
|
||||||
|
setCompletedSteps([...completedSteps, stepNum]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePushCommit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newCommit.trim()) return;
|
||||||
|
setGitLogs([
|
||||||
|
{
|
||||||
|
id: `git-${Date.now()}`,
|
||||||
|
text: newCommit,
|
||||||
|
type: 'commit',
|
||||||
|
time: 'Just now'
|
||||||
|
},
|
||||||
|
...gitLogs
|
||||||
|
]);
|
||||||
|
setNewCommit('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate statistics
|
||||||
|
const gpa = 3.84; // Matches the exact design value 3.84
|
||||||
|
|
||||||
|
const completedAssignmentsCount = assignments.filter(a => a.status === 'Completed').length;
|
||||||
|
const totalAssignmentsCount = assignments.length;
|
||||||
|
|
||||||
|
const avgAttendance = 92; // Matches the exact design value 92%
|
||||||
|
const totalCredits = courses.reduce((sum, c) => sum + c.credits, 0);
|
||||||
|
|
||||||
|
// Filter urgent assignments
|
||||||
|
const pendingAssignments = assignments.filter(a => a.status !== 'Completed');
|
||||||
|
const urgentAssignments = [...pendingAssignments]
|
||||||
|
.sort((a,b) => new Date(a.dueDate).getTime() - new Date(b.dueDate).getTime())
|
||||||
|
.slice(0, 3);
|
||||||
|
|
||||||
|
// Stats Card mock arrays
|
||||||
|
const statCards = [
|
||||||
|
{
|
||||||
|
id: 'stat-gpa',
|
||||||
|
title: 'Current GPA',
|
||||||
|
value: gpa.toFixed(2),
|
||||||
|
desc: 'Top 5% in Department',
|
||||||
|
icon: Award,
|
||||||
|
color: 'from-indigo-500 to-indigo-600',
|
||||||
|
textColor: 'text-indigo-600',
|
||||||
|
bgLight: 'bg-indigo-50',
|
||||||
|
barPercent: '90%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stat-tasks',
|
||||||
|
title: 'Attendance',
|
||||||
|
value: `${avgAttendance}%`,
|
||||||
|
desc: '+2% since last month',
|
||||||
|
icon: CalendarCheck,
|
||||||
|
color: 'from-slate-700 to-slate-805',
|
||||||
|
textColor: 'text-slate-800',
|
||||||
|
bgLight: 'bg-slate-100',
|
||||||
|
barPercent: '92%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stat-attendance',
|
||||||
|
title: 'Pending Tasks',
|
||||||
|
value: '04',
|
||||||
|
desc: 'Due in next 48 hours',
|
||||||
|
icon: CheckCircle2,
|
||||||
|
color: 'from-rose-500 to-pink-600',
|
||||||
|
textColor: 'text-rose-500',
|
||||||
|
bgLight: 'bg-rose-50',
|
||||||
|
barPercent: '40%'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'stat-credits',
|
||||||
|
title: 'Registered Credits',
|
||||||
|
value: `${totalCredits} Hrs`,
|
||||||
|
desc: 'Active Semester Schedule',
|
||||||
|
icon: BookOpen,
|
||||||
|
color: 'from-violet-500 to-indigo-500',
|
||||||
|
textColor: 'text-indigo-600',
|
||||||
|
bgLight: 'bg-violet-50',
|
||||||
|
barPercent: '100%'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Daily Schedule for Monday, June 1st, 2026
|
||||||
|
const todaysSchedule = [
|
||||||
|
{ time: '09:00 AM - 10:30 AM', code: 'CS-401', name: 'Advanced Algorithms', room: 'Lab-3C, Tech Block', color: 'border-l-indigo-505 bg-indigo-50/20' },
|
||||||
|
{ time: '12:00 PM - 01:30 PM', code: 'CS-403', name: 'Full-Stack Web Development', room: 'Lab-1A, Tech Block', color: 'border-l-emerald-500 bg-emerald-50/20' },
|
||||||
|
{ time: '03:00 PM - 04:30 PM', code: 'CS-402', name: 'Research Colloquium (Guest)', room: 'Seminar Hall B', color: 'border-l-violet-550 bg-violet-50/20' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// SVG Chart mock study stats
|
||||||
|
const weeklyStudyHours = [
|
||||||
|
{ day: 'Mon', hours: 4.5 },
|
||||||
|
{ day: 'Tue', hours: 5.2 },
|
||||||
|
{ day: 'Wed', hours: 3.0 },
|
||||||
|
{ day: 'Thu', hours: 6.8 },
|
||||||
|
{ day: 'Fri', hours: 4.0 },
|
||||||
|
{ day: 'Sat', hours: 8.5 },
|
||||||
|
{ day: 'Sun', hours: 5.0 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const maxHours = Math.max(...weeklyStudyHours.map(d => d.hours));
|
||||||
|
|
||||||
|
const filteredAnnouncements = activeAnnCategory === 'All'
|
||||||
|
? announcements
|
||||||
|
: announcements.filter(a => a.category === activeAnnCategory);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="dashboard-home-view" className="space-y-8">
|
||||||
|
|
||||||
|
{/* Header Greeting Banner with a high-contrast theme */}
|
||||||
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 bg-slate-905 text-white p-6 md:p-8 rounded-xl shadow-md border border-slate-800 relative overflow-hidden">
|
||||||
|
{/* Glow Effects */}
|
||||||
|
<div className="absolute -top-24 -right-24 h-48 w-48 bg-indigo-500/10 rounded-full blur-3xl text-white"></div>
|
||||||
|
<div className="absolute -bottom-12 -left-12 h-36 w-36 bg-indigo-505 bg-indigo-500/5 rounded-full blur-2xl text-white"></div>
|
||||||
|
|
||||||
|
<div className="relative z-10 space-y-2">
|
||||||
|
<div className="flex items-center gap-2 text-indigo-400 text-xs font-semibold uppercase tracking-wider">
|
||||||
|
<span className="flex h-2 w-2 relative">
|
||||||
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
||||||
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
||||||
|
</span>
|
||||||
|
Development Environment Live
|
||||||
|
</div>
|
||||||
|
<h2 className="text-2xl md:text-3xl font-bold tracking-tight text-white">
|
||||||
|
Welcome back, Hamza!
|
||||||
|
</h2>
|
||||||
|
<p className="text-slate-300 text-sm md:text-base max-w-2xl leading-relaxed animate-fade-in">
|
||||||
|
Only <strong className="text-amber-400">17 days</strong> are left before your exams start. Today you have <strong className="text-emerald-400">2 live classes</strong> scheduled. Keep up the hustle!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10 flex gap-3 self-start md:self-center">
|
||||||
|
<button
|
||||||
|
id="view-assignments-banner-btn"
|
||||||
|
onClick={() => setActiveTab('assignments')}
|
||||||
|
className="cursor-pointer px-4 py-2.5 bg-white text-slate-900 hover:bg-slate-50 transition-all rounded-lg font-semibold text-xs flex items-center gap-2 shadow-sm border border-slate-200"
|
||||||
|
>
|
||||||
|
Manage Tasks
|
||||||
|
<ArrowUpRight className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="streak-indicator-btn"
|
||||||
|
className="px-4 py-2.5 bg-slate-800 hover:bg-slate-800/80 rounded-lg font-semibold text-xs text-amber-400 border border-slate-700/80 flex items-center gap-2 transition"
|
||||||
|
>
|
||||||
|
<Flame className="h-4 w-4 fill-amber-500 shrink-0 text-amber-500" />
|
||||||
|
<span>8 Day Streak!</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid of Summary Stats - Professional Polish matching the design cards exactly */}
|
||||||
|
<div id="stats-grid" className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||||
|
{statCards.map((card, i) => {
|
||||||
|
const Icon = card.icon;
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key={card.id}
|
||||||
|
id={`stat-card-${card.id}`}
|
||||||
|
initial={{ opacity: 0, y: 15 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: i * 0.05 }}
|
||||||
|
className="bg-white rounded-xl p-5 border border-slate-200 shadow-xs flex flex-col justify-between h-36 relative overflow-hidden hover:border-indigo-200 transition-all group"
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider">{card.title}</p>
|
||||||
|
<p className={`text-3xl font-extrabold tracking-tight mt-1.5 ${
|
||||||
|
card.id === 'stat-gpa' ? 'text-indigo-600' :
|
||||||
|
card.id === 'stat-attendance' ? 'text-rose-505 text-rose-500' : 'text-slate-800'
|
||||||
|
}`}>
|
||||||
|
{card.value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-2 bg-slate-50 border border-slate-100 rounded-lg text-slate-500 group-hover:bg-indigo-50 group-hover:text-indigo-600 transition-colors">
|
||||||
|
<Icon className="h-4.5 w-4.5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full mt-2">
|
||||||
|
<div className="flex justify-between items-center mb-1">
|
||||||
|
<span className="text-[10.5px] text-slate-500 font-medium">{card.desc}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-slate-100 h-1.5 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full rounded-full transition-all duration-1000 ${
|
||||||
|
card.id === 'stat-gpa' ? 'bg-indigo-650 bg-indigo-600' :
|
||||||
|
card.id === 'stat-attendance' ? 'bg-rose-500' :
|
||||||
|
'bg-slate-700'
|
||||||
|
}`}
|
||||||
|
style={{ width: card.barPercent }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dynamic Analytics & Today's Schedule layout split */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
|
||||||
|
{/* Study Hours SVG chart (Left 2-Columns) */}
|
||||||
|
<div id="analytics-chart-container" className="lg:col-span-2 bg-white rounded-xl p-6 border border-slate-200 shadow-xs flex flex-col justify-between space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-slate-800 text-lg">Weekly Study Tracker</h3>
|
||||||
|
<p className="text-xs text-slate-400">Hours spent preparing courses and tasks</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 px-3 py-1 bg-indigo-50 text-indigo-600 rounded-full text-xs font-semibold border border-indigo-100 shadow-xxs">
|
||||||
|
<TrendingUp className="h-3.5 w-3.5" />
|
||||||
|
<span>+18% this week</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom SVG Chart */}
|
||||||
|
<div className="relative pt-4 w-full h-48 flex items-end justify-between px-2">
|
||||||
|
{/* Horizontal guide lines */}
|
||||||
|
<div className="absolute inset-x-0 top-0 bottom-8 flex flex-col justify-between pointer-events-none border-b border-dashed border-slate-150">
|
||||||
|
<div className="w-full border-t border-slate-100/70"></div>
|
||||||
|
<div className="w-full border-t border-slate-100/70"></div>
|
||||||
|
<div className="w-full border-t border-slate-100/70"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{weeklyStudyHours.map((data, index) => {
|
||||||
|
const heightPct = (data.hours / maxHours) * 80;
|
||||||
|
return (
|
||||||
|
<div key={data.day} className="flex flex-col items-center group relative z-10 w-full">
|
||||||
|
<div className="absolute bottom-full mb-2 opacity-0 group-hover:opacity-100 bg-slate-800 text-white text-[10px] py-1 px-2.5 rounded-md transition-opacity pointer-events-none shadow-md font-mono z-50 whitespace-nowrap">
|
||||||
|
{data.hours} hrs
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{ height: 0 }}
|
||||||
|
animate={{ height: `${heightPct}%` }}
|
||||||
|
transition={{ delay: index * 0.05, type: 'spring', damping: 15 }}
|
||||||
|
className="w-8 sm:w-10 rounded-t bg-indigo-600 opacity-90 group-hover:opacity-100 group-hover:shadow-xs transition-all"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-x-0 top-0 h-1 bg-white/20 rounded-t"></div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<span className="text-xs text-slate-400 mt-2.5 font-medium">{data.day}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2 border-t border-slate-100 pt-4 text-center">
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-slate-400 uppercase font-bold tracking-wider">Total hours</p>
|
||||||
|
<p className="text-lg font-bold text-slate-700">36.3 Hrs</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-slate-405 text-slate-400 uppercase font-bold tracking-wider">Daily Average</p>
|
||||||
|
<p className="text-lg font-bold text-slate-700">5.2 Hrs</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-slate-400 uppercase font-bold tracking-wider">Peak Day</p>
|
||||||
|
<p className="text-lg font-bold text-indigo-600 font-extrabold text-indigo-600">Sat (8.5h)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Classes Schedule Card */}
|
||||||
|
<div id="todays-classes-timeline" className="bg-white rounded-xl p-6 border border-slate-200 shadow-xs flex flex-col justify-between">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-slate-800 text-base">Classes Today</h3>
|
||||||
|
<p className="text-xs text-slate-400">Monday, June 1, 2026</p>
|
||||||
|
</div>
|
||||||
|
<Clock3 className="h-5 w-5 text-slate-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 flex-1">
|
||||||
|
{todaysSchedule.map((sched, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`relative p-3.5 rounded-lg border-l-4 ${sched.color} border border-slate-100 shadow-xxs transition-all hover:translate-x-0.5`}
|
||||||
|
>
|
||||||
|
<div className="flex justify-between items-start gap-1">
|
||||||
|
<span className="text-[10px] font-bold text-slate-500 uppercase">{sched.code}</span>
|
||||||
|
<span className="text-[10px] text-slate-400 font-medium flex items-center gap-1">
|
||||||
|
<Clock className="h-3 w-3 inline" />
|
||||||
|
{sched.time.split(' - ')[0]}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h4 className="font-semibold text-sm text-slate-800 mt-1 line-clamp-1">{sched.name}</h4>
|
||||||
|
<p className="text-xs text-slate-505 text-slate-500 mt-1 flex items-center gap-1.5 font-mono">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-indigo-500"></span>
|
||||||
|
{sched.room}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="view-full-attendance-btn"
|
||||||
|
onClick={() => setActiveTab('attendance')}
|
||||||
|
className="mt-4 w-full py-2 bg-slate-50 hover:bg-slate-100 text-slate-600 text-xs font-semibold rounded-lg border border-slate-200 transition-colors flex items-center justify-center gap-1.5"
|
||||||
|
>
|
||||||
|
Check Complete Attendance
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Core Gitea Integration & Workspace Grid Panel */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6" id="gitea-integration-workspace">
|
||||||
|
|
||||||
|
{/* Left Span: Development Steps - 8 Cols */}
|
||||||
|
<div className="col-span-1 lg:col-span-8 bg-white rounded-xl p-6 border border-slate-200 shadow-xs flex flex-col h-[480px]">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-bold text-slate-805 text-slate-800">Development Steps</h3>
|
||||||
|
<p className="text-xs text-slate-400">Step-by-step developer guidelines mapped for students</p>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-mono text-indigo-650 bg-indigo-50 border border-indigo-100 px-2 py-0.5 rounded-md text-indigo-600">
|
||||||
|
{completedSteps.length}/3 Read
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4 overflow-y-auto flex-1 pr-1 max-h-[380px] custom-scrollbar">
|
||||||
|
|
||||||
|
{/* Step 1 */}
|
||||||
|
<div
|
||||||
|
onClick={() => toggleStep(1)}
|
||||||
|
className={`p-4 rounded-xl border border-slate-150 transition-all cursor-pointer border-l-4 ${
|
||||||
|
completedSteps.includes(1)
|
||||||
|
? 'border-l-indigo-600 bg-slate-50/50'
|
||||||
|
: 'border-l-slate-300 bg-white hover:bg-slate-50/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className={`text-sm font-bold ${completedSteps.includes(1) ? 'text-indigo-700' : 'text-slate-700'}`}>
|
||||||
|
Step 1: Setup React & Tailwind
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={completedSteps.includes(1)}
|
||||||
|
readOnly
|
||||||
|
className="h-4 w-4 text-indigo-600 border-slate-300 rounded focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-650 text-slate-600 mt-1.5 italic leading-relaxed">
|
||||||
|
Open your terminal and run the command: <code className="bg-white/80 border border-slate-205 border-slate-200 px-2 py-0.5 rounded font-mono text-xs font-semibold select-all text-slate-800">npx create-react-app my-dashboard</code>. After that, follow the official docs to install Tailwind CSS so that the styles work properly.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 2 */}
|
||||||
|
<div
|
||||||
|
onClick={() => toggleStep(2)}
|
||||||
|
className={`p-4 rounded-xl border border-slate-150 transition-all cursor-pointer border-l-4 ${
|
||||||
|
completedSteps.includes(2)
|
||||||
|
? 'border-l-indigo-600 bg-slate-50/50'
|
||||||
|
: 'border-l-slate-300 bg-white hover:bg-slate-50/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className={`text-sm font-bold ${completedSteps.includes(2) ? 'text-indigo-700' : 'text-slate-700'}`}>
|
||||||
|
Step 2: Component Structure
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={completedSteps.includes(2)}
|
||||||
|
readOnly
|
||||||
|
className="h-4 w-4 text-indigo-600 border-slate-300 rounded focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-600 mt-1.5 italic leading-relaxed">
|
||||||
|
You will need to create separate components for the Sidebar, Navbar, and MainContent. Reusable components keep the code clean and make the layout polished.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step 3 */}
|
||||||
|
<div
|
||||||
|
onClick={() => toggleStep(3)}
|
||||||
|
className={`p-4 rounded-xl border border-slate-150 transition-all cursor-pointer border-l-4 ${
|
||||||
|
completedSteps.includes(3)
|
||||||
|
? 'border-l-indigo-600 bg-slate-50/50'
|
||||||
|
: 'border-l-slate-300 bg-white hover:bg-slate-50/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className={`text-sm font-bold ${completedSteps.includes(3) ? 'text-indigo-700' : 'text-slate-700'}`}>
|
||||||
|
Step 3: Gitea Connection
|
||||||
|
</p>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={completedSteps.includes(3)}
|
||||||
|
readOnly
|
||||||
|
className="h-4 w-4 text-indigo-600 border-slate-300 rounded focus:ring-indigo-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-slate-600 mt-1.5 italic leading-relaxed">
|
||||||
|
First, create a new repository on Gitea. Then run <code className="bg-white/80 border border-slate-200 px-1 py-0.5 rounded font-mono text-xs select-all">git remote add origin [url]</code> and use the <code className="bg-white/80 border border-slate-200 px-1 py-0.5 rounded font-mono text-xs select-all">git push -u origin main</code> command to push your code.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Span: Upcoming Deadlines & Git Activity - 4 Cols */}
|
||||||
|
<div className="col-span-1 lg:col-span-4 space-y-6">
|
||||||
|
|
||||||
|
{/* Upcoming Deadlines */}
|
||||||
|
<div className="bg-white rounded-xl p-5 border border-slate-205 shadow-xs h-[245px] flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-bold text-slate-800 mb-3 border-b border-slate-100 pb-2">
|
||||||
|
Upcoming Deadlines
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="w-8 h-8 rounded bg-indigo-50 flex items-center justify-center text-indigo-700 font-bold text-xs border border-indigo-100 shrink-0">
|
||||||
|
CS
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold text-slate-800">Database Final Project</p>
|
||||||
|
<p className="text-[10px] text-slate-400">Deadline: Tomorrow Night 12:00 AM</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="w-8 h-8 rounded bg-emerald-50 flex items-center justify-center text-emerald-700 font-bold text-xs border border-emerald-100 shrink-0">
|
||||||
|
AI
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold text-slate-800">Neural Network Quiz</p>
|
||||||
|
<p className="text-[10px] text-slate-400">Date: 15th Oct, 2024</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-3">
|
||||||
|
<div className="w-8 h-8 rounded bg-amber-50 flex items-center justify-center text-amber-700 font-bold text-xs border border-amber-100 shrink-0">
|
||||||
|
UX
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-bold text-slate-800">Dashboard Prototyping</p>
|
||||||
|
<p className="text-[10px] text-slate-400">Due: 18th Oct, 2024</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('assignments')}
|
||||||
|
className="text-[11px] font-semibold text-indigo-600 hover:underline block text-center"
|
||||||
|
>
|
||||||
|
See Worksheets →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Git Activity terminal style block */}
|
||||||
|
<div className="bg-slate-900 text-white rounded-xl p-5 border border-slate-800 shadow-md h-[210px] flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-bold text-white mb-2.5 flex items-center justify-between">
|
||||||
|
<span>Git Activity Log</span>
|
||||||
|
<span className="w-2 h-2 rounded-full bg-green-400 animate-pulse"></span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-2 overflow-y-auto max-h-[105px] pr-1 custom-scrollbar">
|
||||||
|
{gitLogs.map((log) => (
|
||||||
|
<div key={log.id} className="flex items-start space-x-2.5 p-1.5 bg-slate-850 rounded border border-slate-802">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-green-400 mt-1.5 shrink-0"></div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-[11px] text-slate-100 font-medium select-all">
|
||||||
|
{log.text}
|
||||||
|
</p>
|
||||||
|
<span className="text-[9px] text-slate-404 text-slate-400 font-mono italic block">{log.time}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive simulating input box */}
|
||||||
|
<form onSubmit={handlePushCommit} className="flex items-center gap-2 mt-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Simulate commit message..."
|
||||||
|
value={newCommit}
|
||||||
|
onChange={(e) => setNewCommit(e.target.value)}
|
||||||
|
className="bg-slate-800 text-white border-none rounded text-[10.5px] px-2.5 py-1.5 flex-1 focus:ring-1 focus:ring-indigo-400 outline-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-indigo-650 hover:bg-indigo-700 bg-indigo-600 text-white font-bold text-[10px] px-2.5 py-1.5 rounded transition shadow-sm uppercase shrink-0"
|
||||||
|
>
|
||||||
|
Push
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid of Announcements & Urgent Assignments */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
|
||||||
|
{/* Active Campus Announcements */}
|
||||||
|
<div id="campus-announcements-card" className="bg-white rounded-xl p-6 border border-slate-200 shadow-xs flex flex-col">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-5">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-slate-800 text-lg">Announcements</h3>
|
||||||
|
<p className="text-xs text-slate-400">Important briefs and exam rosters</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Filter buttons */}
|
||||||
|
<div className="flex gap-1 bg-slate-101 border border-slate-200 p-1 rounded-lg self-start shadow-xxs">
|
||||||
|
{['All', 'Exam', 'Academic', 'Event'].map((cat) => (
|
||||||
|
<button
|
||||||
|
key={cat}
|
||||||
|
id={`ann-filter-btn-${cat}`}
|
||||||
|
onClick={() => setActiveAnnCategory(cat)}
|
||||||
|
className={`text-[10px] px-2.5 py-1 rounded font-semibold transition-all cursor-pointer ${
|
||||||
|
activeAnnCategory === cat
|
||||||
|
? 'bg-white text-slate-800 shadow-xxs border border-slate-200/50'
|
||||||
|
: 'text-slate-505 text-slate-500 hover:text-slate-850'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 flex-1 overflow-y-auto max-h-80 pr-1">
|
||||||
|
{filteredAnnouncements.map((ann) => (
|
||||||
|
<div
|
||||||
|
key={ann.id}
|
||||||
|
className="p-4 rounded-lg bg-slate-50/30 border border-slate-150 hover:bg-slate-50/40 transition-colors space-y-2 hover:border-slate-300"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between flex-wrap gap-1">
|
||||||
|
<span className={`text-[10px] font-extrabold px-2 py-0.5 rounded-full border ${
|
||||||
|
ann.category === 'Exam' ? 'bg-rose-50 text-rose-600 border-rose-100' :
|
||||||
|
ann.category === 'Event' ? 'bg-amber-50 text-amber-600 border-amber-100' :
|
||||||
|
ann.category === 'Academic' ? 'bg-blue-50 text-blue-600 border-blue-100' :
|
||||||
|
'bg-slate-100 text-slate-600 border-slate-200'
|
||||||
|
}`}>
|
||||||
|
{ann.category}
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] text-slate-400 font-semibold font-mono">{ann.date}</span>
|
||||||
|
</div>
|
||||||
|
<h4 className="font-bold text-sm text-slate-800">{ann.title}</h4>
|
||||||
|
<p className="text-xs text-slate-500 leading-relaxed line-clamp-2">{ann.content}</p>
|
||||||
|
<div className="flex justify-between items-center pt-2 border-t border-slate-100">
|
||||||
|
<span className="text-[10px] text-slate-400">By: {ann.sender}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{filteredAnnouncements.length === 0 && (
|
||||||
|
<p className="text-center font-medium text-slate-400 text-sm py-8">There are no announcements in this category.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Urgent Assignments Tracker */}
|
||||||
|
<div id="urgent-assignments-card" className="bg-white rounded-xl p-6 border border-slate-200 shadow-xs flex flex-col">
|
||||||
|
<div className="flex items-center justify-between mb-5">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-slate-850 text-lg">Urgent Submissions</h3>
|
||||||
|
<p className="text-xs text-slate-400">Assignments needing immediate attention</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="view-all-assignments-link"
|
||||||
|
onClick={() => setActiveTab('assignments')}
|
||||||
|
className={`text-xs font-semibold ${themeColor.text} hover:underline flex items-center gap-1`}
|
||||||
|
>
|
||||||
|
See All Assignments
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 flex-1">
|
||||||
|
{urgentAssignments.map((ass) => (
|
||||||
|
<div
|
||||||
|
key={ass.id}
|
||||||
|
className="p-4 rounded-lg bg-slate-50/20 border border-slate-150 flex items-center justify-between hover:border-slate-200 transition-all shadow-xxs hover:shadow-xs"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3.5 pr-2 truncate">
|
||||||
|
<button
|
||||||
|
id={`quick-complete-btn-${ass.id}`}
|
||||||
|
onClick={() => onToggleAssignmentStatus(ass.id)}
|
||||||
|
className="mt-0.5 h-4.5 w-4.5 rounded border border-slate-300 flex items-center justify-center cursor-pointer hover:border-indigo-500 hover:bg-indigo-50 text-transparent hover:text-indigo-600 transition-colors"
|
||||||
|
>
|
||||||
|
<CheckCircle className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="truncate">
|
||||||
|
<h4 className="font-semibold text-sm text-slate-800 truncate" title={ass.title}>
|
||||||
|
{ass.title}
|
||||||
|
</h4>
|
||||||
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<span className="text-[10px] font-semibold text-slate-500 uppercase">{ass.courseName}</span>
|
||||||
|
<span className="h-1 w-1 bg-slate-300 rounded-full"></span>
|
||||||
|
<span className={`text-[9px] font-bold uppercase rounded px-1.5 py-0.2 border ${
|
||||||
|
ass.priority === 'High' ? 'bg-rose-50 text-rose-600 border-rose-100' :
|
||||||
|
ass.priority === 'Medium' ? 'bg-amber-50 text-amber-600 border-amber-100' :
|
||||||
|
'bg-slate-50 text-slate-600 border-slate-200'
|
||||||
|
}`}>
|
||||||
|
{ass.priority} priority
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-right shrink-0">
|
||||||
|
<p className="text-xs font-bold text-slate-700 font-mono">
|
||||||
|
{new Date(ass.dueDate).toLocaleDateString('en-US', { day: 'numeric', month: 'short' })}
|
||||||
|
</p>
|
||||||
|
<span className="text-[10px] text-slate-400 font-medium font-mono">Due date</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{urgentAssignments.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-10 text-center space-y-2">
|
||||||
|
<div className="bg-emerald-50 p-3 rounded-full text-emerald-500">
|
||||||
|
<CheckCircle className="h-8 w-8" />
|
||||||
|
</div>
|
||||||
|
<h4 className="font-bold text-slate-700 text-sm">Great job! No urgent tasks.</h4>
|
||||||
|
<p className="text-xs text-slate-400 max-w-xs">All urgent assignments have been successfully completed & logged.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { motion } from 'motion/react';
|
||||||
|
import {
|
||||||
|
GraduationCap,
|
||||||
|
TrendingUp,
|
||||||
|
Plus,
|
||||||
|
Trash2,
|
||||||
|
Calculator,
|
||||||
|
Sparkles,
|
||||||
|
BookMarked
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const GRADE_POINTS = {
|
||||||
|
'A+': 4.0, 'A': 4.0, 'A-': 3.7, 'B+': 3.3, 'B': 3.0, 'B-': 2.7, 'C+': 2.3, 'C': 2.0, 'C-': 1.7, 'D': 1.0, 'F': 0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function GradeCalculator({ courses, themeColor }) {
|
||||||
|
|
||||||
|
// Custom courses estimating list state
|
||||||
|
const [estimateCourses, setEstimateCourses] = useState([
|
||||||
|
{ id: '1', name: 'Advanced Algorithms', credits: 4, expectedGrade: 'A-' },
|
||||||
|
{ id: '2', name: 'Database Systems & SQL', credits: 4, expectedGrade: 'A' },
|
||||||
|
{ id: '3', name: 'Full-Stack Web Development', credits: 4, expectedGrade: 'A+' },
|
||||||
|
{ id: '4', name: 'Human-Computer Interaction', credits: 3, expectedGrade: 'B+' },
|
||||||
|
{ id: '5', name: 'Technical Writing & Ethics', credits: 2, expectedGrade: 'B' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [newEstName, setNewEstName] = useState('');
|
||||||
|
const [newEstCredits, setNewEstCredits] = useState(3);
|
||||||
|
const [newEstGrade, setNewEstGrade] = useState('A');
|
||||||
|
|
||||||
|
// Semester CGPA history values
|
||||||
|
const gpaMilestones = [
|
||||||
|
{ sem: 'Sem 1', gpa: 3.52 },
|
||||||
|
{ sem: 'Sem 2', gpa: 3.65 },
|
||||||
|
{ sem: 'Sem 3', gpa: 3.74 },
|
||||||
|
{ sem: 'Sem 4 (Cur)', gpa: 3.82 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleAddEstimateCourse = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!newEstName.trim()) return;
|
||||||
|
|
||||||
|
const newCourse = {
|
||||||
|
id: Math.random().toString(),
|
||||||
|
name: newEstName,
|
||||||
|
credits: Number(newEstCredits),
|
||||||
|
expectedGrade: newEstGrade
|
||||||
|
};
|
||||||
|
|
||||||
|
setEstimateCourses([...estimateCourses, newCourse]);
|
||||||
|
setNewEstName('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteEstimateCourse = (id) => {
|
||||||
|
setEstimateCourses(estimateCourses.filter(c => c.id !== id));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compute SGPA dynamically
|
||||||
|
const expectedSGPA = useMemo(() => {
|
||||||
|
let totalPoints = 0;
|
||||||
|
let totalCredits = 0;
|
||||||
|
|
||||||
|
estimateCourses.forEach(c => {
|
||||||
|
const gp = GRADE_POINTS[c.expectedGrade] ?? 4.0;
|
||||||
|
totalPoints += gp * c.credits;
|
||||||
|
totalCredits += c.credits;
|
||||||
|
});
|
||||||
|
|
||||||
|
return totalCredits > 0 ? (totalPoints / totalCredits) : 0;
|
||||||
|
}, [estimateCourses]);
|
||||||
|
|
||||||
|
const handleGradeChange = (id, grade) => {
|
||||||
|
const updated = estimateCourses.map(c => {
|
||||||
|
if (c.id === id) {
|
||||||
|
return { ...c, expectedGrade: grade };
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
});
|
||||||
|
setEstimateCourses(updated);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="grades-gpa-view" className="space-y-6">
|
||||||
|
|
||||||
|
{/* Grades Summary info */}
|
||||||
|
<div className="bg-white p-6 rounded-2xl border border-slate-100 shadow-sm">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 border-b border-slate-50 pb-5">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xl font-bold text-slate-800">Grades Ledger & SGPA Estimator</h2>
|
||||||
|
<p className="text-xs text-slate-500 mt-1">Review past scores and forecast your upcoming term results</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5 px-3 py-1 bg-amber-50 text-amber-700 rounded-full font-bold text-xs uppercase">
|
||||||
|
<Sparkles className="h-3.5 w-3.5" />
|
||||||
|
<span>Honors Roll Class</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Semester Line progression (D3 equivalent visual SVG) */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<h3 className="text-xs font-bold text-slate-400 uppercase tracking-widest mb-4">CGPA Progression Trend</h3>
|
||||||
|
<div className="w-full bg-slate-50 p-4 rounded-xl border border-slate-100 flex flex-col md:flex-row items-center gap-6 justify-between">
|
||||||
|
|
||||||
|
{/* SVG linear line */}
|
||||||
|
<div className="w-full max-w-md h-24 relative flex items-end justify-between px-4 pb-4">
|
||||||
|
<svg className="absolute inset-0 h-full w-full" preserveAspectRatio="none">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="gpaGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor="#4f46e5" stopOpacity="0.25" />
|
||||||
|
<stop offset="100%" stopColor="#4f46e5" stopOpacity="0.0" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
{/* Area under the line */}
|
||||||
|
<path
|
||||||
|
d="M 20 80 Q 150 45 280 25 L 340 15 L 340 96 L 20 96 Z"
|
||||||
|
fill="url(#gpaGradient)"
|
||||||
|
className="transition-all"
|
||||||
|
/>
|
||||||
|
{/* Solid connecting line */}
|
||||||
|
<path
|
||||||
|
d="M 20 80 Q 150 45 280 25 L 340 15"
|
||||||
|
fill="none"
|
||||||
|
stroke="#4f46e5"
|
||||||
|
strokeWidth="3.2"
|
||||||
|
className="transition-all"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Graphical values overlay */}
|
||||||
|
{gpaMilestones.map((ms, idx) => {
|
||||||
|
return (
|
||||||
|
<div key={idx} className="flex flex-col items-center relative z-10">
|
||||||
|
<span className="text-[10px] font-bold text-indigo-600 font-mono tracking-tighter bg-white shadow-xs px-1.5 py-0.5 rounded-md border border-slate-100">
|
||||||
|
{ms.gpa.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
<span className="text-[10px] font-bold text-slate-400 mt-2">{ms.sem}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick stats summarizing CGPA */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-center border-t md:border-t-0 md:border-l border-slate-200/60 pt-4 md:pt-0 md:pl-6 w-full md:w-auto shrink-0">
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] text-slate-400 uppercase font-black">Target GPA</span>
|
||||||
|
<p className="text-xl font-bold text-slate-800">3.90</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] text-slate-400 uppercase font-black">Current CGPA</span>
|
||||||
|
<p className="text-xl font-bold text-indigo-600">3.82</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid: Estimator Section + Midterm Score ledger list */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||||
|
|
||||||
|
{/* GPA Estimator (Columns 3) */}
|
||||||
|
<div id="sgpa-projector-card" className="lg:col-span-3 bg-white rounded-2xl p-6 border border-slate-100 shadow-sm flex flex-col justify-between">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Calculator className="h-5 w-5 text-indigo-500 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-slate-850 text-base">SGPA Goal Estimator</h3>
|
||||||
|
<p className="text-xs text-slate-400">Project your Semester GPA based on target results</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Interactive Courses Estimator List */}
|
||||||
|
<div className="space-y-2.5 max-h-[320px] overflow-y-auto pr-1">
|
||||||
|
{estimateCourses.map((c) => (
|
||||||
|
<div
|
||||||
|
key={c.id}
|
||||||
|
id={`estimator-row-${c.id}`}
|
||||||
|
className="p-3 bg-slate-50 border border-slate-100 rounded-xl flex items-center justify-between"
|
||||||
|
>
|
||||||
|
<div className="min-w-0 pr-2">
|
||||||
|
<p className="font-semibold text-slate-850 text-xs truncate">{c.name}</p>
|
||||||
|
<span className="text-[10px] text-slate-400 font-mono">{c.credits} Credits</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
{/* Grade Selector */}
|
||||||
|
<select
|
||||||
|
id={`gpa-select-${c.id}`}
|
||||||
|
value={c.expectedGrade}
|
||||||
|
onChange={(e) => handleGradeChange(c.id, e.target.value)}
|
||||||
|
className="px-2 py-0.5 rounded border border-slate-200 text-xs text-slate-700 bg-white font-bold"
|
||||||
|
>
|
||||||
|
{Object.keys(GRADE_POINTS).map(grd => (
|
||||||
|
<option key={grd} value={grd}>{grd} ({GRADE_POINTS[grd]})</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id={`delete-estimate-row-${c.id}`}
|
||||||
|
onClick={() => handleDeleteEstimateCourse(c.id)}
|
||||||
|
className="text-slate-300 hover:text-rose-500 p-1"
|
||||||
|
title="Remove Course"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dynamic input form to add estimation rows */}
|
||||||
|
<form onSubmit={handleAddEstimateCourse} className="grid grid-cols-1 md:grid-cols-4 gap-2 pt-2 border-t border-slate-100">
|
||||||
|
<div className="md:col-span-2">
|
||||||
|
<input
|
||||||
|
id="est-course-name-input"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
placeholder="e.g. Elective..."
|
||||||
|
value={newEstName}
|
||||||
|
onChange={(e) => setNewEstName(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-800 px-3.5 py-1.5 rounded-lg border border-slate-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
id="est-course-credits-select"
|
||||||
|
value={newEstCredits}
|
||||||
|
onChange={(e) => setNewEstCredits(Number(e.target.value))}
|
||||||
|
className="w-full text-xs text-slate-700 px-2.5 py-1.5 rounded-lg border border-slate-200 bg-white"
|
||||||
|
>
|
||||||
|
<option value="1">1 Cr</option>
|
||||||
|
<option value="2">2 Cr</option>
|
||||||
|
<option value="3">3 Cr</option>
|
||||||
|
<option value="4">4 Cr</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="add-estimation-btn"
|
||||||
|
type="submit"
|
||||||
|
className={`py-1.5 px-3 text-xs font-semibold ${themeColor.primary} ${themeColor.hover} text-white rounded-lg flex items-center justify-center gap-1.5`}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SGPA projections footer summary */}
|
||||||
|
<div className="bg-slate-900 text-white rounded-xl p-4 mt-6 flex justify-between items-center relative overflow-hidden">
|
||||||
|
<div className="absolute -right-12 -bottom-12 h-24 w-24 bg-indigo-500/10 rounded-full blur-xl"></div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-indigo-300 font-bold uppercase tracking-wider">Projected Term SGPA</p>
|
||||||
|
<h4 className="text-3xl font-black mt-1 font-mono tracking-tight" id="estimated-sgpa-text">
|
||||||
|
{expectedSGPA.toFixed(2)}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="text-[10px] bg-slate-800 text-white font-medium border border-slate-700 px-3 py-1 rounded-lg">
|
||||||
|
{expectedSGPA >= 3.6 ? "Dean's list level" : 'Regular Standing'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Existing Midterm Ledger logs lists (Columns 2) */}
|
||||||
|
<div id="grades-history-ledger" className="lg:col-span-2 bg-white rounded-2xl p-6 border border-slate-100 shadow-sm flex flex-col space-y-4">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<BookMarked className="h-5 w-5 text-indigo-500 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold text-slate-800 text-base">Completed Midterm Records</h3>
|
||||||
|
<p className="text-xs text-slate-400 font-mono">Current session weightings</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 flex-1 overflow-y-auto max-h-[380px] pr-1">
|
||||||
|
{courses.map((c, idx) => {
|
||||||
|
// Simulated mid and assignment mock score
|
||||||
|
const midScore = idx % 2 === 0 ? 88 : 79;
|
||||||
|
const assScore = idx % 2 === 0 ? 94 : 85;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={c.id} className="p-3.5 bg-slate-50 border border-slate-100/50 rounded-xl space-y-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<span className="text-[10px] font-bold text-indigo-600 font-mono">{c.code}</span>
|
||||||
|
<span className="text-xs font-bold text-slate-700">Course grade: {c.grade}</span>
|
||||||
|
</div>
|
||||||
|
<h4 className="text-xs font-bold text-slate-800 line-clamp-1">{c.name}</h4>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-dashed border-slate-200 text-[10px] text-slate-500">
|
||||||
|
<div>
|
||||||
|
<span>Midterms: </span>
|
||||||
|
<strong className="text-slate-700 font-mono">{midScore}/100</strong>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Practical Labs: </span>
|
||||||
|
<strong className="text-slate-700 font-mono">{assScore}/100</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
Mail,
|
||||||
|
MapPin,
|
||||||
|
Save,
|
||||||
|
Check,
|
||||||
|
Camera,
|
||||||
|
Sparkles,
|
||||||
|
Smile,
|
||||||
|
Palette,
|
||||||
|
BookOpen,
|
||||||
|
Clock,
|
||||||
|
TrendingUp,
|
||||||
|
ShieldCheck,
|
||||||
|
Code2,
|
||||||
|
GraduationCap
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export default function ProfileView({ profile, setProfile, themeColor, selectedColorKey, setSelectedColorKey, colorOptions }) {
|
||||||
|
// Controlled forms fields states
|
||||||
|
const [name, setName] = useState(profile.name);
|
||||||
|
const [email, setEmail] = useState(profile.email);
|
||||||
|
const [idCode, setIdCode] = useState(profile.id);
|
||||||
|
const [major, setMajor] = useState(profile.major);
|
||||||
|
const [semester, setSemester] = useState(profile.semester);
|
||||||
|
|
||||||
|
const [selectedAvatar, setSelectedAvatar] = useState(profile.avatarUrl);
|
||||||
|
const [saveSuccess, setSaveSuccess] = useState(false);
|
||||||
|
|
||||||
|
const avatars = [
|
||||||
|
{ name: 'Developer Hamza', url: 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&q=80&w=120' },
|
||||||
|
{ name: 'Creative Techie', url: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?auto=format&fit=crop&q=80&w=120' },
|
||||||
|
{ name: 'Female Student A', url: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&q=80&w=12' },
|
||||||
|
{ name: 'Designer Student', url: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&q=80&w=120' },
|
||||||
|
{ name: 'Glasses Geek', url: 'https://images.unsplash.com/photo-1628157582853-a796fa650a6a?auto=format&fit=crop&q=80&w=120' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleSaveProfile = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!name.trim() || !email.trim()) return;
|
||||||
|
|
||||||
|
setProfile({
|
||||||
|
...profile,
|
||||||
|
name,
|
||||||
|
email,
|
||||||
|
id: idCode,
|
||||||
|
major,
|
||||||
|
semester,
|
||||||
|
avatarUrl: selectedAvatar
|
||||||
|
});
|
||||||
|
|
||||||
|
setSaveSuccess(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setSaveSuccess(false);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div id="student-profile-settings-panel" className="space-y-6">
|
||||||
|
|
||||||
|
{/* Visual profile header banner card */}
|
||||||
|
<div className="bg-slate-900 rounded-2xl p-6 text-white flex flex-col md:flex-row items-center gap-6 relative overflow-hidden">
|
||||||
|
|
||||||
|
{/* Absolute visual patterns background lines */}
|
||||||
|
<div className="absolute right-0 top-0 h-48 w-48 rounded-full bg-indigo-505 bg-indigo-500/10 blur-2xl pointer-events-none"></div>
|
||||||
|
<div className="absolute left-1/3 bottom-0 h-32 w-32 rounded-full bg-emerald-500/5 blur-xl pointer-events-none"></div>
|
||||||
|
|
||||||
|
<div className="relative group shrink-0">
|
||||||
|
<img
|
||||||
|
id="profile-display-avatar"
|
||||||
|
src={selectedAvatar}
|
||||||
|
alt={profile.name}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
className="h-24 w-24 rounded-full object-cover border-4 border-slate-100 shadow-md group-hover:scale-105 transition-transform duration-300"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-black/40 rounded-full flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
|
||||||
|
<Smile className="h-5 w-5 text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center md:text-left space-y-1.5 flex-1 min-w-0">
|
||||||
|
<h2 className="text-xl md:text-2xl font-bold text-slate-800 truncate text-white">{profile.name}</h2>
|
||||||
|
<p className="text-xs text-slate-350 font-medium flex items-center justify-center md:justify-start gap-1.5">
|
||||||
|
<GraduationCap className="h-4 w-4 shrink-0 text-slate-400" />
|
||||||
|
<span className="truncate">{profile.major}</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-[11px] font-mono font-medium text-slate-400">
|
||||||
|
ID: {profile.id} • Registered credits: 17 Credits
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Settings Grid layouts */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
|
||||||
|
|
||||||
|
{/* Profile details editing (Columns 2) */}
|
||||||
|
<div id="edit-profile-card" className="lg:col-span-2 bg-white rounded-2xl p-6 border border-slate-100 shadow-sm space-y-4">
|
||||||
|
<div className="flex items-center gap-2 mb-4 pb-2 border-b border-slate-100">
|
||||||
|
<User className="h-5 w-5 text-indigo-500" />
|
||||||
|
<h3 className="font-bold text-slate-800 text-base">Update Personal Details</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSaveProfile} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
|
||||||
|
{/* Full Name */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 uppercase mb-1">Full Student Name</label>
|
||||||
|
<input
|
||||||
|
id="profile-name-input"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-800 px-3.5 py-2.5 rounded-xl border border-slate-200 focus:outline-none focus:border-indigo-400 bg-slate-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Student ID */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 uppercase mb-1">Student University ID</label>
|
||||||
|
<input
|
||||||
|
id="profile-id-input"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={idCode}
|
||||||
|
onChange={(e) => setIdCode(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-800 px-3.5 py-2.5 rounded-xl border border-slate-200 focus:outline-none focus:border-indigo-400 bg-slate-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email Address */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 uppercase mb-1">Registered Email Address</label>
|
||||||
|
<input
|
||||||
|
id="profile-email-input"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-800 px-3.5 py-2.5 rounded-xl border border-slate-200 focus:outline-none bg-slate-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Major Department Option */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 uppercase mb-1">Academic Major</label>
|
||||||
|
<input
|
||||||
|
id="profile-major-input"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={major}
|
||||||
|
onChange={(e) => setMajor(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-800 px-3.5 py-2.5 rounded-xl border border-slate-200 focus:outline-none bg-slate-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Semester level */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 uppercase mb-1">Enrollment Semester level</label>
|
||||||
|
<input
|
||||||
|
id="profile-semester-input"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
value={semester}
|
||||||
|
onChange={(e) => setSemester(e.target.value)}
|
||||||
|
className="w-full text-xs text-slate-800 px-3.5 py-2.5 rounded-xl border border-slate-200 focus:outline-none bg-slate-50"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Change Profile avatar box list */}
|
||||||
|
<div className="pt-2">
|
||||||
|
<label className="block text-[11px] font-bold text-slate-400 uppercase mb-2">Configure Custom Avatar icon</label>
|
||||||
|
<div className="grid grid-cols-5 gap-3.5" id="change-avatar-collection">
|
||||||
|
{avatars.map((av) => {
|
||||||
|
const isAvatarSelected = selectedAvatar === av.url;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={av.name}
|
||||||
|
id={`avatar-btn-${av.name.toLowerCase().replace(' ', '-')}`}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedAvatar(av.url)}
|
||||||
|
className={`relative aspect-square rounded-full overflow-hidden border-2 cursor-pointer ${
|
||||||
|
isAvatarSelected ? 'border-indigo-600 scale-105 shadow' : 'border-slate-100 hover:border-slate-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={av.url}
|
||||||
|
alt={av.name}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
|
{isAvatarSelected && (
|
||||||
|
<div className="absolute inset-0 bg-indigo-600/30 flex items-center justify-center">
|
||||||
|
<Check className="h-4 w-4 text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Notification alert / CTAs */}
|
||||||
|
<div className="pt-4 flex items-center justify-between gap-4">
|
||||||
|
{saveSuccess ? (
|
||||||
|
<div id="save-success-alert" className="text-xs font-bold text-emerald-600 bg-emerald-50 px-4 py-2.5 rounded-xl border border-emerald-100 flex items-center gap-1.5 animate-pulse">
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
Details updated successfully!
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="w-1"></span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="save-profile-btn"
|
||||||
|
type="submit"
|
||||||
|
className={`px-6 py-2.5 text-xs font-semibold text-white ${themeColor.primary} ${themeColor.hover} rounded-xl shadow-sm flex items-center gap-1.5 transition-all`}
|
||||||
|
>
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Brand Theme / Design Preferences (Column 1) */}
|
||||||
|
<div id="brand-theme-picker-card" className="bg-white rounded-2xl p-6 border border-slate-100 shadow-sm space-y-5">
|
||||||
|
<div className="flex items-center gap-2 pb-2 border-b border-slate-100">
|
||||||
|
<Palette className="h-5 w-5 text-indigo-500 shrink-0" />
|
||||||
|
<h3 className="font-bold text-slate-800 text-sm">Theme Settings</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-xs text-slate-500 leading-relaxed font-medium">
|
||||||
|
Select the accent theme custom color for the system dashboard, which will be applied to every icon, calendar, button, and badge:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Selecting circular color palettes */}
|
||||||
|
<div className="grid grid-cols-2 gap-2" id="theme-color-palette-selector">
|
||||||
|
{Object.keys(colorOptions).map((key) => {
|
||||||
|
const opt = colorOptions[key];
|
||||||
|
const isThemeActive = selectedColorKey === key;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={key}
|
||||||
|
id={`theme-color-opt-${key}`}
|
||||||
|
onClick={() => setSelectedColorKey(key)}
|
||||||
|
className={`flex items-center gap-2.5 p-2 rounded-xl border text-xs text-slate-700 font-semibold transition-all ${
|
||||||
|
isThemeActive
|
||||||
|
? 'border-indigo-500 bg-indigo-50/20'
|
||||||
|
: 'border-slate-100 hover:bg-slate-50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className={`block h-4 w-4 rounded-full ${opt.bg} border-2 border-white shadow-xs`}></span>
|
||||||
|
<span>{opt.name}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick aesthetic prompt message */}
|
||||||
|
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100 space-y-1">
|
||||||
|
<p className="text-[10px] text-slate-400 font-bold uppercase font-sans tracking-wide">University Policy</p>
|
||||||
|
<p className="text-xs text-slate-500 leading-relaxed font-medium">
|
||||||
|
The university portal's dark theme is automatically optimized scheduled for sunset levels. Keep tracking tasks efficiently.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import { motion, AnimatePresence } from 'motion/react';
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
BookOpen,
|
||||||
|
CheckSquare,
|
||||||
|
Calendar,
|
||||||
|
GraduationCap,
|
||||||
|
Bell,
|
||||||
|
User,
|
||||||
|
X,
|
||||||
|
Menu,
|
||||||
|
GraduationCap as LogoIcon
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
export default function Sidebar({
|
||||||
|
activeTab,
|
||||||
|
setActiveTab,
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
profile,
|
||||||
|
themeColor
|
||||||
|
}) {
|
||||||
|
|
||||||
|
const menuItems = [
|
||||||
|
{ id: 'dashboard', name: 'Dashboard', icon: LayoutDashboard },
|
||||||
|
{ id: 'courses', name: 'My Courses', icon: BookOpen },
|
||||||
|
{ id: 'assignments', name: 'Task Board', icon: CheckSquare },
|
||||||
|
{ id: 'attendance', name: 'Attendance', icon: Calendar },
|
||||||
|
{ id: 'grades', name: 'Grades & GPA', icon: GraduationCap },
|
||||||
|
{ id: 'profile', name: 'Profile Settings', icon: User },
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleTabClick = (tabId) => {
|
||||||
|
setActiveTab(tabId);
|
||||||
|
setIsOpen(false); // Close mobile sidebar on select
|
||||||
|
};
|
||||||
|
|
||||||
|
const navContent = (
|
||||||
|
<div className="flex flex-col h-full bg-slate-900 text-slate-100 z-50">
|
||||||
|
{/* Brand Logo Header */}
|
||||||
|
<div className="flex items-center justify-between p-5 border-b border-slate-800">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className={`p-2 rounded-lg ${themeColor.bg} ${themeColor.text}`}>
|
||||||
|
<LogoIcon className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="font-bold text-lg tracking-tight bg-gradient-to-r from-white to-slate-400 bg-clip-text text-transparent">
|
||||||
|
CampusFlow
|
||||||
|
</h1>
|
||||||
|
<p className="text-xs text-slate-400">Student Portal</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
id="close-sidebar-btn"
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
className="md:hidden p-1 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800"
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mini Student Card */}
|
||||||
|
<div className="p-4 mx-3 my-4 bg-slate-800/80 rounded-xl border border-slate-700/50 flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
id="sidebar-profile-avatar"
|
||||||
|
src={profile.avatarUrl}
|
||||||
|
alt={profile.name}
|
||||||
|
referrerPolicy="no-referrer"
|
||||||
|
className="h-10 w-10 rounded-full object-cover border-2 border-slate-600 shadow"
|
||||||
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="text-sm font-semibold text-slate-50 truncate" id="sidebar-profile-name">
|
||||||
|
{profile.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-[10px] text-slate-400 font-mono truncate">
|
||||||
|
ID: {profile.id}
|
||||||
|
</p>
|
||||||
|
<span className={`inline-block mt-1 text-[9px] font-medium px-1.5 py-0.2 rounded bg-slate-700 ${themeColor.text}`}>
|
||||||
|
GPA {profile.gpa.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Navigation Menu */}
|
||||||
|
<nav id="sidebar-nav-container" className="flex-1 px-3 space-y-1 overflow-y-auto">
|
||||||
|
{menuItems.map((item) => {
|
||||||
|
const Icon = item.icon;
|
||||||
|
const isActive = activeTab === item.id;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
id={`sidebar-tab-btn-${item.id}`}
|
||||||
|
onClick={() => handleTabClick(item.id)}
|
||||||
|
className={`w-full flex items-center gap-3.5 px-4 py-3 rounded-xl text-sm font-medium transition-all ${
|
||||||
|
isActive
|
||||||
|
? `${themeColor.bg} ${themeColor.text} font-semibold shadow-md shadow-slate-905`
|
||||||
|
: 'text-slate-400 hover:text-slate-150 hover:bg-slate-800/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className={`h-5 w-5 ${isActive ? 'scale-110' : ''}`} />
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Footer Info info */}
|
||||||
|
<div className="p-4 border-t border-slate-800 text-center text-xs text-slate-500">
|
||||||
|
<p className="font-mono">Uni Portal v2.4.1</p>
|
||||||
|
<p className="mt-1">© 2026 CampusFlow</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Desktop Sidebar (Docked) */}
|
||||||
|
<aside
|
||||||
|
id="desktop-sidebar-pane"
|
||||||
|
className="hidden md:flex flex-col w-64 h-screen fixed top-0 left-0 border-r border-slate-800 bg-slate-900 pointer-events-auto"
|
||||||
|
>
|
||||||
|
{navContent}
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
{/* Mobile Drawer (with dynamic overlays) */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
{/* Backdrop */}
|
||||||
|
<motion.div
|
||||||
|
id="sidebar-backdrop"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 0.5 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
onClick={() => setIsOpen(false)}
|
||||||
|
className="md:hidden fixed inset-0 bg-black z-40"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Nav Panel Drawer */}
|
||||||
|
<motion.div
|
||||||
|
id="mobile-sidebar-drawer"
|
||||||
|
initial={{ x: '-100%' }}
|
||||||
|
animate={{ x: 0 }}
|
||||||
|
exit={{ x: '-100%' }}
|
||||||
|
transition={{ type: 'spring', damping: 25, stiffness: 220 }}
|
||||||
|
className="md:hidden fixed top-0 left-0 bottom-0 w-72 h-screen z-50 shadow-2xl"
|
||||||
|
>
|
||||||
|
{navContent}
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
export const INITIAL_PROFILE = {
|
||||||
|
name: 'Hamza Khan',
|
||||||
|
id: 'ST-2025-089',
|
||||||
|
email: 'hamza.khan@university.edu',
|
||||||
|
major: 'Computer Science & Engineering',
|
||||||
|
semester: '4th Semester (Sophomore)',
|
||||||
|
gpa: 3.82,
|
||||||
|
avatarUrl: 'https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?auto=format&fit=crop&q=80&w=120',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INITIAL_COURSES = [
|
||||||
|
{
|
||||||
|
id: 'CS-401',
|
||||||
|
code: 'CS-401',
|
||||||
|
name: 'Advanced Algorithms',
|
||||||
|
instructor: 'Dr. Aisha Rahman',
|
||||||
|
instructorEmail: 'aisha.rahman@univ.edu',
|
||||||
|
room: 'Lab-3C, Tech Block',
|
||||||
|
schedule: 'Mon, Wed - 10:00 AM',
|
||||||
|
progress: 78,
|
||||||
|
attendanceRate: 92,
|
||||||
|
grade: 'A-',
|
||||||
|
credits: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'CS-402',
|
||||||
|
code: 'CS-402',
|
||||||
|
name: 'Database Systems & SQL',
|
||||||
|
instructor: 'Dr. Vikram Seth',
|
||||||
|
instructorEmail: 'v.seth@univ.edu',
|
||||||
|
room: 'Room-405, Admin Block',
|
||||||
|
schedule: 'Tue, Thu - 11:30 AM',
|
||||||
|
progress: 85,
|
||||||
|
attendanceRate: 95,
|
||||||
|
grade: 'A',
|
||||||
|
credits: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'CS-403',
|
||||||
|
code: 'CS-403',
|
||||||
|
name: 'Full-Stack Web Development',
|
||||||
|
instructor: 'Prof. Sarah Thomas',
|
||||||
|
instructorEmail: 's.thomas@univ.edu',
|
||||||
|
room: 'Lab-1A, Tech Block',
|
||||||
|
schedule: 'Mon, Wed - 02:00 PM',
|
||||||
|
progress: 92,
|
||||||
|
attendanceRate: 100,
|
||||||
|
grade: 'A+',
|
||||||
|
credits: 4,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'CS-404',
|
||||||
|
code: 'CS-404',
|
||||||
|
name: 'Human-Computer Interaction',
|
||||||
|
instructor: 'Prof. Kabir Mehta',
|
||||||
|
instructorEmail: 'k.mehta@univ.edu',
|
||||||
|
room: 'Room-201, Design Wing',
|
||||||
|
schedule: 'Fri - 09:00 AM',
|
||||||
|
progress: 64,
|
||||||
|
attendanceRate: 85,
|
||||||
|
grade: 'B+',
|
||||||
|
credits: 3,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'CS-405',
|
||||||
|
code: 'CS-405',
|
||||||
|
name: 'Technical Writing & Ethics',
|
||||||
|
instructor: 'Prof. Helen Clark',
|
||||||
|
instructorEmail: 'h.clark@univ.edu',
|
||||||
|
room: 'Room-102, Arts Block',
|
||||||
|
schedule: 'Thu - 03:30 PM',
|
||||||
|
progress: 50,
|
||||||
|
attendanceRate: 88,
|
||||||
|
grade: 'B',
|
||||||
|
credits: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_ASSIGNMENTS = [
|
||||||
|
{
|
||||||
|
id: 'ass-1',
|
||||||
|
title: 'Red-Black Tree Implementation',
|
||||||
|
courseId: 'CS-401',
|
||||||
|
courseName: 'Advanced Algorithms',
|
||||||
|
dueDate: '2026-06-05',
|
||||||
|
status: 'Pending',
|
||||||
|
priority: 'High',
|
||||||
|
description: 'Implement insertion and deletion operations for Red-Black Trees in Java or C++. Prove O(log n) complexity.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ass-2',
|
||||||
|
title: 'Database Schema Normalization',
|
||||||
|
courseId: 'CS-402',
|
||||||
|
courseName: 'Database Systems & SQL',
|
||||||
|
dueDate: '2026-06-10',
|
||||||
|
status: 'In_Progress',
|
||||||
|
priority: 'Medium',
|
||||||
|
description: 'Normalize the given retail transaction database schema up to Boyce-Codd Normal Form (BCNF). Include all SQL scripts.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ass-3',
|
||||||
|
title: 'Build a Real-Time Chat Application',
|
||||||
|
courseId: 'CS-403',
|
||||||
|
courseName: 'Full-Stack Web Development',
|
||||||
|
dueDate: '2026-06-03',
|
||||||
|
status: 'Completed',
|
||||||
|
priority: 'High',
|
||||||
|
description: 'Build a secure, live chat application using React, Express, and WebSockets. Use high contrast UX themes.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ass-4',
|
||||||
|
title: 'UI/UX Interactive Audit',
|
||||||
|
courseId: 'CS-404',
|
||||||
|
courseName: 'Human-Computer Interaction',
|
||||||
|
dueDate: '2026-06-12',
|
||||||
|
status: 'Pending',
|
||||||
|
priority: 'Low',
|
||||||
|
description: 'Perform a cognitive walkthrough of the university portal. Document 5 critical usability bugs and suggest modern designs.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ass-5',
|
||||||
|
title: 'Intellectual Property Essay',
|
||||||
|
courseId: 'CS-405',
|
||||||
|
courseName: 'Technical Writing & Ethics',
|
||||||
|
dueDate: '2026-06-08',
|
||||||
|
status: 'In_Progress',
|
||||||
|
priority: 'Medium',
|
||||||
|
description: 'Write a 1500-word argumentative essay on the ethical implications of using large language models in open-source software.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_ANNOUNCEMENTS = [
|
||||||
|
{
|
||||||
|
id: 'ann-1',
|
||||||
|
title: 'End Semester Exam Schedule Released',
|
||||||
|
sender: 'Academic Registrar Office',
|
||||||
|
date: '2026-05-30',
|
||||||
|
content: 'The end-semester examinations will commence from June 18th, 2026. The full date sheet has been uploaded to the university server. Please ensure all dues are cleared to download exam hall tickets.',
|
||||||
|
category: 'Exam',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ann-2',
|
||||||
|
title: 'Annual Tech Fest "Hackathon 2026" Registrations Open',
|
||||||
|
sender: 'Computer Society Council',
|
||||||
|
date: '2026-05-28',
|
||||||
|
content: 'Register for our annual 36-hour hackathon happening this weekend! Huge prizes, delicious free pizzas, and recruitment opportunities from top tech companies. Team limit is 4 members.',
|
||||||
|
category: 'Event',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ann-3',
|
||||||
|
title: 'New Library E-Resources Access Protocol',
|
||||||
|
sender: 'Central Library',
|
||||||
|
date: '2026-05-25',
|
||||||
|
content: 'We have subscribed to IEEE Xplore Digital Library and ACM Digital Library. Students can access these repositories anywhere off-campus using their student credentials via OpenAthens portal.',
|
||||||
|
category: 'Academic',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_ATTENDANCE = [
|
||||||
|
// Today's Date is around 2026-06-01. Let's seed history.
|
||||||
|
{ date: '2026-05-28', status: 'Present', courseId: 'CS-402', courseName: 'Database Systems & SQL' },
|
||||||
|
{ date: '2026-05-28', status: 'Present', courseId: 'CS-405', courseName: 'Technical Writing & Ethics' },
|
||||||
|
{ date: '2026-05-29', status: 'Present', courseId: 'CS-404', courseName: 'Human-Computer Interaction' },
|
||||||
|
{ date: '2026-05-31', status: 'Present', courseId: 'CS-403', courseName: 'Full-Stack Web Development' },
|
||||||
|
{ date: '2026-05-31', status: 'Late', courseId: 'CS-401', courseName: 'Advanced Algorithms' },
|
||||||
|
{ date: '2026-06-01', status: 'Present', courseId: 'CS-401', courseName: 'Advanced Algorithms' },
|
||||||
|
{ date: '2026-06-01', status: 'Present', courseId: 'CS-403', courseName: 'Full-Stack Web Development' },
|
||||||
|
{ date: '2026-05-27', status: 'Absent', courseId: 'CS-401', courseName: 'Advanced Algorithms' },
|
||||||
|
{ date: '2026-05-26', status: 'Present', courseId: 'CS-402', courseName: 'Database Systems & SQL' },
|
||||||
|
{ date: '2026-05-25', status: 'Present', courseId: 'CS-401', courseName: 'Advanced Algorithms' },
|
||||||
|
{ date: '2026-05-25', status: 'Present', courseId: 'CS-403', courseName: 'Full-Stack Web Development' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_GRADES = [
|
||||||
|
{ id: 'grd-1', courseId: 'CS-401', courseName: 'Advanced Algorithms', assessmentName: 'Midterm Exam', score: 84, maxScore: 100, weight: 30 },
|
||||||
|
{ id: 'grd-2', courseId: 'CS-401', courseName: 'Advanced Algorithms', assessmentName: 'Assignment 1', score: 95, maxScore: 100, weight: 10 },
|
||||||
|
{ id: 'grd-3', courseId: 'CS-402', courseName: 'Database Systems & SQL', assessmentName: 'Midterm Exam', score: 90, maxScore: 100, weight: 30 },
|
||||||
|
{ id: 'grd-4', courseId: 'CS-402', courseName: 'Database Systems & SQL', assessmentName: 'SQL Pop Quiz', score: 10, maxScore: 10, weight: 5 },
|
||||||
|
{ id: 'grd-5', courseId: 'CS-403', courseName: 'Full-Stack Web Development', assessmentName: 'Milestone 1 Project', score: 98, maxScore: 100, weight: 25 },
|
||||||
|
{ id: 'grd-6', courseId: 'CS-403', courseName: 'Full-Stack Web Development', assessmentName: 'CSS Design Contest', score: 100, maxScore: 100, weight: 10 },
|
||||||
|
{ id: 'grd-7', courseId: 'CS-404', courseName: 'Human-Computer Interaction', assessmentName: 'Heuristic Review', score: 78, maxScore: 100, weight: 20 },
|
||||||
|
];
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
background-color: #F8FAFC;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom card helper matches design specification of #E2E8F0 border */
|
||||||
|
.polish-card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #E2E8F0;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import {StrictMode} from 'react';
|
||||||
|
import {createRoot} from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
);
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"useDefineForClassFields": false,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": [
|
||||||
|
"ES2022",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"isolatedModules": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"allowJs": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
import {defineConfig} from 'vite';
|
||||||
|
|
||||||
|
export default defineConfig(() => {
|
||||||
|
return {
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, '.'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||||
|
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||||
|
hmr: process.env.DISABLE_HMR !== 'true',
|
||||||
|
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
|
||||||
|
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user