feat: initial commit of CampusFlow student portal with theme configuration
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user