feat: initial commit of CampusFlow student portal with theme configuration

This commit is contained in:
2026-06-01 08:26:25 +05:30
commit eedfbad6c4
19 changed files with 7641 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
node_modules/
build/
dist/
coverage/
.DS_Store
*.log
.env*
!.env.example
+35
View File
@@ -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
View File
@@ -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>
+6
View File
@@ -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"]
}
+4347
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -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
View File
@@ -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>
);
}
+423
View File
@@ -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>
);
}
+394
View File
@@ -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>
);
}
+340
View File
@@ -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>
);
}
+678
View File
@@ -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 &rarr;
</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>
);
}
+310
View File
@@ -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>
);
}
+279
View File
@@ -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>
);
}
+155
View File
@@ -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>
</>
);
}
+182
View File
@@ -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 },
];
+21
View File
@@ -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);
}
+10
View File
@@ -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>,
);
+26
View File
@@ -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
}
}
+22
View File
@@ -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 : {},
},
};
});