Fixed assignment logs page to be flatter

This commit is contained in:
2025-12-22 19:39:58 -06:00
parent 0d7e42d136
commit 607d00c908
2 changed files with 321 additions and 197 deletions

View File

@@ -1,10 +1,14 @@
.logs-container { .logs-container {
max-width: 1200px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
} }
.logs-header { .logs-header {
margin-bottom: 2rem; margin-bottom: 1.5rem;
display: flex;
justify-content: space-between;
align-items: start;
gap: 2rem;
} }
.logs-header h2 { .logs-header h2 {
@@ -44,7 +48,9 @@
} }
@keyframes spin { @keyframes spin {
to { transform: rotate(360deg); } to {
transform: rotate(360deg);
}
} }
.retry-btn { .retry-btn {
@@ -62,154 +68,6 @@
background-color: #6d28d9; 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 { .sort-controls {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -239,11 +97,180 @@
box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1); box-shadow: 0 0 0 3px rgba(124, 58, 237, 0.1);
} }
.log-due-date { /* Table-like list */
color: #fbbf24; .logs-list {
font-weight: 500; background-color: #2b2b2b;
border: 1px solid #374151;
border-radius: 0.5rem;
overflow: hidden;
} }
.log-card {
display: grid;
grid-template-columns: 1.5fr 2.5fr 0.8fr 1fr 0.8fr 1fr;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
border-bottom: 1px solid #374151;
transition: background-color 0.15s;
min-height: 50px;
}
.log-card:last-child {
border-bottom: none;
}
.log-card:hover {
background-color: rgba(124, 58, 237, 0.05);
}
.log-class {
color: #d8b4fe;
font-weight: 600;
font-size: 0.875rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.log-assignment {
color: #e0e0e0;
font-size: 0.875rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.log-meta {
display: flex;
align-items: center;
gap: 0.25rem;
}
.log-category {
background-color: rgba(124, 58, 237, 0.2);
color: #c4b5fd;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.7rem;
white-space: nowrap;
}
.log-due-date {
color: #fbbf24;
font-size: 0.75rem;
white-space: nowrap;
}
.log-grade {
font-size: 1.125rem;
font-weight: 700;
color: #4ade80;
text-align: right;
}
.log-change-timestamp {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
}
.log-change {
display: flex;
align-items: center;
gap: 0.25rem;
font-size: 0.75rem;
font-weight: 600;
}
.change-positive {
color: #4ade80;
}
.change-negative {
color: #f87171;
}
.change-none {
color: #9ca3af;
}
.log-timestamp {
color: #6b7280;
font-size: 0.7rem;
display: flex;
align-items: center;
gap: 0.25rem;
white-space: nowrap;
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 1rem;
margin-top: 1.5rem;
padding: 1rem;
}
.pagination-info {
color: #9ca3af;
font-size: 0.875rem;
font-family: "JetBrains Mono", monospace;
}
.pagination-buttons {
display: flex;
align-items: center;
gap: 0.75rem;
}
.pagination-btn {
background-color: #2b2b2b;
border: 1px solid #374151;
color: #e0e0e0;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
font-family: "JetBrains Mono", monospace;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.pagination-btn:hover:not(:disabled) {
border-color: #7c3aed;
background-color: #323232;
}
.pagination-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
/* Tablet */
@media (max-width: 1024px) {
.log-card {
grid-template-columns: 1.2fr 2fr 0.7fr 0.8fr 0.7fr 1fr;
gap: 0.75rem;
padding: 0.65rem 0.75rem;
}
.log-class,
.log-assignment {
font-size: 0.8rem;
}
.log-grade {
font-size: 1rem;
}
}
/* Mobile */
@media (max-width: 768px) { @media (max-width: 768px) {
.logs-header { .logs-header {
flex-direction: column; flex-direction: column;
@@ -258,4 +285,83 @@
flex: 1; flex: 1;
width: 100%; width: 100%;
} }
.log-card {
grid-template-columns: 1fr;
gap: 0.5rem;
padding: 0.875rem;
}
.log-class {
font-size: 0.95rem;
font-weight: 700;
}
.log-assignment {
font-size: 0.85rem;
margin-top: 0.125rem;
}
.log-meta {
margin-top: 0.25rem;
flex-wrap: wrap;
}
.log-due-date {
font-size: 0.75rem;
}
.log-grade {
font-size: 1.25rem;
text-align: left;
margin-top: 0.5rem;
}
.log-change-timestamp {
align-items: flex-start;
margin-top: 0.25rem;
}
.log-timestamp {
font-size: 0.7rem;
}
.pagination {
padding: 0.75rem;
}
.pagination-info {
font-size: 0.75rem;
}
.pagination-btn {
padding: 0.5rem;
}
}
/* Small mobile */
@media (max-width: 480px) {
.logs-header h2 {
font-size: 1.5rem;
}
.log-card {
padding: 0.75rem;
}
.log-class {
font-size: 0.875rem;
}
.log-assignment {
font-size: 0.8rem;
}
.log-category {
font-size: 0.65rem;
}
.log-grade {
font-size: 1.125rem;
}
} }

View File

@@ -1,28 +1,34 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Clock, TrendingUp, TrendingDown, Minus, ArrowUpDown } from 'lucide-react'; import { Clock, TrendingUp, TrendingDown, Minus, ArrowUpDown, ChevronLeft, ChevronRight } from 'lucide-react';
import { api } from '../services/api'; import { api } from '../services/api';
import { useAuth } from '../contexts/AuthContext'; import { useAuth } from '../contexts/AuthContext';
import './AssignmentLogs.css'; import './AssignmentLogs.css';
const ITEMS_PER_PAGE = 20;
export const AssignmentLogs = () => { export const AssignmentLogs = () => {
const { user } = useAuth(); const { user } = useAuth();
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [sortBy, setSortBy] = useState('due-newest'); const [sortBy, setSortBy] = useState('due-newest');
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => { useEffect(() => {
fetchLogs(); fetchLogs();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => {
setCurrentPage(1); // Reset to page 1 when sort changes
}, [sortBy]);
const fetchLogs = async () => { const fetchLogs = async () => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const result = await api.getGrades(user.username); const result = await api.getGrades(user.username);
// Transform classes into logs
const allLogs = []; const allLogs = [];
result.user.classes.forEach(cls => { result.user.classes.forEach(cls => {
cls.assignments.forEach(assignment => { cls.assignments.forEach(assignment => {
@@ -63,7 +69,6 @@ export const AssignmentLogs = () => {
case 'time-newest': case 'time-newest':
sorted.sort((a, b) => { sorted.sort((a, b) => {
const timeDiff = new Date(b.timestamp) - new Date(a.timestamp); const timeDiff = new Date(b.timestamp) - new Date(a.timestamp);
// Tie-break with due date
if (timeDiff === 0) { if (timeDiff === 0) {
return new Date(b.dueDate) - new Date(a.dueDate); return new Date(b.dueDate) - new Date(a.dueDate);
} }
@@ -74,7 +79,6 @@ export const AssignmentLogs = () => {
case 'time-oldest': case 'time-oldest':
sorted.sort((a, b) => { sorted.sort((a, b) => {
const timeDiff = new Date(a.timestamp) - new Date(b.timestamp); const timeDiff = new Date(a.timestamp) - new Date(b.timestamp);
// Tie-break with due date
if (timeDiff === 0) { if (timeDiff === 0) {
return new Date(a.dueDate) - new Date(b.dueDate); return new Date(a.dueDate) - new Date(b.dueDate);
} }
@@ -85,7 +89,6 @@ export const AssignmentLogs = () => {
case 'class-az': case 'class-az':
sorted.sort((a, b) => { sorted.sort((a, b) => {
const classCompare = a.className.localeCompare(b.className); const classCompare = a.className.localeCompare(b.className);
// Within same class, sort assignments A-Z
if (classCompare === 0) { if (classCompare === 0) {
return a.assignmentName.localeCompare(b.assignmentName); return a.assignmentName.localeCompare(b.assignmentName);
} }
@@ -96,7 +99,6 @@ export const AssignmentLogs = () => {
case 'class-za': case 'class-za':
sorted.sort((a, b) => { sorted.sort((a, b) => {
const classCompare = b.className.localeCompare(a.className); const classCompare = b.className.localeCompare(a.className);
// Within same class, sort assignments Z-A
if (classCompare === 0) { if (classCompare === 0) {
return b.assignmentName.localeCompare(a.assignmentName); return b.assignmentName.localeCompare(a.assignmentName);
} }
@@ -121,7 +123,6 @@ export const AssignmentLogs = () => {
const calculateGradeChanges = (sortedLogs) => { const calculateGradeChanges = (sortedLogs) => {
return sortedLogs.map((log, index) => { return sortedLogs.map((log, index) => {
// Look for an older version of the same assignment
const olderVersion = sortedLogs.slice(index + 1).find( const olderVersion = sortedLogs.slice(index + 1).find(
oldLog => oldLog =>
oldLog.assignmentName === log.assignmentName && oldLog.assignmentName === log.assignmentName &&
@@ -130,7 +131,6 @@ export const AssignmentLogs = () => {
); );
if (olderVersion) { if (olderVersion) {
// Parse scores (remove % if present)
const newScore = parseFloat(log.newGrade.toString().replace('%', '')); const newScore = parseFloat(log.newGrade.toString().replace('%', ''));
const oldScore = parseFloat(olderVersion.newGrade.toString().replace('%', '')); const oldScore = parseFloat(olderVersion.newGrade.toString().replace('%', ''));
return { ...log, gradeChange: newScore - oldScore }; return { ...log, gradeChange: newScore - oldScore };
@@ -145,7 +145,6 @@ export const AssignmentLogs = () => {
return date.toLocaleString('en-US', { return date.toLocaleString('en-US', {
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
year: 'numeric',
hour: 'numeric', hour: 'numeric',
minute: '2-digit', minute: '2-digit',
hour12: true hour12: true
@@ -153,9 +152,9 @@ export const AssignmentLogs = () => {
}; };
const getChangeIcon = (change) => { const getChangeIcon = (change) => {
if (!change) return <Minus size={16} />; if (!change) return <Minus size={14} />;
if (change > 0) return <TrendingUp size={16} />; if (change > 0) return <TrendingUp size={14} />;
return <TrendingDown size={16} />; return <TrendingDown size={14} />;
}; };
const getChangeClass = (change) => { const getChangeClass = (change) => {
@@ -185,12 +184,18 @@ export const AssignmentLogs = () => {
const sortedLogs = sortLogs(logs, sortBy); const sortedLogs = sortLogs(logs, sortBy);
const processedLogs = calculateGradeChanges(sortedLogs); const processedLogs = calculateGradeChanges(sortedLogs);
// Pagination
const totalPages = Math.ceil(processedLogs.length / ITEMS_PER_PAGE);
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
const endIndex = startIndex + ITEMS_PER_PAGE;
const currentLogs = processedLogs.slice(startIndex, endIndex);
return ( return (
<div className="logs-container"> <div className="logs-container">
<div className="logs-header"> <div className="logs-header">
<div> <div>
<h2>Assignment Logs</h2> <h2>Assignment Logs</h2>
<p className="logs-subtitle">Track all grade updates and changes</p> <p className="logs-subtitle">Tracking {processedLogs.length} assignments</p>
</div> </div>
<div className="sort-controls"> <div className="sort-controls">
@@ -218,42 +223,55 @@ export const AssignmentLogs = () => {
<p>No assignment logs found</p> <p>No assignment logs found</p>
</div> </div>
) : ( ) : (
<>
<div className="logs-list"> <div className="logs-list">
{processedLogs.map((log, idx) => ( {currentLogs.map((log, idx) => (
<div key={idx} className="log-card"> <div key={idx} className="log-card">
<div className="log-main">
<div className="log-info">
<div className="log-class">{log.className}</div> <div className="log-class">{log.className}</div>
<div className="log-assignment">{log.assignmentName}</div> <div className="log-assignment">{log.assignmentName}</div>
<div className="log-meta"> <div className="log-meta">
<span className="log-category">{log.category}</span> <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> <div className="log-due-date">{log.dueDate}</div>
<div className="log-grade-info">
<div className="log-grade">{log.newGrade}</div> <div className="log-grade">{log.newGrade}</div>
<div className="log-change-timestamp">
{log.gradeChange !== 0 && ( {log.gradeChange !== 0 && (
<div className={`log-change ${getChangeClass(log.gradeChange)}`}> <div className={`log-change ${getChangeClass(log.gradeChange)}`}>
{getChangeIcon(log.gradeChange)} {getChangeIcon(log.gradeChange)}
<span>{Math.abs(log.gradeChange).toFixed(2)}</span> <span>{Math.abs(log.gradeChange).toFixed(1)}</span>
</div> </div>
)} )}
<div className="log-timestamp">
<Clock size={12} />
{formatDate(log.timestamp)}
</div> </div>
</div> </div>
<div className="log-footer">
<Clock size={14} />
<span className="log-timestamp">{formatDate(log.timestamp)}</span>
</div>
</div> </div>
))} ))}
</div> </div>
<div className="pagination">
<div className="pagination-buttons">
<button
className="pagination-btn"
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
>
<ChevronLeft size={16} />
</button>
<span className="pagination-info">
Page {currentPage} of {totalPages}
</span>
<button
className="pagination-btn"
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
disabled={currentPage === totalPages}
>
<ChevronRight size={16} />
</button>
</div>
</div>
</>
)} )}
</div> </div>
); );