refactor: update UI components and pages with a refreshed visual design and improved styling.

This commit is contained in:
2026-03-03 12:42:37 -03:00
parent 9123337200
commit 15f2649777
27 changed files with 1864 additions and 1557 deletions
+95 -85
View File
@@ -149,9 +149,9 @@ export default function GradebookPage() {
<div className="flex items-center gap-3">
<button
onClick={() => setShowBulkEnroll(true)}
className="bg-white/5 hover:bg-white/10 border border-white/10 px-4 py-2 rounded-xl flex items-center gap-2 transition-all font-medium text-sm"
className="bg-slate-50 dark:bg-white/5 hover:bg-slate-100 dark:hover:bg-white/10 border border-slate-200 dark:border-white/10 px-4 py-2 rounded-xl flex items-center gap-2 transition-all font-bold text-xs uppercase tracking-widest text-slate-600 dark:text-gray-400 shadow-sm"
>
<Users size={16} /> Inscripción Masiva
<Users size={16} className="text-blue-500" /> Inscripción Masiva
</button>
<button
onClick={exportCSV}
@@ -166,66 +166,73 @@ export default function GradebookPage() {
{/* Bulk Enroll Modal */}
{showBulkEnroll && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 dark:bg-black/80 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white dark:bg-[#1a1d23] border border-slate-200 dark:border-white/10 rounded-2xl w-full max-w-2xl overflow-hidden shadow-2xl animate-in fade-in zoom-in duration-200">
<div className="p-6 border-b border-slate-100 dark:border-white/5 flex items-center justify-between">
<h3 className="text-xl font-bold flex items-center gap-2 text-slate-900 dark:text-white">
<Users className="text-blue-500 dark:text-blue-400" /> Inscripción Masiva de Estudiantes
</h3>
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 dark:bg-black/80 backdrop-blur-md animate-in fade-in duration-300">
<div className="bg-white dark:bg-[#1a1d23] border border-slate-200 dark:border-white/10 rounded-[2.5rem] w-full max-w-2xl overflow-hidden shadow-2xl animate-in zoom-in-95 duration-200">
<div className="p-8 border-b border-slate-100 dark:border-white/5 flex items-center justify-between bg-slate-50/50 dark:bg-transparent">
<div>
<h3 className="text-2xl font-black flex items-center gap-3 text-slate-900 dark:text-white uppercase tracking-tight">
<Users className="text-blue-600 dark:text-blue-400" /> Inscripción Masiva
</h3>
<p className="text-xs text-slate-400 dark:text-gray-500 font-bold uppercase tracking-widest mt-1">Enroll multiple students at once</p>
</div>
<button
onClick={() => {
setShowBulkEnroll(false);
setBulkResults(null);
setBulkEmails("");
}}
className="p-1 hover:bg-white/5 rounded-lg transition-colors"
className="p-3 hover:bg-slate-200 dark:hover:bg-white/10 rounded-2xl transition-all group active:scale-95"
>
<X size={20} />
<X size={20} className="text-slate-400 group-hover:text-slate-900 dark:group-hover:text-white" />
</button>
</div>
<div className="p-8 space-y-6">
<div className="p-8 space-y-8">
{!bulkResults ? (
<>
<div>
<label className="block text-sm font-medium text-slate-500 dark:text-gray-400 mb-2">
Ingresa las direcciones de correo (separadas por comas o saltos de línea)
<label className="block text-xs font-black uppercase tracking-widest text-slate-400 dark:text-gray-500 mb-3">
Enter email addresses (separated by commas or new lines)
</label>
<textarea
value={bulkEmails}
onChange={(e) => setBulkEmails(e.target.value)}
placeholder="student1@example.com&#10;student2@example.com"
className="w-full h-48 bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-xl p-4 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-600 resize-none font-mono"
className="w-full h-48 bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-3xl p-5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-600 resize-none font-mono shadow-inner leading-relaxed"
/>
</div>
<div className="bg-blue-500/10 border border-blue-500/20 rounded-xl p-4 flex gap-3 italic text-sm text-blue-400">
<AlertTriangle size={18} className="shrink-0" />
Los estudiantes deben tener una cuenta en esta organización para ser inscritos.
<div className="bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 rounded-2xl p-5 flex gap-4">
<div className="w-10 h-10 rounded-xl bg-white dark:bg-black/20 flex items-center justify-center shrink-0 shadow-sm text-blue-500">
<AlertTriangle size={20} />
</div>
<p className="text-xs text-blue-800/80 dark:text-blue-300 leading-relaxed font-bold uppercase tracking-tight">
Students must already have an account in this organization directory to be successfully enrolled into the course.
</p>
</div>
</>
) : (
<div className="space-y-6">
<div className="grid grid-cols-3 gap-4">
<div className="bg-green-500/10 border border-green-500/20 rounded-xl p-4 text-center">
<div className="text-2xl font-black text-green-400">{bulkResults.successful_emails.length}</div>
<div className="text-xs uppercase tracking-widest text-gray-500 font-bold mt-1">Enrolled</div>
<div className="space-y-8">
<div className="grid grid-cols-3 gap-6">
<div className="bg-green-50 dark:bg-green-500/10 border border-green-100 dark:border-green-500/20 rounded-[2rem] p-6 text-center shadow-sm">
<div className="text-3xl font-black text-green-600 dark:text-green-400">{bulkResults.successful_emails.length}</div>
<div className="text-[10px] uppercase tracking-[0.2em] text-green-700/50 dark:text-gray-500 font-black mt-1">Enrolled</div>
</div>
<div className="bg-yellow-500/10 border border-yellow-500/20 rounded-xl p-4 text-center">
<div className="text-2xl font-black text-yellow-400">{bulkResults.already_enrolled_emails.length}</div>
<div className="text-xs uppercase tracking-widest text-gray-500 font-bold mt-1">Skipped</div>
<div className="bg-yellow-50 dark:bg-yellow-500/10 border border-yellow-100 dark:border-yellow-500/20 rounded-[2rem] p-6 text-center shadow-sm">
<div className="text-3xl font-black text-yellow-600 dark:text-yellow-400">{bulkResults.already_enrolled_emails.length}</div>
<div className="text-[10px] uppercase tracking-[0.2em] text-yellow-700/50 dark:text-gray-500 font-black mt-1">Skipped</div>
</div>
<div className="bg-red-500/10 border border-red-500/20 rounded-xl p-4 text-center">
<div className="text-2xl font-black text-red-400">{bulkResults.failed_emails.length}</div>
<div className="text-xs uppercase tracking-widest text-gray-500 font-bold mt-1">Failed</div>
<div className="bg-red-50 dark:bg-red-500/10 border border-red-100 dark:border-red-500/20 rounded-[2rem] p-6 text-center shadow-sm">
<div className="text-3xl font-black text-red-600 dark:text-red-400">{bulkResults.failed_emails.length}</div>
<div className="text-[10px] uppercase tracking-[0.2em] text-red-700/50 dark:text-gray-500 font-black mt-1">Failed</div>
</div>
</div>
{bulkResults.failed_emails.length > 0 && (
<div>
<p className="text-xs font-bold text-gray-500 uppercase tracking-widest mb-2">Failed Emails (Not Found):</p>
<div className="bg-black/20 rounded-xl p-3 text-xs text-red-400 max-h-32 overflow-y-auto font-mono">
<div className="space-y-3">
<p className="text-[10px] font-black text-red-500/60 dark:text-gray-500 uppercase tracking-[0.2em]">Failed Emails (Not Found):</p>
<div className="bg-red-50/50 dark:bg-black/20 border border-red-100 dark:border-white/10 rounded-2xl p-5 text-xs text-red-600 dark:text-red-400 max-h-48 overflow-y-auto font-mono custom-scrollbar leading-relaxed">
{bulkResults.failed_emails.map(e => (
<div key={e}>{e}</div>
<div key={e} className="py-1 border-b border-red-500/5 last:border-0">{e}</div>
))}
</div>
</div>
@@ -234,21 +241,21 @@ export default function GradebookPage() {
)}
</div>
<div className="p-6 bg-slate-50 dark:bg-white/5 border-t border-slate-100 dark:border-white/5 flex justify-end gap-3">
<div className="p-8 bg-slate-50/50 dark:bg-white/5 border-t border-slate-100 dark:border-white/5 flex justify-end items-center gap-4">
{!bulkResults ? (
<>
<button
onClick={() => setShowBulkEnroll(false)}
className="px-6 py-2 rounded-xl hover:bg-slate-200 dark:hover:bg-white/5 transition-colors font-medium text-slate-600 dark:text-gray-400"
className="px-8 py-3 rounded-2xl hover:bg-slate-200 dark:hover:bg-white/5 transition-all font-black text-[10px] uppercase tracking-widest text-slate-400 dark:text-gray-400 active:scale-95"
>
Cancel
</button>
<button
onClick={handleBulkEnroll}
disabled={bulkLoading || !bulkEmails.trim()}
className="btn-premium px-8 flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
className="flex items-center gap-3 px-10 py-3 bg-blue-600 hover:bg-blue-500 disabled:bg-slate-200 dark:disabled:bg-white/5 text-white disabled:text-slate-400 rounded-2xl font-black text-[10px] uppercase tracking-[0.2em] shadow-xl shadow-blue-500/20 transition-all active:scale-95 disabled:shadow-none disabled:cursor-not-allowed group"
>
{bulkLoading ? <Loader2 className="animate-spin w-4 h-4" /> : 'Procesar Inscripción'}
{bulkLoading ? <Loader2 className="animate-spin w-4 h-4" /> : <CheckCircle size={16} className="group-hover:scale-110 transition-transform" />} Procesar Inscripción
</button>
</>
) : (
@@ -258,7 +265,7 @@ export default function GradebookPage() {
setBulkResults(null);
setBulkEmails("");
}}
className="btn-premium px-8"
className="btn-premium px-12 py-3 rounded-2xl font-black text-[10px] uppercase tracking-widest active:scale-95 shadow-xl shadow-blue-500/20"
>
Done
</button>
@@ -274,41 +281,41 @@ export default function GradebookPage() {
Registro de Calificaciones
</h2>
{/* Controls & Stats */}
<div className="flex flex-col md:flex-row gap-6 justify-between items-center bg-white/5 border border-white/10 rounded-2xl p-6">
<div className="flex items-center gap-4 w-full md:w-auto">
<div className="relative">
{/* Controls & Stats */}
<div className="flex flex-col md:flex-row gap-8 justify-between items-stretch bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-[2.5rem] p-8 shadow-sm">
<div className="flex flex-col sm:flex-row items-center gap-4 flex-1">
<div className="relative w-full sm:w-auto">
<Users size={16} className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 dark:text-gray-500 pointer-events-none z-10" />
<select
value={selectedCohortId}
onChange={(e) => setSelectedCohortId(e.target.value)}
className="appearance-none bg-slate-100 dark:bg-black/20 text-slate-900 dark:text-white border border-slate-200 dark:border-white/10 rounded-xl pl-4 pr-10 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 min-w-[200px]"
className="appearance-none bg-slate-50 dark:bg-black/20 text-slate-900 dark:text-white border border-slate-200 dark:border-white/10 rounded-2xl pl-11 pr-10 py-3.5 text-xs font-black uppercase tracking-widest focus:outline-none focus:ring-2 focus:ring-blue-500/50 min-w-full sm:min-w-[240px] shadow-inner cursor-pointer"
>
<option value="">All Cohorts</option>
{cohorts.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-500">
</div>
</div>
<div className="relative flex-1 md:w-64">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 w-4 h-4" />
<div className="relative w-full flex-1">
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 dark:text-gray-500 w-4 h-4" />
<input
type="text"
placeholder="Search students..."
placeholder="Find a student fast..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full bg-slate-100 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-xl pl-10 pr-4 py-3 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-600"
className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-2xl pl-11 pr-4 py-3.5 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-slate-900 dark:text-white placeholder-slate-400 dark:placeholder-gray-600 font-bold shadow-inner"
/>
</div>
</div>
<div className="flex gap-8 border-l border-white/10 pl-8">
<div className="flex gap-10 border-l border-slate-100 dark:border-white/10 pl-10 items-center">
<div>
<p className="text-xs font-bold text-slate-500 dark:text-gray-500 uppercase tracking-wider mb-1">Students</p>
<p className="text-2xl font-black text-slate-900 dark:text-white">{filteredStudents.length}</p>
<p className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] mb-1.5">Total Enrollment</p>
<p className="text-3xl font-black text-slate-900 dark:text-white uppercase tracking-tighter">{filteredStudents.length}</p>
</div>
<div className="w-px h-10 bg-slate-100 dark:bg-white/5" />
<div>
<p className="text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">Avg Score</p>
<p className={`text-2xl font-black ${averageScore < 0.7 ? 'text-orange-400' : 'text-green-400'}`}>
<p className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] mb-1.5">Avg Performance</p>
<p className={`text-3xl font-black tracking-tighter ${averageScore < 0.7 ? 'text-orange-500' : 'text-blue-600'}`}>
{(averageScore * 100).toFixed(0)}%
</p>
</div>
@@ -316,45 +323,45 @@ export default function GradebookPage() {
</div>
{/* Student List */}
<div className="rounded-3xl border border-white/10 bg-white/[0.02] overflow-hidden">
<div className="rounded-[2.5rem] border border-slate-200 dark:border-white/10 bg-white dark:bg-white/[0.02] overflow-hidden shadow-sm">
<table className="w-full text-left border-collapse">
<thead>
<tr className="bg-white/5 border-b border-white/5">
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Student</th>
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Progress</th>
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Avg. Score</th>
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500">Last Active</th>
<th className="p-6 text-xs font-black uppercase tracking-widest text-gray-500 text-right">Status</th>
<tr className="bg-slate-50 dark:bg-white/5 border-b border-slate-200 dark:border-white/5">
<th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">Student</th>
<th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">Learning Progress</th>
<th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">Score Rating</th>
<th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500">Activity</th>
<th className="p-6 text-[10px] font-black uppercase tracking-[0.2em] text-slate-400 dark:text-gray-500 text-right">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5">
<tbody className="divide-y divide-slate-100 dark:divide-white/5">
{filteredStudents.length === 0 ? (
<tr>
<td colSpan={5} className="p-12 text-center text-gray-600 italic">No students found.</td>
</tr>
) : filteredStudents.map((s) => (
<tr key={s.user_id} className="hover:bg-white/[0.02] transition-colors group">
<tr key={s.user_id} className="hover:bg-slate-50 dark:hover:bg-white/[0.02] transition-colors group">
<td className="p-6">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-gray-900 dark:text-white font-bold text-sm">
<div className="flex items-center gap-5">
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white font-black text-lg shadow-lg shadow-blue-500/20">
{s.full_name.charAt(0)}
</div>
<div>
<div className="font-bold text-gray-900 dark:text-white group-hover:text-blue-400 transition-colors">{s.full_name}</div>
<div className="text-xs text-gray-500 flex items-center gap-1 mt-0.5">
<Mail size={10} /> {s.email}
<div className="font-black text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors uppercase tracking-tight text-sm">{s.full_name}</div>
<div className="text-[11px] text-slate-400 dark:text-gray-500 flex items-center gap-1.5 mt-1 font-medium italic">
<Mail size={12} className="text-blue-400" /> {s.email}
</div>
</div>
</div>
</td>
<td className="p-6 align-middle">
<div className="w-full max-w-[140px]">
<div className="flex justify-between text-xs mb-1 font-bold text-gray-400">
<span>{(s.progress * 100).toFixed(0)}%</span>
<div className="w-full max-w-[160px]">
<div className="flex justify-between text-[10px] mb-2 font-black text-slate-400 uppercase tracking-widest">
<span>{(s.progress * 100).toFixed(0)}% Complete</span>
</div>
<div className="h-1.5 bg-white/10 rounded-full overflow-hidden">
<div className="h-2 bg-slate-100 dark:bg-white/10 rounded-full overflow-hidden shadow-inner">
<div
className="h-full bg-blue-500 rounded-full"
className="h-full bg-gradient-to-r from-blue-600 to-blue-400 rounded-full shadow-lg shadow-blue-500/30 transition-all duration-500"
style={{ width: `${s.progress * 100}%` }}
/>
</div>
@@ -362,31 +369,34 @@ export default function GradebookPage() {
</td>
<td className="p-6 align-middle">
{s.average_score !== null ? (
<span className={`font-black ${(s.average_score || 0) >= 0.8 ? 'text-green-400' : (s.average_score || 0) >= 0.6 ? 'text-yellow-400' : 'text-red-400'}`}>
{((s.average_score || 0) * 100).toFixed(0)}%
</span>
<div className="flex flex-col">
<span className={`text-lg font-black tracking-tighter ${(s.average_score || 0) >= 0.8 ? 'text-green-600 dark:text-green-400' : (s.average_score || 0) >= 0.6 ? 'text-orange-500' : 'text-red-500'}`}>
{((s.average_score || 0) * 100).toFixed(0)}%
</span>
<span className="text-[9px] font-black uppercase tracking-widest text-slate-400 mt-0.5">Weighted Avg</span>
</div>
) : (
<span className="text-gray-600 text-xs italic">No grades</span>
<span className="text-slate-400 dark:text-gray-600 text-[10px] font-black uppercase tracking-widest italic bg-slate-100 dark:bg-white/5 px-2 py-1 rounded-md">Pending</span>
)}
</td>
<td className="p-6 align-middle">
<div className="text-sm text-gray-400 flex items-center gap-2">
<Clock size={14} />
{s.last_active_at ? new Date(s.last_active_at).toLocaleDateString() : 'Never'}
<div className="text-xs text-slate-500 dark:text-gray-400 font-bold flex items-center gap-2 uppercase tracking-tight">
<Clock size={16} className="text-slate-400" />
{s.last_active_at ? new Date(s.last_active_at).toLocaleDateString() : 'No Activity'}
</div>
</td>
<td className="p-6 text-right align-middle">
{s.progress >= 1 ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-green-500/10 text-green-400 border border-green-500/20">
<CheckCircle size={12} /> Completed
<span className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-[10px] font-black uppercase tracking-[0.2em] bg-green-500/10 text-green-600 dark:text-green-400 border border-green-500/20 shadow-sm">
<CheckCircle size={12} /> Graduated
</span>
) : s.last_active_at && (Date.now() - new Date(s.last_active_at).getTime() > 7 * 24 * 60 * 60 * 1000) ? (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-red-500/10 text-red-400 border border-red-500/20">
<AlertCircle size={12} /> Inactive
<span className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-[10px] font-black uppercase tracking-[0.2em] bg-red-500/10 text-red-600 dark:text-red-400 border border-red-500/20 shadow-sm">
<AlertCircle size={12} /> At Risk
</span>
) : (
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest bg-blue-500/10 text-blue-400 border border-blue-500/20">
<GraduationCap size={12} /> Active
<span className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-[10px] font-black uppercase tracking-[0.2em] bg-blue-500/10 text-blue-600 dark:text-blue-400 border border-blue-500/20 shadow-sm">
<GraduationCap size={12} /> Engaged
</span>
)}
</td>