Attempted working dashbaord tablepage

This commit is contained in:
2025-12-22 12:51:01 -06:00
parent 14091f1f25
commit d10f7f3173
12 changed files with 876 additions and 112 deletions

View File

@@ -5,6 +5,8 @@
"": {
"name": "spaceward-frontend",
"dependencies": {
"i": "^0.3.7",
"lucide-react": "^0.562.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
},
@@ -310,6 +312,8 @@
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
"i": ["i@0.3.7", "", {}, "sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
@@ -346,6 +350,8 @@
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.562.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],

View File

@@ -10,6 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"i": "^0.3.7",
"lucide-react": "^0.562.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},

View File

@@ -1,24 +1,73 @@
#root {
max-width: 1280px;
margin: 0 auto;
.app-container {
min-height: 100vh;
background-color: #1e1e1e;
padding: 2rem;
}
.app-header {
max-width: 1400px;
margin: 0 auto 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.app-title {
font-size: 2.5rem;
font-weight: 700;
background: linear-gradient(to right, #a78bfa, #ec4899);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.app-subtitle {
color: #9ca3af;
font-size: 0.875rem;
margin-top: 0.5rem;
}
.username-highlight {
color: #a78bfa;
}
.refresh-button {
background-color: #7c3aed;
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 0.375rem;
font-family: "JetBrains Mono", monospace;
font-size: 0.875rem;
cursor: pointer;
transition: background-color 0.2s;
}
.refresh-button:hover {
background-color: #6d28d9;
}
.loading-container,
.error-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: #1e1e1e;
}
.loading-content,
.error-content {
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
.loading-spinner {
animation: spin 1s linear infinite;
color: #a78bfa;
margin: 0 auto 1rem;
}
@keyframes logo-spin {
@keyframes spin {
from {
transform: rotate(0deg);
}
@@ -27,16 +76,33 @@
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
.loading-text,
.error-text {
color: #9ca3af;
font-family: "JetBrains Mono", monospace;
}
.card {
padding: 2em;
.error-icon {
color: #ef4444;
margin: 0 auto 1rem;
}
.read-the-docs {
color: #888;
.error-message {
color: #f87171;
margin-bottom: 1rem;
}
.retry-button {
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-button:hover {
background-color: #6d28d9;
}

View File

@@ -1,35 +1,71 @@
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'
import { useState, useEffect } from 'react';
import { AlertCircle } from 'lucide-react';
import { GradesTable } from './components/GradesTable';
import { LoadingSpinner } from './components/LoadingSpinner';
import { api } from './services/api';
import './App.css';
function App() {
const [count, setCount] = useState(0)
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [username] = useState(
import.meta.env.VITE_DEFAULT_USERNAME || 'keshav.anand.1'
);
useEffect(() => {
fetchGrades();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [username]);
const fetchGrades = async () => {
setLoading(true);
setError(null);
try {
const result = await api.getGrades(username);
setData(result.user);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (loading) {
return <LoadingSpinner />;
}
if (error) {
return (
<div className="error-container">
<div className="error-content">
<AlertCircle className="error-icon" size={48} />
<p className="error-message">Error: {error}</p>
<button onClick={fetchGrades} className="retry-button">
Retry
</button>
</div>
</div>
);
}
return (
<>
<div>
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
</div>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
<div className="app-container">
<div className="app-header">
<div>
<h1 className="app-title">STUDENT GRADES</h1>
<p className="app-subtitle">
Logged in as: <span className="username-highlight">{username}</span>
</p>
</div>
<button onClick={fetchGrades} className="refresh-button">
Refresh
</button>
<p>
Edit <code>src/App.jsx</code> and save to test HMR
</p>
</div>
<p className="read-the-docs">
Click on the Vite and React logos to learn more
</p>
</>
)
<GradesTable data={data} />
</div>
);
}
export default App
export default App;

View File

@@ -0,0 +1,213 @@
.modal-overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
padding: 1rem;
z-index: 50;
}
.modal-content {
background-color: #1e1e1e;
border-radius: 0.5rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
max-width: 1200px;
width: 100%;
max-height: 90vh;
overflow: hidden;
border: 1px solid #374151;
}
.modal-header {
background: linear-gradient(to right, #581c87, #4338ca);
color: white;
padding: 1.5rem;
border-bottom: 1px solid #374151;
}
.header-content {
display: flex;
justify-content: space-between;
align-items: start;
}
.class-name {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.class-info {
display: flex;
align-items: center;
gap: 0.75rem;
color: #ddd6fe;
font-size: 0.875rem;
}
.category-badge {
padding: 0.25rem 0.5rem;
background-color: rgba(139, 92, 246, 0.3);
border-radius: 0.25rem;
}
.final-grade {
font-size: 4rem;
font-weight: 700;
background: linear-gradient(to right, #4ade80, #3b82f6);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1;
}
.action-buttons {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.action-btn {
background-color: rgba(255, 255, 255, 0.1);
color: white;
padding: 0.5rem 1rem;
border: none;
border-radius: 0.375rem;
display: flex;
align-items: center;
gap: 0.5rem;
font-family: "JetBrains Mono", monospace;
font-size: 0.875rem;
cursor: pointer;
transition: background-color 0.2s;
}
.action-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.assignments-container {
overflow-y: auto;
max-height: calc(90vh - 250px);
background-color: #2b2b2b;
}
.assignments-table {
width: 100%;
font-size: 0.875rem;
border-collapse: collapse;
}
.assignments-table thead {
background-color: #1e1e1e;
position: sticky;
top: 0;
z-index: 10;
}
.assignments-table th {
text-align: left;
padding: 0.75rem;
font-weight: 600;
color: #d1d5db;
border-bottom: 1px solid #374151;
}
.section-header {
font-weight: 600;
}
.section-header.major {
background-color: rgba(234, 88, 12, 0.2);
color: #fdba74;
}
.section-header.minor {
background-color: rgba(234, 179, 8, 0.2);
color: #fde047;
}
.section-header td {
padding: 0.75rem;
}
.assignment-row {
border-bottom: 1px solid #1f2937;
transition: background-color 0.2s;
}
.assignment-row:hover {
background-color: #323232;
}
.assignment-row td {
padding: 0.75rem;
}
.assignment-name {
display: flex;
align-items: center;
gap: 0.5rem;
color: #d8b4fe;
}
.alert-icon {
color: #f87171;
flex-shrink: 0;
}
.due-date {
color: #9ca3af;
}
.score {
font-weight: 700;
}
.score-excellent {
color: #4ade80;
}
.score-good {
color: #60a5fa;
}
.score-average {
color: #fbbf24;
}
.score-poor {
color: #f87171;
}
.attempts {
text-align: center;
color: #60a5fa;
}
.modal-footer {
background-color: #1e1e1e;
padding: 1rem;
display: flex;
justify-content: flex-end;
border-top: 1px solid #374151;
}
.close-btn {
background-color: #7c3aed;
color: white;
padding: 0.5rem 1.5rem;
border: none;
border-radius: 0.375rem;
font-family: "JetBrains Mono", monospace;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s;
}
.close-btn:hover {
background-color: #6d28d9;
}

View File

@@ -0,0 +1,149 @@
import { Calculator, Scale, Printer, AlertCircle } from 'lucide-react';
import './ClassDetail.css';
export const ClassDetail = ({ classData, onClose }) => {
const majorGrades = classData.assignments.filter(a => a.isMajorGrade);
const minorGrades = classData.assignments.filter(a => !a.isMajorGrade);
const calculateAverage = (grades) => {
if (grades.length === 0) return '0.00%';
const sum = grades.reduce((acc, g) => {
const scoreMatch = g.score.match(/[\d.]+/);
const score = scoreMatch ? parseFloat(scoreMatch[0]) : 0;
return acc + score;
}, 0);
return `${(sum / grades.length).toFixed(2)}%`;
};
const getScoreColor = (score) => {
const scoreMatch = score.match(/[\d.]+/);
const numScore = scoreMatch ? parseFloat(scoreMatch[0]) : 0;
if (numScore >= 90) return 'score-excellent';
if (numScore >= 80) return 'score-good';
if (numScore >= 70) return 'score-average';
return 'score-poor';
};
const hasAlert = (score) => {
const scoreMatch = score.match(/[\d.]+/);
const numScore = scoreMatch ? parseFloat(scoreMatch[0]) : 0;
return numScore < 80;
};
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
{/* Header */}
<div className="modal-header">
<div className="header-content">
<div className="header-left">
<h2 className="class-name">{classData.className}</h2>
<div className="class-info">
<span>{classData.teacher}</span>
<span></span>
<span>{classData.period}</span>
<span></span>
<span className="category-badge">{classData.category}</span>
</div>
</div>
<div className="header-right">
<div className="final-grade">
{classData.finalGrades?.[0]?.grade || 'N/A'}
</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>
{/* Assignments Table */}
<div className="assignments-container">
<table className="assignments-table">
<thead>
<tr>
<th>Description</th>
<th>Due Date</th>
<th>Score</th>
<th>Attempts</th>
</tr>
</thead>
<tbody>
{/* Major Grades */}
{majorGrades.length > 0 && (
<>
<tr className="section-header major">
<td colSpan={2}>Major Grades</td>
<td>{calculateAverage(majorGrades)}</td>
<td></td>
</tr>
{majorGrades.map((assignment, idx) => (
<tr key={idx} className="assignment-row">
<td className="assignment-name">
{hasAlert(assignment.score) && (
<AlertCircle size={16} className="alert-icon" />
)}
<span>{assignment.name}</span>
</td>
<td className="due-date">{assignment.dueDate}</td>
<td className={`score ${getScoreColor(assignment.score)}`}>
{assignment.score}
</td>
<td className="attempts">{assignment.attempts}</td>
</tr>
))}
</>
)}
{/* Minor Grades */}
{minorGrades.length > 0 && (
<>
<tr className="section-header minor">
<td colSpan={2}>Minor Grades</td>
<td>{calculateAverage(minorGrades)}</td>
<td></td>
</tr>
{minorGrades.map((assignment, idx) => (
<tr key={idx} className="assignment-row">
<td className="assignment-name">
{hasAlert(assignment.score) && (
<AlertCircle size={16} className="alert-icon" />
)}
<span>{assignment.name}</span>
</td>
<td className="due-date">{assignment.dueDate}</td>
<td className={`score ${getScoreColor(assignment.score)}`}>
{assignment.score}
</td>
<td className="attempts">{assignment.attempts}</td>
</tr>
))}
</>
)}
</tbody>
</table>
</div>
{/* Footer */}
<div className="modal-footer">
<button onClick={onClose} className="close-btn">
Close
</button>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,140 @@
.grades-table-container {
background-color: #2b2b2b;
border-radius: 0.5rem;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
overflow: hidden;
border: 1px solid #374151;
}
.grades-table {
width: 100%;
font-size: 0.875rem;
border-collapse: collapse;
}
.grades-table thead {
background-color: #1e1e1e;
}
.grades-table th {
padding: 1rem;
font-weight: 600;
color: #d1d5db;
border-bottom: 2px solid #7c3aed;
text-align: center;
}
.header-class {
text-align: left !important;
position: sticky;
left: 0;
background-color: #1e1e1e;
z-index: 10;
}
.header-category {
min-width: 80px;
}
.class-row {
border-bottom: 1px solid #1f2937;
transition: background-color 0.2s;
}
.class-row:hover {
background-color: #323232;
}
.cell-class {
padding: 1rem;
position: sticky;
left: 0;
background-color: #2b2b2b;
z-index: 5;
}
.class-row:hover .cell-class {
background-color: #323232;
}
.class-name {
color: #d8b4fe;
font-weight: 600;
font-size: 1rem;
margin-bottom: 0.25rem;
}
.class-period {
color: #6b7280;
font-size: 0.75rem;
margin-top: 0.25rem;
}
.class-teacher {
color: #9ca3af;
font-size: 0.75rem;
margin-top: 0.125rem;
}
.cell-missing {
text-align: center;
padding: 1rem;
}
.cell-grade {
text-align: center;
padding: 1rem;
}
.grade-btn {
background: none;
border: none;
font-family: "JetBrains Mono", monospace;
font-weight: 700;
font-size: 1.125rem;
cursor: pointer;
padding: 0.5rem 0.75rem;
border-radius: 0.375rem;
transition: all 0.2s;
}
.grade-btn:hover {
transform: scale(1.1);
}
.grade-excellent {
color: #4ade80;
}
.grade-excellent:hover {
background-color: rgba(74, 222, 128, 0.1);
}
.grade-good {
color: #60a5fa;
}
.grade-good:hover {
background-color: rgba(96, 165, 250, 0.1);
}
.grade-average {
color: #fbbf24;
}
.grade-average:hover {
background-color: rgba(251, 191, 36, 0.1);
}
.grade-poor {
color: #f87171;
}
.grade-poor:hover {
background-color: rgba(248, 113, 113, 0.1);
}
.grade-empty {
color: #4b5563;
font-size: 1.5rem;
}

View File

@@ -0,0 +1,134 @@
import { useState } from 'react';
import { ClassDetail } from './ClassDetail';
import './GradesTable.css';
const CATEGORIES = ['NW1', 'NW2', 'SE1', 'S1', 'NW3', 'NW4', 'SE2', 'S2', 'FIN'];
export const GradesTable = ({ data }) => {
const [selectedClass, setSelectedClass] = useState(null);
// Group classes by class name and period
const groupedClasses = data?.classes.reduce((acc, cls) => {
const key = `${cls.className}-${cls.period}`;
if (!acc[key]) {
acc[key] = {
className: cls.className,
period: cls.period,
teacher: cls.teacher,
grades: {},
classObjects: {}
};
}
// Store the grade
const finalGrade = cls.finalGrades?.[0]?.grade || '';
const gradeNum = finalGrade.match(/[\d.]+/)?.[0] || '';
acc[key].grades[cls.category] = gradeNum;
acc[key].classObjects[cls.category] = cls;
return acc;
}, {});
const calculateSemesterGrade = (nw1, nw2, se1) => {
const grades = [
parseFloat(nw1) * 0.4,
parseFloat(nw2) * 0.4,
parseFloat(se1) * 0.2
];
// Check if all grades are valid numbers
if (grades.some(g => isNaN(g))) return null;
const total = grades.reduce((sum, g) => sum + g, 0);
return total.toFixed(2);
};
const getGradeColor = (grade) => {
const numGrade = parseFloat(grade);
if (isNaN(numGrade)) return 'grade-none';
if (numGrade >= 90) return 'grade-excellent';
if (numGrade >= 80) return 'grade-good';
if (numGrade >= 70) return 'grade-average';
return 'grade-poor';
};
return (
<>
<div className="grades-table-container">
<table className="grades-table">
<thead>
<tr>
<th className="header-class">Class</th>
<th className="header-missing">Missing<br/>Assignments</th>
{CATEGORIES.map(cat => (
<th key={cat} className="header-category">{cat}</th>
))}
</tr>
</thead>
<tbody>
{groupedClasses && Object.values(groupedClasses)
.sort((a, b) => {
const periodA = a.period.match(/Period\s+(\d+)/)?.[1];
const periodB = b.period.match(/Period\s+(\d+)/)?.[1];
return (parseInt(periodA) || 0) - (parseInt(periodB) || 0);
})
.map((cls, idx) => (
<tr key={idx} className="class-row">
<td className="cell-class">
<div className="class-name">{cls.className}</div>
<div className="class-period">{cls.period}</div>
<div className="class-teacher">{cls.teacher}</div>
</td>
<td className="cell-missing"></td>
{CATEGORIES.map(cat => {
let displayGrade = cls.grades[cat];
let isCalculated = false;
// Calculate S1 from NW1, NW2, SE1
if (cat === 'S1') {
displayGrade = calculateSemesterGrade(
cls.grades['NW1'],
cls.grades['NW2'],
cls.grades['SE1']
);
isCalculated = true;
}
// Calculate S2 from NW3, NW4, SE2
else if (cat === 'S2') {
displayGrade = calculateSemesterGrade(
cls.grades['NW3'],
cls.grades['NW4'],
cls.grades['SE2']
);
isCalculated = true;
}
return (
<td key={cat} className="cell-grade">
{displayGrade ? (
<button
onClick={() => !isCalculated && setSelectedClass(cls.classObjects[cat])}
className={`grade-btn ${getGradeColor(displayGrade)} ${isCalculated ? 'calculated' : ''}`}
disabled={isCalculated}
>
{displayGrade}
</button>
) : (
<span className="grade-empty"></span>
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
{selectedClass && (
<ClassDetail
classData={selectedClass}
onClose={() => setSelectedClass(null)}
/>
)}
</>
);
};

View File

@@ -0,0 +1,12 @@
import { Loader2 } from 'lucide-react';
export const LoadingSpinner = () => {
return (
<div className="loading-container">
<div className="loading-content">
<Loader2 className="loading-spinner" size={48} />
<p className="loading-text">Loading grades...</p>
</div>
</div>
);
};

View File

@@ -1,68 +1,39 @@
:root {
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap');
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
font-synthesis: none;
text-rendering: optimizeLegibility;
body {
font-family: 'JetBrains Mono', monospace;
background-color: #1e1e1e;
color: #e0e0e0;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
#root {
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
/* Custom scrollbar */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
::-webkit-scrollbar-track {
background: #2b2b2b;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
::-webkit-scrollbar-thumb {
background: #4a4a4a;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #5a5a5a;
}

View File

@@ -1,10 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.jsx';
import './index.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</StrictMode>,
)
</React.StrictMode>,
);

35
src/services/api.js Normal file
View File

@@ -0,0 +1,35 @@
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:4000/api';
export const api = {
async getGrades(username) {
const response = await fetch(`${API_BASE_URL}/users/${username}/grades`);
if (!response.ok) {
throw new Error('Failed to fetch grades');
}
return response.json();
},
async getClasses(username) {
const response = await fetch(`${API_BASE_URL}/users/${username}/classes`);
if (!response.ok) {
throw new Error('Failed to fetch classes');
}
return response.json();
},
async getFinalGrades(username) {
const response = await fetch(`${API_BASE_URL}/users/${username}/final-grades`);
if (!response.ok) {
throw new Error('Failed to fetch final grades');
}
return response.json();
},
async getStats(username) {
const response = await fetch(`${API_BASE_URL}/stats/users/${username}`);
if (!response.ok) {
throw new Error('Failed to fetch stats');
}
return response.json();
}
};