Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
628 changes: 516 additions & 112 deletions frontend/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,12 @@
"@eslint/js": "^9.33.0",
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"@vitejs/plugin-react-swc": "^4.2.3",
"eslint": "^9.33.0",
"eslint-plugin-react-hooks": "^7.0.0",
"eslint-plugin-react-refresh": "^0.5.0",
"globals": "^17.0.0",
"lightningcss": "^1.31.1",
"sass-embedded": "^1.96.0",
"vite": "^7.1.2"
}
Expand Down
28 changes: 15 additions & 13 deletions frontend/platform/course/Course.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import FilterListIcon from '@mui/icons-material/FilterList';
import DescriptionIcon from '@mui/icons-material/Description';
import BallotIcon from '@mui/icons-material/Ballot';
import { useState, useEffect } from 'react';
import { Box, Grid, Button, Dialog, Typography } from '@mui/material'
import { Box, Grid, Button, Dialog, LinearProgress, Typography } from '@mui/material'
import { useTheme } from '@mui/material/styles';
import LessonForm from './components/LessonForm.jsx';
import QuizForm from './components/QuizForm.jsx';
import ContentTable from './components/ContentTable.jsx';
import DeleteContentForm from './components/DeleteContentForm.jsx';
import { PieChart } from '@mui/x-charts/PieChart'
import { BarChart } from '@mui/x-charts/BarChart';
import { getCookie } from '../../src/utils.js';
import { lazy, Suspense } from "react";

const QuizForm = lazy(() => import("./components/QuizForm.jsx"));
const LessonForm = lazy(() => import("./components/LessonForm.jsx"));
const DeleteContentForm = lazy(() => import("./components/DeleteContentForm.jsx"));


function Course() {
Expand Down Expand Up @@ -140,7 +142,7 @@ function Course() {
if (content.type == 'lesson') {
console.log("Opening lesson editor for content:", content);
setDialogOpen(true);
setDialogContent(<LessonForm
setDialogContent(<Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><LessonForm
header={localeMessages["update_lesson"]}
initialTitle={content.lesson.title}
initialContent={content.lesson.content}
Expand All @@ -150,11 +152,11 @@ function Course() {
courseId={course_id}
lessonId={content.lesson.id}
initialWaitingPeriod={content.waiting_period}
contentId={content.id} />);
contentId={content.id} /></Suspense>);
} else if (content.type == 'quiz') {
console.log("Opening quiz editor for content:", content);
setDialogOpen(true);
setDialogContent(<QuizForm
setDialogContent(<Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><QuizForm
cancelCallback={() => setDialogOpen(false)}
successCallback={resetDialog}
courseId={course_id}
Expand All @@ -166,7 +168,7 @@ function Course() {
initialWaitingPeriod={content.waiting_period}
initialStrategy={content.quiz.selection_strategy}
initialDeadlineDays={content.quiz.deadline_days}
/>);
/></Suspense>);
}
}
if (event.type === 'content_reordered') {
Expand All @@ -190,7 +192,7 @@ function Course() {
.catch(error => console.error('Error reordering contents:', error));
}
if (event.type === 'delete_content') {
setDialogContent(<DeleteContentForm content={event.content} onDelete={deletContent} onCancel={() => {setDialogOpen(false); setDialogMaxWidth('lg');}} />);
setDialogContent(<Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><DeleteContentForm content={event.content} onDelete={deletContent} onCancel={() => {setDialogOpen(false); setDialogMaxWidth('lg');}} /></Suspense>);
setDialogMaxWidth('sm');
setDialogOpen(true);
}
Expand All @@ -211,19 +213,19 @@ function Course() {
<Grid size={{xs: 12, md: 9}} py={2} pl={2}>
<Box p={2} sx={{ border: '1px solid', borderColor: 'grey.300', borderRadius: 1, minHeight: 300 }}>
{userRole !== 'viewer' && <><Button variant="contained" startIcon={<DescriptionIcon sx={{ marginLeft: direction == 'rtl' ? 1 : 0 }} />} sx={{ marginBottom: 2 }} onClick={() => {
setDialogContent(<LessonForm
setDialogContent(<Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><LessonForm
header={localeMessages["new_lesson"]}
initialContent={lessonCache}
onContentChange={setLessonCache}
cancelCallback={() => setDialogOpen(false)}
successCallback={resetDialog}
courseId={course_id} />);
courseId={course_id} /></Suspense>);
setDialogOpen(true);}}>{localeMessages["add_lesson"]}</Button>
<Button variant="contained" startIcon={<BallotIcon sx={{ marginLeft: direction == 'rtl' ? 1 : 0 }} />} sx={{ marginBottom: 2, marginLeft: 1, marginRight: 1 }} onClick={() => {
setDialogContent(<QuizForm
setDialogContent(<Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><QuizForm
cancelCallback={() => setDialogOpen(false)}
successCallback={resetDialog}
courseId={course_id} />);
courseId={course_id} /></Suspense>);
setDialogOpen(true);}}>{localeMessages["add_quiz"]}</Button></> }
<ContentTable courseId={course_id} loaded={contentLoaded} eventHandler={(event) => tableEventHandler(event)} />
</Box>
Expand Down
41 changes: 29 additions & 12 deletions frontend/platform/courses/Courses.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,34 @@
import 'vite/modulepreload-polyfill'
import { useState, useEffect } from 'react'
import { Grid, Box, Link, Button, IconButton, Dialog, Paper, Switch, TableContainer, Table, TableHead, TableRow,TableBody, TableCell } from '@mui/material'
import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
import Link from '@mui/material/Link';
import Button from '@mui/material/Button';
import IconButton from '@mui/material/IconButton';
import Dialog from '@mui/material/Dialog';
import LinearProgress from '@mui/material/LinearProgress';
import Paper from '@mui/material/Paper';
import Switch from '@mui/material/Switch';
import TableContainer from '@mui/material/TableContainer';
import Table from '@mui/material/Table';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import Base from '../../src/components/Base.jsx'
import CourseForm from './components/CourseForm.jsx';
import FilterListIcon from '@mui/icons-material/FilterList';
import SchoolIcon from '@mui/icons-material/School';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import render from '../../src/render.jsx';
import { getCookie } from '../../src/utils.js';
import EnableCourseSwitchPopup from './components/EnableCourseSwitchPopup.jsx';
import DeleteCoursePopup from './components/DeleteCoursePopup.jsx';
import FilterForm from './components/FilterForm.jsx';
import { lazy, Suspense } from "react";

const CourseForm = lazy(() => import("./components/CourseForm.jsx"));
const EnableCourseSwitchPopup = lazy(() => import("./components/EnableCourseSwitchPopup.jsx"));
const DeleteCoursePopup = lazy(() => import("./components/DeleteCoursePopup.jsx"));



function Courses() {
Expand Down Expand Up @@ -62,7 +79,7 @@ function Courses() {
console.log(`${action} course with ID:`, courseId);
console.log('Event:', event.target.checked);
setDialogContent(<Grid sx={{ p: 2 }}>
<EnableCourseSwitchPopup courseId={courseId} action={action} courseTitle={courseTitle} handleClose={() => setDialogOpen(false)} handleSuccess={updateCourseState}/>
<Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><EnableCourseSwitchPopup courseId={courseId} action={action} courseTitle={courseTitle} handleClose={() => setDialogOpen(false)} handleSuccess={updateCourseState}/></Suspense>
</Grid>);
setDialogOpen(true);
}
Expand All @@ -78,7 +95,7 @@ function Courses() {
};

const showEditCourseDialog = (course) => {
setDialogContent(<CourseForm
setDialogContent(<Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><CourseForm
successCallback={(data) => {
const index = courses.findIndex(item => item.id === course.id);
courses[index] = data;
Expand All @@ -92,7 +109,7 @@ function Courses() {
activeOrganizationId={organizationId}
createMode={false}
courseId={course.id}
/>);
/></Suspense>);
setDialogOpen(true);
}

Expand All @@ -108,13 +125,13 @@ function Courses() {
<Grid size={{xs: 12, md: 9}} py={2} pl={2}>
<Box p={2} sx={{ border: '1px solid', borderColor: 'border.main', backgroundColor: 'background.main', borderRadius: 1, minHeight: 300 }}>
{userRole !== 'viewer' && <Button variant="contained" startIcon={<SchoolIcon sx={{ marginLeft: direction === 'rtl' ? 1 : 0 }} />} sx={{ marginBottom: 2 }} onClick={() => {
setDialogContent(<CourseForm
setDialogContent(<Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><CourseForm
successCallback={handleCourseCreated}
failureCallback={handleCourseCreationFailed}
cancelCallback={() => setDialogOpen(false)}
activeOrganizationId={organizationId}
createMode={true}
/>);
/></Suspense>);
setDialogOpen(true);}}>{localeMessages["add_course"]}</Button>}
<TableContainer component={Paper}>
<Table sx={{ width: "100%" }} aria-label={localeMessages["courses"]}>
Expand Down Expand Up @@ -148,10 +165,10 @@ function Courses() {
<IconButton onClick={() => {
showEditCourseDialog(course);}}><EditIcon fontSize="small" /></IconButton>
<IconButton aria-label={`Delete ${course.title}`} onClick={() => {
setDialogContent(<DeleteCoursePopup courseId={course.id} courseTitle={course.title} handleClose={() => setDialogOpen(false)} handleSuccess={() => {
setDialogContent(<Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><DeleteCoursePopup courseId={course.id} courseTitle={course.title} handleClose={() => setDialogOpen(false)} handleSuccess={() => {
const index = courses.findIndex(item => item.id === course.id);
setCourses(courses.filter((_, i) => i !== index));
}} />);
}} /></Suspense>);
setDialogOpen(true);
}}><DeleteIcon fontSize="small" /></IconButton>
</TableCell>}
Expand All @@ -163,7 +180,7 @@ function Courses() {
</Box>
</Grid>
<Grid display={{xs: "none", md: "block"}} size={{ md: 3 }} p={2}>
<Box p={2} sx={{ border: '1px solid', borderColor: 'border.main', borderRadius: 1, minHeight: 300, position: 'sticky', top: 80, backgroundColor: 'background.main' }}>
<Box p={2} sx={{ border: '1px solid', borderColor: 'border.main', borderRadius: 1, minHeight: 300, position: 'sticky', top: 85, backgroundColor: 'background.main' }}>
<FilterForm onStatusChange={(params) => setQueryParameters(params)} />
</Box>
</Grid>
Expand Down
34 changes: 4 additions & 30 deletions frontend/platform/learners/Learners.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,39 +13,13 @@ import SchoolIcon from '@mui/icons-material/School';
import BackspaceIcon from '@mui/icons-material/Backspace';
import render from '../../src/render.jsx';
import { getCookie } from '../../src/utils.js';
import { lazy, Suspense } from "react";


const apiBaseUrl = localStorage.getItem('apiBaseUrl');
const EnrollentList = lazy(() => import("./components/EnrollmentList.jsx"));


function EnrolmentList({enrollments, selectHandler}) {
if (enrollments.length === 0) {
return <Typography component="span">{localeMessages["nor_enrollments_found"]}</Typography>
}
const apiBaseUrl = localStorage.getItem('apiBaseUrl');

return (
<TableContainer component={Paper} sx={{ maxHeight: 300, border: '1px solid', borderColor: 'grey.300', borderRadius: 1 }}>
<Table>
<TableHead>
<TableRow>
<TableCell align={direction=="rtl" ? "right" : "left"}>{localeMessages["course"]}</TableCell>
<TableCell align={direction=="rtl" ? "right" : "left"}>{localeMessages["status"]}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{enrollments.map((enrollment) => (
<TableRow
key={enrollment.id} sx={(theme) => ({':hover': {backgroundColor: theme.palette.background.dark, cursor: 'pointer', borderBottomColor: 'secondary.light', borderBottomWidth: 2, borderBottomStyle: 'solid'}})}
onClick={() => selectHandler(enrollment.id)}>
<TableCell align={direction=="rtl" ? "right" : "left"}>{enrollment.course_title}</TableCell>
<TableCell align={direction=="rtl" ? "right" : "left"}>{localeMessages[enrollment.status]}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}

function Learners(initialQs="") {

Expand Down Expand Up @@ -242,7 +216,7 @@ function Learners(initialQs="") {
<Typography component="span">{learner.email}</Typography>
</AccordionSummary>
<AccordionDetails>
<Typography component="span">{learner.state === 0 ? "" : learner.state === 1 ? <LinearProgress /> : <EnrolmentList enrollments={learner.enrollments} selectHandler={showEnrollmentStatus}/>}</Typography>
<Typography component="span">{learner.state === 0 ? "" : learner.state === 1 ? <LinearProgress /> : <Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><EnrollentList enrollments={learner.enrollments} selectHandler={showEnrollmentStatus}/></Suspense>}</Typography>
</AccordionDetails>
</Accordion>
</TableCell>
Expand Down
32 changes: 32 additions & 0 deletions frontend/platform/learners/components/EnrollmentList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, Typography } from '@mui/material';

function EnrollentList({enrollments, selectHandler}) {
if (enrollments.length === 0) {
return <Typography component="span">{localeMessages["nor_enrollments_found"]}</Typography>
}

return (
<TableContainer component={Paper} sx={{ maxHeight: 300, border: '1px solid', borderColor: 'grey.300', borderRadius: 1 }}>
<Table>
<TableHead>
<TableRow>
<TableCell align={direction=="rtl" ? "right" : "left"}>{localeMessages["course"]}</TableCell>
<TableCell align={direction=="rtl" ? "right" : "left"}>{localeMessages["status"]}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{enrollments.map((enrollment) => (
<TableRow
key={enrollment.id} sx={(theme) => ({':hover': {backgroundColor: theme.palette.background.dark, cursor: 'pointer', borderBottomColor: 'secondary.light', borderBottomWidth: 2, borderBottomStyle: 'solid'}})}
onClick={() => selectHandler(enrollment.id)}>
<TableCell align={direction=="rtl" ? "right" : "left"}>{enrollment.course_title}</TableCell>
<TableCell align={direction=="rtl" ? "right" : "left"}>{localeMessages[enrollment.status]}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}

export default EnrollentList;
28 changes: 22 additions & 6 deletions frontend/platform/organizations/Organizations.jsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,29 @@
import Base from "../../src/components/Base";
import { Alert, Box, Button, Dialog, Grid, IconButton, Paper, TableContainer, Table, TableHead, TableRow,TableBody, TableCell, Typography } from "@mui/material";
import Alert from "@mui/material/Alert"
import Box from "@mui/material/Box"
import Button from "@mui/material/Button"
import Dialog from "@mui/material/Dialog"
import Grid from "@mui/material/Grid"
import IconButton from "@mui/material/IconButton"
import LinearProgress from "@mui/material/LinearProgress"
import Paper from "@mui/material/Paper"
import TableContainer from "@mui/material/TableContainer"
import Table from "@mui/material/Table"
import TableHead from "@mui/material/TableHead"
import TableRow from "@mui/material/TableRow"
import TableBody from "@mui/material/TableBody"
import TableCell from "@mui/material/TableCell"
import Typography from "@mui/material/Typography"
import AddIcon from '@mui/icons-material/Add';
import PublicIcon from '@mui/icons-material/Public';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import { useState, useEffect, use } from "react";
import { getCookie } from "../../src/utils";
import render from "../../src/render";
import OrganizationForm from "./components/OrganizationForm";
import { lazy, Suspense } from "react";

const OrganizationForm = lazy(() => import("./components/OrganizationForm.jsx"));

function Organizations() {
const [dialogOpen, setDialogOpen] = useState(false);
Expand Down Expand Up @@ -93,12 +109,12 @@ function Organizations() {
<Grid size={12} py={2} pl={2}>
<Box p={2} sx={{ border: '1px solid', borderColor: 'grey.300', borderRadius: 1, minHeight: 300, width: { lg: '80%' } }}>
<Button variant="contained" startIcon={<AddIcon sx={{ marginLeft: direction == 'rtl' ? 1 : 0 }} />} sx={{ marginBottom: 2 }} onClick={() => {
setDialogContent(<OrganizationForm
setDialogContent(<Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><OrganizationForm
successCallback={handleSuccessFormSubmission}
failureCallback={handleFailedFormSubmission}
cancelCallback={() => setDialogOpen(false)}
createMode={true}
/>);
/></Suspense>);
setDialogOpen(true);
}}>{localeMessages["add_organization"]}</Button>

Expand All @@ -117,7 +133,7 @@ function Organizations() {
<TableCell>
<IconButton onClick={() => goToUrl(org.public_url)}><PublicIcon fontSize="small"/></IconButton>
<IconButton onClick={() => {
setDialogContent(<OrganizationForm
setDialogContent(<Suspense fallback={<Box sx={{ p: 2 }}><LinearProgress /></Box>}><OrganizationForm
successCallback={handleSuccessFormSubmission}
failureCallback={handleFailedFormSubmission}
cancelCallback={() => setDialogOpen(false)}
Expand All @@ -126,7 +142,7 @@ function Organizations() {
initialDescription={org.description}
initialLogoUrl={org.logo}
organizationId={org.id}
/>);
/></Suspense>);
setDialogOpen(true);
}}><EditIcon fontSize="small"/></IconButton>
<IconButton onClick={() => deleteConfirmationDialog(org)}><DeleteIcon fontSize="small" /></IconButton>
Expand Down
Loading