64 lines
2.9 KiB
TypeScript
64 lines
2.9 KiB
TypeScript
import { useEffect, useMemo, useState } from "react";
|
|
import { Link as RouterLink } from "react-router-dom";
|
|
import { Box, Button, Card, CardContent, Chip, LinearProgress, Stack, Typography } from "@mui/material";
|
|
import { listCourses, enroll, getProgress } from "../api/client";
|
|
import { CourseOut, ProgressOut } from "../api/types";
|
|
|
|
export default function CoursesPage() {
|
|
const [courses, setCourses] = useState<CourseOut[]>([]);
|
|
const [progress, setProgress] = useState<ProgressOut | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
listCourses().then(setCourses).catch((err) => setError(err.message));
|
|
getProgress().then(setProgress).catch(() => null);
|
|
}, []);
|
|
|
|
const progressMap = useMemo(() => {
|
|
const map: Record<number, { percent: number; status: string }> = {};
|
|
progress?.courses.forEach((c) => {
|
|
map[c.course_id] = { percent: c.percent, status: c.status };
|
|
});
|
|
return map;
|
|
}, [progress]);
|
|
|
|
return (
|
|
<Box>
|
|
<Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 3 }}>
|
|
<Typography variant="h4">Courses</Typography>
|
|
{progress && <Chip label="Progress tracked" color="primary" variant="outlined" />}
|
|
</Stack>
|
|
{error && <Typography color="error">{error}</Typography>}
|
|
<Box sx={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))", gap: 2 }}>
|
|
{courses.map((course) => {
|
|
const info = progressMap[course.id] || { percent: 0, status: "" };
|
|
const isCompleted = info.percent >= 100 || info.status === "passed";
|
|
return (
|
|
<Card key={course.id} sx={{ height: "100%" }}>
|
|
<CardContent>
|
|
<Stack spacing={1.5}>
|
|
<Typography variant="h6">{course.title}</Typography>
|
|
<Typography variant="body2" color="text.secondary">{course.description}</Typography>
|
|
<LinearProgress variant="determinate" value={info.percent} />
|
|
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
|
<Typography variant="caption">{Math.round(info.percent)}% complete</Typography>
|
|
{isCompleted && <Chip label="Completed" color="success" size="small" />}
|
|
</Stack>
|
|
<Stack direction="row" spacing={1}>
|
|
<Button component={RouterLink} to={`/courses/${course.id}`} variant="contained">Open</Button>
|
|
<Button variant="outlined" onClick={async () => {
|
|
await enroll(course.id);
|
|
const updated = await getProgress();
|
|
setProgress(updated);
|
|
}}>Enroll</Button>
|
|
</Stack>
|
|
</Stack>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
})}
|
|
</Box>
|
|
</Box>
|
|
);
|
|
}
|