Files
student-dashboard/src/components/AssignmentTracker.jsx
T

424 lines
18 KiB
React

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>
);
}