From d10f7f3173a631d5138bfc6d769a940312c9c7b9 Mon Sep 17 00:00:00 2001 From: KeshavAnandCode Date: Mon, 22 Dec 2025 12:51:01 -0600 Subject: [PATCH] Attempted working dashbaord tablepage --- bun.lock | 6 + package.json | 2 + src/App.css | 112 ++++++++++++---- src/App.jsx | 90 +++++++++---- src/components/ClassDetail.css | 213 ++++++++++++++++++++++++++++++ src/components/ClassDetail.jsx | 149 +++++++++++++++++++++ src/components/GradesTable.css | 140 ++++++++++++++++++++ src/components/GradesTable.jsx | 134 +++++++++++++++++++ src/components/LoadingSpinner.jsx | 12 ++ src/index.css | 79 ++++------- src/main.jsx | 16 +-- src/services/api.js | 35 +++++ 12 files changed, 876 insertions(+), 112 deletions(-) create mode 100644 src/components/ClassDetail.css create mode 100644 src/components/ClassDetail.jsx create mode 100644 src/components/GradesTable.css create mode 100644 src/components/GradesTable.jsx create mode 100644 src/components/LoadingSpinner.jsx create mode 100644 src/services/api.js diff --git a/bun.lock b/bun.lock index 06203fb..806a6c4 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index 5f0947b..cbc3c1c 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/App.css b/src/App.css index b9d355d..8a877a1 100644 --- a/src/App.css +++ b/src/App.css @@ -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; } diff --git a/src/App.jsx b/src/App.jsx index f67355a..d77753d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -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 ; + } + + if (error) { + return ( +
+
+ +

Error: {error}

+ +
+
+ ); + } return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.jsx and save to test HMR -

-

- Click on the Vite and React logos to learn more -

- - ) + + + + ); } -export default App +export default App; \ No newline at end of file diff --git a/src/components/ClassDetail.css b/src/components/ClassDetail.css new file mode 100644 index 0000000..f61cbc9 --- /dev/null +++ b/src/components/ClassDetail.css @@ -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; +} diff --git a/src/components/ClassDetail.jsx b/src/components/ClassDetail.jsx new file mode 100644 index 0000000..2f83a57 --- /dev/null +++ b/src/components/ClassDetail.jsx @@ -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 ( +
+
e.stopPropagation()}> + {/* Header */} +
+
+
+

{classData.className}

+
+ {classData.teacher} + + {classData.period} + + {classData.category} +
+
+
+
+ {classData.finalGrades?.[0]?.grade || 'N/A'} +
+
+
+ + {/* Action buttons */} +
+ + + +
+
+ + {/* Assignments Table */} +
+ + + + + + + + + + + {/* Major Grades */} + {majorGrades.length > 0 && ( + <> + + + + + + {majorGrades.map((assignment, idx) => ( + + + + + + + ))} + + )} + + {/* Minor Grades */} + {minorGrades.length > 0 && ( + <> + + + + + + {minorGrades.map((assignment, idx) => ( + + + + + + + ))} + + )} + +
DescriptionDue DateScoreAttempts
Major Grades{calculateAverage(majorGrades)}
+ {hasAlert(assignment.score) && ( + + )} + {assignment.name} + {assignment.dueDate} + {assignment.score} + {assignment.attempts}
Minor Grades{calculateAverage(minorGrades)}
+ {hasAlert(assignment.score) && ( + + )} + {assignment.name} + {assignment.dueDate} + {assignment.score} + {assignment.attempts}
+
+ + {/* Footer */} +
+ +
+
+
+ ); +}; \ No newline at end of file diff --git a/src/components/GradesTable.css b/src/components/GradesTable.css new file mode 100644 index 0000000..f51f70b --- /dev/null +++ b/src/components/GradesTable.css @@ -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; +} diff --git a/src/components/GradesTable.jsx b/src/components/GradesTable.jsx new file mode 100644 index 0000000..297ab69 --- /dev/null +++ b/src/components/GradesTable.jsx @@ -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 ( + <> +
+ + + + + + {CATEGORIES.map(cat => ( + + ))} + + + + {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) => ( + + + + {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 ( + + ); + })} + + ))} + +
ClassMissing
Assignments
{cat}
+
{cls.className}
+
{cls.period}
+
{cls.teacher}
+
+ {displayGrade ? ( + + ) : ( + + )} +
+
+ + {selectedClass && ( + setSelectedClass(null)} + /> + )} + + ); +}; \ No newline at end of file diff --git a/src/components/LoadingSpinner.jsx b/src/components/LoadingSpinner.jsx new file mode 100644 index 0000000..9361f0d --- /dev/null +++ b/src/components/LoadingSpinner.jsx @@ -0,0 +1,12 @@ +import { Loader2 } from 'lucide-react'; + +export const LoadingSpinner = () => { + return ( +
+
+ +

Loading grades...

+
+
+ ); +}; \ No newline at end of file diff --git a/src/index.css b/src/index.css index 08a3ac9..0844130 100644 --- a/src/index.css +++ b/src/index.css @@ -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; +} \ No newline at end of file diff --git a/src/main.jsx b/src/main.jsx index b9a1a6d..ecf2cca 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -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( - +ReactDOM.createRoot(document.getElementById('root')).render( + - , -) + , +); \ No newline at end of file diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 0000000..48822ef --- /dev/null +++ b/src/services/api.js @@ -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(); + } +}; \ No newline at end of file