Assignment Logs
This commit is contained in:
52
src/App.css
52
src/App.css
@@ -106,3 +106,55 @@
|
|||||||
.retry-button:hover {
|
.retry-button:hover {
|
||||||
background-color: #6d28d9;
|
background-color: #6d28d9;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-tabs {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
border-bottom: 2px solid #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #9ca3af;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab:hover {
|
||||||
|
color: #d8b4fe;
|
||||||
|
background-color: rgba(124, 58, 237, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab.active {
|
||||||
|
color: #a78bfa;
|
||||||
|
border-bottom-color: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav-tabs {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-tab {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/App.jsx
41
src/App.jsx
@@ -1,6 +1,8 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle, BarChart3, FileText, TableIcon } from 'lucide-react';
|
||||||
import { GradesTable } from './components/GradesTable';
|
import { GradesTable } from './components/GradesTable';
|
||||||
|
import { AssignmentLogs } from './components/AssignmentLogs';
|
||||||
|
import { Metrics } from './components/Metrics';
|
||||||
import { LoadingSpinner } from './components/LoadingSpinner';
|
import { LoadingSpinner } from './components/LoadingSpinner';
|
||||||
import { AuthPage } from './components/AuthPage';
|
import { AuthPage } from './components/AuthPage';
|
||||||
import { useAuth } from './contexts/AuthContext';
|
import { useAuth } from './contexts/AuthContext';
|
||||||
@@ -12,6 +14,7 @@ function App() {
|
|||||||
const [data, setData] = useState(null);
|
const [data, setData] = useState(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
const [currentPage, setCurrentPage] = useState('grades');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
@@ -44,12 +47,12 @@ function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show loading spinner while fetching grades
|
// Show loading spinner while fetching grades
|
||||||
if (loading) {
|
if (loading && currentPage === 'grades') {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show error state
|
// Show error state
|
||||||
if (error) {
|
if (error && currentPage === 'grades') {
|
||||||
return (
|
return (
|
||||||
<div className="error-container">
|
<div className="error-container">
|
||||||
<div className="error-content">
|
<div className="error-content">
|
||||||
@@ -73,16 +76,46 @@ function App() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||||
|
{currentPage === 'grades' && (
|
||||||
<button onClick={fetchGrades} className="refresh-button">
|
<button onClick={fetchGrades} className="refresh-button">
|
||||||
Refresh
|
Refresh
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
<button onClick={logout} className="refresh-button" style={{ background: '#dc2626' }}>
|
<button onClick={logout} className="refresh-button" style={{ background: '#dc2626' }}>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<GradesTable data={data} />
|
<nav className="nav-tabs">
|
||||||
|
<button
|
||||||
|
className={`nav-tab ${currentPage === 'grades' ? 'active' : ''}`}
|
||||||
|
onClick={() => setCurrentPage('grades')}
|
||||||
|
>
|
||||||
|
<TableIcon size={18} />
|
||||||
|
<span>Grades</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`nav-tab ${currentPage === 'logs' ? 'active' : ''}`}
|
||||||
|
onClick={() => setCurrentPage('logs')}
|
||||||
|
>
|
||||||
|
<FileText size={18} />
|
||||||
|
<span>Assignment Logs</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`nav-tab ${currentPage === 'metrics' ? 'active' : ''}`}
|
||||||
|
onClick={() => setCurrentPage('metrics')}
|
||||||
|
>
|
||||||
|
<BarChart3 size={18} />
|
||||||
|
<span>Metrics</span>
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div className="page-content">
|
||||||
|
{currentPage === 'grades' && <GradesTable data={data} />}
|
||||||
|
{currentPage === 'logs' && <AssignmentLogs />}
|
||||||
|
{currentPage === 'metrics' && <Metrics />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
261
src/components/AssignmentLogs.css
Normal file
261
src/components/AssignmentLogs.css
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
.logs-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(to right, #a78bfa, #ec4899);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-subtitle {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-loading,
|
||||||
|
.logs-error,
|
||||||
|
.logs-empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border: 4px solid #374151;
|
||||||
|
border-top-color: #a78bfa;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn {
|
||||||
|
background-color: #7c3aed;
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 1.5rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn:hover {
|
||||||
|
background-color: #6d28d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-card {
|
||||||
|
background-color: #2b2b2b;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
padding: 1.25rem;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-card:hover {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 4px 12px rgba(124, 58, 237, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-main {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-class {
|
||||||
|
color: #d8b4fe;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-assignment {
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-category {
|
||||||
|
background-color: rgba(124, 58, 237, 0.2);
|
||||||
|
color: #c4b5fd;
|
||||||
|
padding: 0.125rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-separator {
|
||||||
|
color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-period {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-grade-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-grade {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-change {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-positive {
|
||||||
|
background-color: rgba(74, 222, 128, 0.1);
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-negative {
|
||||||
|
background-color: rgba(248, 113, 113, 0.1);
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.change-none {
|
||||||
|
background-color: rgba(156, 163, 175, 0.1);
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
border-top: 1px solid #374151;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-timestamp {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-due-date {
|
||||||
|
color: #fbbf24;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.log-main {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-grade-info {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-grade {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-dropdown {
|
||||||
|
background-color: #2b2b2b;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
color: #e0e0e0;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-dropdown:hover {
|
||||||
|
border-color: #7c3aed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-dropdown:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #7c3aed;
|
||||||
|
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.log-due-date {
|
||||||
|
color: #fbbf24;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.logs-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-controls {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-dropdown {
|
||||||
|
flex: 1;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
260
src/components/AssignmentLogs.jsx
Normal file
260
src/components/AssignmentLogs.jsx
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { Clock, TrendingUp, TrendingDown, Minus, ArrowUpDown } from 'lucide-react';
|
||||||
|
import { api } from '../services/api';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import './AssignmentLogs.css';
|
||||||
|
|
||||||
|
export const AssignmentLogs = () => {
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [logs, setLogs] = useState([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [sortBy, setSortBy] = useState('due-newest');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLogs();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchLogs = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const result = await api.getGrades(user.username);
|
||||||
|
|
||||||
|
// Transform classes into logs
|
||||||
|
const allLogs = [];
|
||||||
|
result.user.classes.forEach(cls => {
|
||||||
|
cls.assignments.forEach(assignment => {
|
||||||
|
allLogs.push({
|
||||||
|
className: cls.className,
|
||||||
|
assignmentName: assignment.name,
|
||||||
|
category: cls.category,
|
||||||
|
period: cls.period,
|
||||||
|
newGrade: assignment.score,
|
||||||
|
gradeChange: 0,
|
||||||
|
timestamp: assignment.createdAt,
|
||||||
|
dueDate: assignment.dueDate,
|
||||||
|
classId: cls.id,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setLogs(allLogs);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortLogs = (logsToSort, sortType) => {
|
||||||
|
const sorted = [...logsToSort];
|
||||||
|
|
||||||
|
switch(sortType) {
|
||||||
|
case 'due-newest':
|
||||||
|
sorted.sort((a, b) => new Date(b.dueDate) - new Date(a.dueDate));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'due-oldest':
|
||||||
|
sorted.sort((a, b) => new Date(a.dueDate) - new Date(b.dueDate));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'time-newest':
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
const timeDiff = new Date(b.timestamp) - new Date(a.timestamp);
|
||||||
|
// Tie-break with due date
|
||||||
|
if (timeDiff === 0) {
|
||||||
|
return new Date(b.dueDate) - new Date(a.dueDate);
|
||||||
|
}
|
||||||
|
return timeDiff;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'time-oldest':
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
const timeDiff = new Date(a.timestamp) - new Date(b.timestamp);
|
||||||
|
// Tie-break with due date
|
||||||
|
if (timeDiff === 0) {
|
||||||
|
return new Date(a.dueDate) - new Date(b.dueDate);
|
||||||
|
}
|
||||||
|
return timeDiff;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'class-az':
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
const classCompare = a.className.localeCompare(b.className);
|
||||||
|
// Within same class, sort assignments A-Z
|
||||||
|
if (classCompare === 0) {
|
||||||
|
return a.assignmentName.localeCompare(b.assignmentName);
|
||||||
|
}
|
||||||
|
return classCompare;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'class-za':
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
const classCompare = b.className.localeCompare(a.className);
|
||||||
|
// Within same class, sort assignments Z-A
|
||||||
|
if (classCompare === 0) {
|
||||||
|
return b.assignmentName.localeCompare(a.assignmentName);
|
||||||
|
}
|
||||||
|
return classCompare;
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'assignment-az':
|
||||||
|
sorted.sort((a, b) => a.assignmentName.localeCompare(b.assignmentName));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'assignment-za':
|
||||||
|
sorted.sort((a, b) => b.assignmentName.localeCompare(a.assignmentName));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateGradeChanges = (sortedLogs) => {
|
||||||
|
return sortedLogs.map((log, index) => {
|
||||||
|
// Look for an older version of the same assignment
|
||||||
|
const olderVersion = sortedLogs.slice(index + 1).find(
|
||||||
|
oldLog =>
|
||||||
|
oldLog.assignmentName === log.assignmentName &&
|
||||||
|
oldLog.className === log.className &&
|
||||||
|
oldLog.category === log.category
|
||||||
|
);
|
||||||
|
|
||||||
|
if (olderVersion) {
|
||||||
|
// Parse scores (remove % if present)
|
||||||
|
const newScore = parseFloat(log.newGrade.toString().replace('%', ''));
|
||||||
|
const oldScore = parseFloat(olderVersion.newGrade.toString().replace('%', ''));
|
||||||
|
return { ...log, gradeChange: newScore - oldScore };
|
||||||
|
}
|
||||||
|
|
||||||
|
return log;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (timestamp) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return date.toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChangeIcon = (change) => {
|
||||||
|
if (!change) return <Minus size={16} />;
|
||||||
|
if (change > 0) return <TrendingUp size={16} />;
|
||||||
|
return <TrendingDown size={16} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getChangeClass = (change) => {
|
||||||
|
if (!change) return 'change-none';
|
||||||
|
if (change > 0) return 'change-positive';
|
||||||
|
return 'change-negative';
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="logs-loading">
|
||||||
|
<div className="spinner"></div>
|
||||||
|
<p>Loading assignment logs...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="logs-error">
|
||||||
|
<p>Error loading logs: {error}</p>
|
||||||
|
<button onClick={fetchLogs} className="retry-btn">Retry</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedLogs = sortLogs(logs, sortBy);
|
||||||
|
const processedLogs = calculateGradeChanges(sortedLogs);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="logs-container">
|
||||||
|
<div className="logs-header">
|
||||||
|
<div>
|
||||||
|
<h2>Assignment Logs</h2>
|
||||||
|
<p className="logs-subtitle">Track all grade updates and changes</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sort-controls">
|
||||||
|
<ArrowUpDown size={18} />
|
||||||
|
<select
|
||||||
|
value={sortBy}
|
||||||
|
onChange={(e) => setSortBy(e.target.value)}
|
||||||
|
className="sort-dropdown"
|
||||||
|
>
|
||||||
|
<option value="due-newest">Due Date (Newest First)</option>
|
||||||
|
<option value="due-oldest">Due Date (Oldest First)</option>
|
||||||
|
<option value="time-newest">Time Added (Newest First)</option>
|
||||||
|
<option value="time-oldest">Time Added (Oldest First)</option>
|
||||||
|
<option value="class-az">Class Name (A-Z)</option>
|
||||||
|
<option value="class-za">Class Name (Z-A)</option>
|
||||||
|
<option value="assignment-az">Assignment Name (A-Z)</option>
|
||||||
|
<option value="assignment-za">Assignment Name (Z-A)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{processedLogs.length === 0 ? (
|
||||||
|
<div className="logs-empty">
|
||||||
|
<Clock size={48} />
|
||||||
|
<p>No assignment logs found</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="logs-list">
|
||||||
|
{processedLogs.map((log, idx) => (
|
||||||
|
<div key={idx} className="log-card">
|
||||||
|
<div className="log-main">
|
||||||
|
<div className="log-info">
|
||||||
|
<div className="log-class">{log.className}</div>
|
||||||
|
<div className="log-assignment">{log.assignmentName}</div>
|
||||||
|
<div className="log-meta">
|
||||||
|
<span className="log-category">{log.category}</span>
|
||||||
|
<span className="log-separator">•</span>
|
||||||
|
<span className="log-period">{log.period}</span>
|
||||||
|
{log.dueDate && (
|
||||||
|
<>
|
||||||
|
<span className="log-separator">•</span>
|
||||||
|
<span className="log-due-date">Due: {log.dueDate}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="log-grade-info">
|
||||||
|
<div className="log-grade">{log.newGrade}</div>
|
||||||
|
{log.gradeChange !== 0 && (
|
||||||
|
<div className={`log-change ${getChangeClass(log.gradeChange)}`}>
|
||||||
|
{getChangeIcon(log.gradeChange)}
|
||||||
|
<span>{Math.abs(log.gradeChange).toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="log-footer">
|
||||||
|
<Clock size={14} />
|
||||||
|
<span className="log-timestamp">{formatDate(log.timestamp)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -53,21 +53,6 @@ export const ClassDetail = ({ classData, onClose }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action buttons */}
|
|
||||||
<div className="action-buttons">
|
|
||||||
<button className="action-btn">
|
|
||||||
<Calculator size={16} />
|
|
||||||
Calculations
|
|
||||||
</button>
|
|
||||||
<button className="action-btn">
|
|
||||||
<Scale size={16} />
|
|
||||||
Grading Scale
|
|
||||||
</button>
|
|
||||||
<button className="action-btn">
|
|
||||||
<Printer size={16} />
|
|
||||||
Print
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Assignments Table */}
|
{/* Assignments Table */}
|
||||||
|
|||||||
41
src/components/Metrics.css
Normal file
41
src/components/Metrics.css
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
.metrics-container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-header h2 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 700;
|
||||||
|
background: linear-gradient(to right, #a78bfa, #ec4899);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-subtitle {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-placeholder {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 6rem 2rem;
|
||||||
|
background-color: #2b2b2b;
|
||||||
|
border: 2px dashed #374151;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: #9ca3af;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-icon {
|
||||||
|
font-size: 4rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
17
src/components/Metrics.jsx
Normal file
17
src/components/Metrics.jsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import './Metrics.css';
|
||||||
|
|
||||||
|
export const Metrics = () => {
|
||||||
|
return (
|
||||||
|
<div className="metrics-container">
|
||||||
|
<div className="metrics-header">
|
||||||
|
<h2>Metrics</h2>
|
||||||
|
<p className="metrics-subtitle">Coming soon...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="metrics-placeholder">
|
||||||
|
<div className="placeholder-icon">📊</div>
|
||||||
|
<p>Grade analytics and insights will appear here</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
130
src/index.css
130
src/index.css
@@ -1,4 +1,4 @@
|
|||||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap");
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: "JetBrains Mono", monospace;
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
@@ -16,7 +16,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
min-height: 10a0vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
@@ -37,3 +37,127 @@ body {
|
|||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: #5a5a5a;
|
background: #5a5a5a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsiveness */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.app-container {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grades-table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grades-table {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grades-table th,
|
||||||
|
.grades-table td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-class {
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-category {
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-name {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-period,
|
||||||
|
.class-teacher {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-btn {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
max-width: 95vw;
|
||||||
|
max-height: 95vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-name {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-grade {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignments-table {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignments-table th,
|
||||||
|
.assignments-table td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-card {
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auth-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.app-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grades-table {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-category {
|
||||||
|
min-width: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grade-btn {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.final-grade {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-info {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user