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;
+ }
+}