424 lines
18 KiB
React
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>
|
|
);
|
|
}
|