From 0d7e42d1364ade282a01f11ee249cd7d730c307d Mon Sep 17 00:00:00 2001 From: KeshavAnandCode Date: Mon, 22 Dec 2025 17:28:07 -0600 Subject: [PATCH] Assignment Logs --- src/App.css | 52 ++++++ src/App.jsx | 49 +++++- src/components/AssignmentLogs.css | 261 ++++++++++++++++++++++++++++++ src/components/AssignmentLogs.jsx | 260 +++++++++++++++++++++++++++++ src/components/ClassDetail.jsx | 17 +- src/components/Metrics.css | 41 +++++ src/components/Metrics.jsx | 17 ++ src/index.css | 132 ++++++++++++++- 8 files changed, 801 insertions(+), 28 deletions(-) create mode 100644 src/components/AssignmentLogs.css create mode 100644 src/components/AssignmentLogs.jsx create mode 100644 src/components/Metrics.css create mode 100644 src/components/Metrics.jsx diff --git a/src/App.css b/src/App.css index 8a877a1..f2fd9d0 100644 --- a/src/App.css +++ b/src/App.css @@ -106,3 +106,55 @@ .retry-button:hover { 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; + } +} \ No newline at end of file diff --git a/src/App.jsx b/src/App.jsx index 70fadf8..4ffeda2 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,6 +1,8 @@ import { useState, useEffect } from 'react'; -import { AlertCircle } from 'lucide-react'; +import { AlertCircle, BarChart3, FileText, TableIcon } from 'lucide-react'; import { GradesTable } from './components/GradesTable'; +import { AssignmentLogs } from './components/AssignmentLogs'; +import { Metrics } from './components/Metrics'; import { LoadingSpinner } from './components/LoadingSpinner'; import { AuthPage } from './components/AuthPage'; import { useAuth } from './contexts/AuthContext'; @@ -12,6 +14,7 @@ function App() { const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [currentPage, setCurrentPage] = useState('grades'); useEffect(() => { if (user) { @@ -44,12 +47,12 @@ function App() { } // Show loading spinner while fetching grades - if (loading) { + if (loading && currentPage === 'grades') { return ; } // Show error state - if (error) { + if (error && currentPage === 'grades') { return (
@@ -73,16 +76,46 @@ function App() {

- + {currentPage === 'grades' && ( + + )}
- - + + + +
+ {currentPage === 'grades' && } + {currentPage === 'logs' && } + {currentPage === 'metrics' && } +
); } diff --git a/src/components/AssignmentLogs.css b/src/components/AssignmentLogs.css new file mode 100644 index 0000000..b797b66 --- /dev/null +++ b/src/components/AssignmentLogs.css @@ -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%; + } +} \ No newline at end of file diff --git a/src/components/AssignmentLogs.jsx b/src/components/AssignmentLogs.jsx new file mode 100644 index 0000000..abe4ef3 --- /dev/null +++ b/src/components/AssignmentLogs.jsx @@ -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 ; + if (change > 0) return ; + return ; + }; + + const getChangeClass = (change) => { + if (!change) return 'change-none'; + if (change > 0) return 'change-positive'; + return 'change-negative'; + }; + + if (loading) { + return ( +
+
+

Loading assignment logs...

+
+ ); + } + + if (error) { + return ( +
+

Error loading logs: {error}

+ +
+ ); + } + + const sortedLogs = sortLogs(logs, sortBy); + const processedLogs = calculateGradeChanges(sortedLogs); + + return ( +
+
+
+

Assignment Logs

+

Track all grade updates and changes

+
+ +
+ + +
+
+ + {processedLogs.length === 0 ? ( +
+ +

No assignment logs found

+
+ ) : ( +
+ {processedLogs.map((log, idx) => ( +
+
+
+
{log.className}
+
{log.assignmentName}
+
+ {log.category} + + {log.period} + {log.dueDate && ( + <> + + Due: {log.dueDate} + + )} +
+
+
+
{log.newGrade}
+ {log.gradeChange !== 0 && ( +
+ {getChangeIcon(log.gradeChange)} + {Math.abs(log.gradeChange).toFixed(2)} +
+ )} +
+
+
+ + {formatDate(log.timestamp)} +
+
+ ))} +
+ )} +
+ ); +}; \ No newline at end of file diff --git a/src/components/ClassDetail.jsx b/src/components/ClassDetail.jsx index 2f83a57..90027ff 100644 --- a/src/components/ClassDetail.jsx +++ b/src/components/ClassDetail.jsx @@ -52,22 +52,7 @@ export const ClassDetail = ({ classData, onClose }) => { - - {/* Action buttons */} -
- - - -
+ {/* Assignments Table */} diff --git a/src/components/Metrics.css b/src/components/Metrics.css new file mode 100644 index 0000000..b42e267 --- /dev/null +++ b/src/components/Metrics.css @@ -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; +} \ No newline at end of file diff --git a/src/components/Metrics.jsx b/src/components/Metrics.jsx new file mode 100644 index 0000000..f517398 --- /dev/null +++ b/src/components/Metrics.jsx @@ -0,0 +1,17 @@ +import './Metrics.css'; + +export const Metrics = () => { + return ( +
+
+

Metrics

+

Coming soon...

+
+ +
+
📊
+

Grade analytics and insights will appear here

+
+
+ ); +}; \ No newline at end of file diff --git a/src/index.css b/src/index.css index c3030a2..afb9acb 100644 --- a/src/index.css +++ b/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; @@ -7,7 +7,7 @@ } body { - font-family: 'JetBrains Mono', monospace; + font-family: "JetBrains Mono", monospace; background-color: #1e1e1e; color: #e0e0e0; line-height: 1.6; @@ -16,7 +16,7 @@ body { } #root { - min-height: 10a0vh; + min-height: 100vh; } /* Custom scrollbar */ @@ -36,4 +36,128 @@ body { ::-webkit-scrollbar-thumb:hover { background: #5a5a5a; -} \ No newline at end of file +} + +/* 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; + } +}