// DEPLOY_CONFIG: {"cron": [{"name": "end_of_semester_email_summary", "schedule": "0 9 * * 1-5", "action": "event", "config": {"event_type": "end_of_semester_email_summary"}}], "triggers": []}
import { useMemo, useEffect, useRef, useState } from 'react';
import { useAuth, useCollection, renderChart } from '@deplixo/sdk';
import { CoursesTab } from './components/CoursesTab.jsx';
import { GradesTab } from './components/GradesTab.jsx';
import { DashboardTab } from './components/DashboardTab.jsx';
function App() {
const { user, loading, login, logout } = useAuth();
const [activeTab, setActiveTab] = useState('dashboard');
const tabs = [
{ id: 'dashboard', label: 'Dashboard', icon: '📊' },
{ id: 'courses', label: 'Courses', icon: '📚' },
{ id: 'grades', label: 'Grades', icon: '✏️' },
];
if (loading) {
return (
GradeTrack
Signing you in...
);
}
if (!user) {
return (
🔒 Personal data access
Sign in with Google to continue
Use your Google account to securely access your courses, grades, and GPA dashboard.
);
}
return (
{activeTab === 'dashboard' && }
{activeTab === 'courses' && }
{activeTab === 'grades' && }
);
}
function getCourseDisplayName(course) {
return course?.name || course?.title || 'Untitled Course';
}
function getCategoryEntries(course) {
if (!course) return [];
if (Array.isArray(course.categories) && course.categories.length > 0) return course.categories;
const fallback = [
{ key: 'assignments', label: 'Assignments', weight: course.assignmentWeight ?? course.assignmentsWeight ?? 0 },
{ key: 'midterm', label: 'Midterm', weight: course.midtermWeight ?? 0 },
{ key: 'final', label: 'Final', weight: course.finalWeight ?? 0 },
];
return fallback;
}
function normalizeScoreValue(value) {
const num = Number(value);
if (!Number.isFinite(num)) return null;
return Math.max(0, Math.min(100, num));
}
function calculateCourseGrade(course) {
const categories = getCategoryEntries(course);
const grades = Array.isArray(course?.grades) ? course.grades : Array.isArray(course?.items) ? course.items : [];
if (!categories.length || !grades.length) return null;
let totalWeight = 0;
let weightedSum = 0;
categories.forEach(cat => {
const weight = Number(cat.weight ?? 0);
if (weight <= 0) return;
const catGrades = grades.filter(g => (g.category || g.type || g.bucket || '').toLowerCase() === String(cat.key || cat.label || '').toLowerCase());
if (!catGrades.length) return;
const avg = catGrades
.map(g => normalizeScoreValue(g.score ?? g.value ?? g.percentage))
.filter(v => v !== null)
.reduce((a, b) => a + b, 0) / catGrades.length;
if (Number.isFinite(avg)) {
weightedSum += avg * weight;
totalWeight += weight;
}
});
if (!totalWeight) return null;
return weightedSum / totalWeight;
}
function calculateGpaFromPercentage(pct) {
if (!Number.isFinite(pct)) return null;
if (pct >= 93) return 4.0;
if (pct >= 90) return 3.7;
if (pct >= 87) return 3.3;
if (pct >= 83) return 3.0;
if (pct >= 80) return 2.7;
if (pct >= 77) return 2.3;
if (pct >= 73) return 2.0;
if (pct >= 70) return 1.7;
if (pct >= 67) return 1.3;
if (pct >= 65) return 1.0;
return 0;
}
function getSemesterLabel(course, index) {
return (
course?.semester ||
course?.term ||
course?.period ||
course?.session ||
`Semester ${index + 1}`
);
}
function DashboardCharts() {
const { items: courses = [] } = useCollection('courses', { personal: true });
const { items: grades = [] } = useCollection('grades', { personal: true });
const semesterChartRef = useRef(null);
const distributionChartRef = useRef(null);
const semesterData = useMemo(() => {
const courseGradeEntries = courses
.map(course => ({
course,
percentage: calculateCourseGrade({ ...course, grades: grades.filter(g => String(g.courseId ?? g.course_id ?? g.course ?? '') === String(course.id ?? course._id)) }),
}))
.filter(entry => Number.isFinite(entry.percentage));
const semesterMap = new Map();
courseGradeEntries.forEach((entry, index) => {
const label = getSemesterLabel(entry.course, index);
const gpa = calculateGpaFromPercentage(entry.percentage);
if (!semesterMap.has(label)) {
semesterMap.set(label, { totalGpa: 0, count: 0 });
}
const bucket = semesterMap.get(label);
bucket.totalGpa += gpa ?? 0;
bucket.count += 1;
});
return Array.from(semesterMap.entries()).map(([label, bucket]) => ({
label,
value: bucket.count ? Number((bucket.totalGpa / bucket.count).toFixed(2)) : 0,
}));
}, [courses, grades]);
const distributionData = useMemo(() => {
return courses.map((course, index) => {
const courseGrades = grades.filter(g => String(g.courseId ?? g.course_id ?? g.course ?? '') === String(course.id ?? course._id));
const percentage = calculateCourseGrade({ ...course, grades: courseGrades });
return {
label: getCourseDisplayName(course),
value: Number.isFinite(percentage) ? Number(percentage.toFixed(1)) : 0,
order: index,
};
});
}, [courses, grades]);
useEffect(() => {
if (semesterChartRef.current && semesterData.length > 0) {
renderChart(semesterChartRef.current, {
type: 'line',
data: {
labels: semesterData.map(d => d.label),
datasets: [{ label: 'GPA', data: semesterData.map(d => d.value) }],
},
});
}
}, [semesterData]);
useEffect(() => {
if (distributionChartRef.current && distributionData.length > 0) {
renderChart(distributionChartRef.current, {
type: 'bar',
data: {
labels: distributionData.map(d => d.label),
datasets: [{ label: 'Course %', data: distributionData.map(d => d.value) }],
},
});
}
}, [distributionData]);
return (
GPA Over Semesters
{semesterData.length > 0 ? (
) : (
Add courses and grades to see your GPA trend by semester.
)}
Grade Distribution by Course
{distributionData.length > 0 ? (
) : (
No course grades yet. Log grades to view per-course distribution.
)}
);
}
ReactDOM.createRoot(document.getElementById('root')).render();