Merge branch 'dev' into feat/caching
This commit is contained in:
commit
fd6691f6aa
39 changed files with 1054 additions and 592 deletions
|
@ -62,6 +62,11 @@ export async function getAllSubmissionsHandler(req: Request, res: Response): Pro
|
||||||
|
|
||||||
// TODO: gerald moet nog dingen toevoegen aan de databank voor dat dit gefinaliseerd kan worden
|
// TODO: gerald moet nog dingen toevoegen aan de databank voor dat dit gefinaliseerd kan worden
|
||||||
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
|
export async function createSubmissionHandler(req: Request, res: Response): Promise<void> {
|
||||||
|
const submitter = req.body.submitter;
|
||||||
|
const usernameSubmitter = req.body.submitter.username;
|
||||||
|
const group = req.body.group;
|
||||||
|
requireFields({ group, submitter, usernameSubmitter });
|
||||||
|
|
||||||
const submissionDTO = req.body as SubmissionDTO;
|
const submissionDTO = req.body as SubmissionDTO;
|
||||||
const submission = await createSubmission(submissionDTO);
|
const submission = await createSubmission(submissionDTO);
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ import {
|
||||||
getJoinRequestsByClass,
|
getJoinRequestsByClass,
|
||||||
getStudentsByTeacher,
|
getStudentsByTeacher,
|
||||||
getTeacher,
|
getTeacher,
|
||||||
getTeacherQuestions,
|
|
||||||
updateClassJoinRequestStatus,
|
updateClassJoinRequestStatus,
|
||||||
} from '../services/teachers.js';
|
} from '../services/teachers.js';
|
||||||
import { requireFields } from './error-helper.js';
|
import { requireFields } from './error-helper.js';
|
||||||
|
@ -70,16 +69,6 @@ export async function getTeacherStudentHandler(req: Request, res: Response): Pro
|
||||||
res.json({ students });
|
res.json({ students });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherQuestionHandler(req: Request, res: Response): Promise<void> {
|
|
||||||
const username = req.params.username;
|
|
||||||
const full = req.query.full === 'true';
|
|
||||||
requireFields({ username });
|
|
||||||
|
|
||||||
const questions = await getTeacherQuestions(username, full);
|
|
||||||
|
|
||||||
res.json({ questions });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
|
export async function getStudentJoinRequestHandler(req: Request, res: Response): Promise<void> {
|
||||||
const classId = req.params.classId;
|
const classId = req.params.classId;
|
||||||
requireFields({ classId });
|
requireFields({ classId });
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js';
|
||||||
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
import { LearningObject } from '../../entities/content/learning-object.entity.js';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||||
import { Language } from '@dwengo-1/common/util/language';
|
import { Language } from '@dwengo-1/common/util/language';
|
||||||
import { Teacher } from '../../entities/users/teacher.entity.js';
|
|
||||||
|
|
||||||
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
export class LearningObjectRepository extends DwengoEntityRepository<LearningObject> {
|
||||||
public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
public async findByIdentifier(identifier: LearningObjectIdentifier): Promise<LearningObject | null> {
|
||||||
|
@ -32,11 +31,4 @@ export class LearningObjectRepository extends DwengoEntityRepository<LearningObj
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async findAllByTeacher(teacher: Teacher): Promise<LearningObject[]> {
|
|
||||||
return this.find(
|
|
||||||
{ admins: teacher },
|
|
||||||
{ populate: ['admins'] } // Make sure to load admin relations
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,9 @@ export class Assignment {
|
||||||
@Property({ type: 'string' })
|
@Property({ type: 'string' })
|
||||||
learningPathHruid!: string;
|
learningPathHruid!: string;
|
||||||
|
|
||||||
|
@Property({ type: 'datetime', nullable: true })
|
||||||
|
deadline?: Date;
|
||||||
|
|
||||||
@Enum({
|
@Enum({
|
||||||
items: () => Language,
|
items: () => Language,
|
||||||
})
|
})
|
||||||
|
|
|
@ -20,6 +20,7 @@ export function mapToAssignmentDTO(assignment: Assignment): AssignmentDTO {
|
||||||
description: assignment.description,
|
description: assignment.description,
|
||||||
learningPath: assignment.learningPathHruid,
|
learningPath: assignment.learningPathHruid,
|
||||||
language: assignment.learningPathLanguage,
|
language: assignment.learningPathLanguage,
|
||||||
|
deadline: assignment.deadline ?? new Date(),
|
||||||
groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)),
|
groups: assignment.groups.map((group) => mapToGroupDTO(group, assignment.within)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -31,6 +32,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi
|
||||||
description: assignmentData.description,
|
description: assignmentData.description,
|
||||||
learningPathHruid: assignmentData.learningPath,
|
learningPathHruid: assignmentData.learningPath,
|
||||||
learningPathLanguage: languageMap[assignmentData.language],
|
learningPathLanguage: languageMap[assignmentData.language],
|
||||||
|
deadline: assignmentData.deadline,
|
||||||
groups: [],
|
groups: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import {
|
||||||
getStudentJoinRequestHandler,
|
getStudentJoinRequestHandler,
|
||||||
getTeacherClassHandler,
|
getTeacherClassHandler,
|
||||||
getTeacherHandler,
|
getTeacherHandler,
|
||||||
getTeacherQuestionHandler,
|
|
||||||
getTeacherStudentHandler,
|
getTeacherStudentHandler,
|
||||||
updateStudentJoinRequestHandler,
|
updateStudentJoinRequestHandler,
|
||||||
} from '../controllers/teachers.js';
|
} from '../controllers/teachers.js';
|
||||||
|
@ -27,8 +26,6 @@ router.get('/:username/classes', getTeacherClassHandler);
|
||||||
|
|
||||||
router.get('/:username/students', getTeacherStudentHandler);
|
router.get('/:username/students', getTeacherStudentHandler);
|
||||||
|
|
||||||
router.get('/:username/questions', getTeacherQuestionHandler);
|
|
||||||
|
|
||||||
router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler);
|
router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler);
|
||||||
|
|
||||||
router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler);
|
router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { getLearningPathRepository } from '../../data/repositories.js';
|
||||||
import learningObjectService from '../learning-objects/learning-object-service.js';
|
import learningObjectService from '../learning-objects/learning-object-service.js';
|
||||||
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
|
import { LearningPathNode } from '../../entities/content/learning-path-node.entity.js';
|
||||||
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
||||||
import { getLastSubmissionForGroup, isTransitionPossible } from './learning-path-personalization-util.js';
|
import { getLastSubmissionForGroup, idFromLearningPathNode, isTransitionPossible } from './learning-path-personalization-util.js';
|
||||||
import {
|
import {
|
||||||
FilteredLearningObject,
|
FilteredLearningObject,
|
||||||
LearningObjectNode,
|
LearningObjectNode,
|
||||||
|
@ -95,7 +95,7 @@ async function convertNode(
|
||||||
personalizedFor: Group | undefined,
|
personalizedFor: Group | undefined,
|
||||||
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>
|
nodesToLearningObjects: Map<LearningPathNode, FilteredLearningObject>
|
||||||
): Promise<LearningObjectNode> {
|
): Promise<LearningObjectNode> {
|
||||||
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(node, personalizedFor) : null;
|
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(idFromLearningPathNode(node), personalizedFor) : null;
|
||||||
const transitions = node.transitions
|
const transitions = node.transitions
|
||||||
.filter(
|
.filter(
|
||||||
(trans) =>
|
(trans) =>
|
||||||
|
|
|
@ -3,11 +3,33 @@ import { DWENGO_API_BASE } from '../../config.js';
|
||||||
import { LearningPathProvider } from './learning-path-provider.js';
|
import { LearningPathProvider } from './learning-path-provider.js';
|
||||||
import { getLogger, Logger } from '../../logging/initalize.js';
|
import { getLogger, Logger } from '../../logging/initalize.js';
|
||||||
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
|
import { LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content';
|
||||||
|
import { Group } from '../../entities/assignments/group.entity.js';
|
||||||
|
import { getLastSubmissionForGroup, idFromLearningObjectNode } from './learning-path-personalization-util.js';
|
||||||
|
|
||||||
const logger: Logger = getLogger();
|
const logger: Logger = getLogger();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds progress information to the learning path. Modifies the learning path in-place.
|
||||||
|
* @param learningPath The learning path to add progress to.
|
||||||
|
* @param personalizedFor The group whose progress should be shown.
|
||||||
|
* @returns the modified learning path.
|
||||||
|
*/
|
||||||
|
async function addProgressToLearningPath(learningPath: LearningPath, personalizedFor: Group): Promise<LearningPath> {
|
||||||
|
await Promise.all(
|
||||||
|
learningPath.nodes.map(async (node) => {
|
||||||
|
const lastSubmission = personalizedFor ? await getLastSubmissionForGroup(idFromLearningObjectNode(node), personalizedFor) : null;
|
||||||
|
node.done = Boolean(lastSubmission);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
learningPath.num_nodes = learningPath.nodes.length;
|
||||||
|
learningPath.num_nodes_left = learningPath.nodes.filter((it) => !it.done).length;
|
||||||
|
|
||||||
|
return learningPath;
|
||||||
|
}
|
||||||
|
|
||||||
const dwengoApiLearningPathProvider: LearningPathProvider = {
|
const dwengoApiLearningPathProvider: LearningPathProvider = {
|
||||||
async fetchLearningPaths(hruids: string[], language: string, source: string): Promise<LearningPathResponse> {
|
async fetchLearningPaths(hruids: string[], language: string, source: string, personalizedFor: Group): Promise<LearningPathResponse> {
|
||||||
if (hruids.length === 0) {
|
if (hruids.length === 0) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
|
@ -32,17 +54,23 @@ const dwengoApiLearningPathProvider: LearningPathProvider = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all(learningPaths?.map(async (it) => addProgressToLearningPath(it, personalizedFor)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
source,
|
source,
|
||||||
data: learningPaths,
|
data: learningPaths,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
async searchLearningPaths(query: string, language: string): Promise<LearningPath[]> {
|
async searchLearningPaths(query: string, language: string, personalizedFor: Group): Promise<LearningPath[]> {
|
||||||
const apiUrl = `${DWENGO_API_BASE}/learningPath/search`;
|
const apiUrl = `${DWENGO_API_BASE}/learningPath/search`;
|
||||||
const params = { all: query, language };
|
const params = { all: query, language };
|
||||||
|
|
||||||
const searchResults = await fetchRemote<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params });
|
const searchResults = await fetchRemote<LearningPath[]>(apiUrl, `Search learning paths with query "${query}"`, { params });
|
||||||
|
|
||||||
|
if (searchResults) {
|
||||||
|
await Promise.all(searchResults?.map(async (it) => addProgressToLearningPath(it, personalizedFor)));
|
||||||
|
}
|
||||||
return searchResults ?? [];
|
return searchResults ?? [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,18 +5,36 @@ import { getSubmissionRepository } from '../../data/repositories.js';
|
||||||
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
|
||||||
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
import { LearningPathTransition } from '../../entities/content/learning-path-transition.entity.js';
|
||||||
import { JSONPath } from 'jsonpath-plus';
|
import { JSONPath } from 'jsonpath-plus';
|
||||||
|
import { LearningObjectNode } from '@dwengo-1/common/interfaces/learning-content';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the last submission for the learning object associated with the given node and for the group
|
* Returns the last submission for the learning object associated with the given node and for the group
|
||||||
*/
|
*/
|
||||||
export async function getLastSubmissionForGroup(node: LearningPathNode, pathFor: Group): Promise<Submission | null> {
|
export async function getLastSubmissionForGroup(learningObjectId: LearningObjectIdentifier, pathFor: Group): Promise<Submission | null> {
|
||||||
const submissionRepo = getSubmissionRepository();
|
const submissionRepo = getSubmissionRepository();
|
||||||
const learningObjectId: LearningObjectIdentifier = {
|
return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a LearningObjectIdentifier describing the specified node.
|
||||||
|
*/
|
||||||
|
export function idFromLearningObjectNode(node: LearningObjectNode): LearningObjectIdentifier {
|
||||||
|
return {
|
||||||
|
hruid: node.learningobject_hruid,
|
||||||
|
language: node.language,
|
||||||
|
version: node.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a LearningObjectIdentifier describing the specified node.
|
||||||
|
*/
|
||||||
|
export function idFromLearningPathNode(node: LearningPathNode): LearningObjectIdentifier {
|
||||||
|
return {
|
||||||
hruid: node.learningObjectHruid,
|
hruid: node.learningObjectHruid,
|
||||||
language: node.language,
|
language: node.language,
|
||||||
version: node.version,
|
version: node.version,
|
||||||
};
|
};
|
||||||
return await submissionRepo.findMostRecentSubmissionForGroup(learningObjectId, pathFor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -42,7 +42,7 @@ export async function fetchStudent(username: string): Promise<Student> {
|
||||||
const user = await studentRepository.findByUsername(username);
|
const user = await studentRepository.findByUsername(username);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('Student with username not found');
|
throw new NotFoundException(`Student with username ${username} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
import {
|
import { getClassJoinRequestRepository, getClassRepository, getTeacherRepository } from '../data/repositories.js';
|
||||||
getClassJoinRequestRepository,
|
|
||||||
getClassRepository,
|
|
||||||
getLearningObjectRepository,
|
|
||||||
getQuestionRepository,
|
|
||||||
getTeacherRepository,
|
|
||||||
} from '../data/repositories.js';
|
|
||||||
import { mapToClassDTO } from '../interfaces/class.js';
|
import { mapToClassDTO } from '../interfaces/class.js';
|
||||||
import { mapToQuestionDTO, mapToQuestionDTOId } from '../interfaces/question.js';
|
|
||||||
import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js';
|
import { mapToTeacher, mapToTeacherDTO } from '../interfaces/teacher.js';
|
||||||
import { Teacher } from '../entities/users/teacher.entity.js';
|
import { Teacher } from '../entities/users/teacher.entity.js';
|
||||||
import { fetchStudent } from './students.js';
|
import { fetchStudent } from './students.js';
|
||||||
|
@ -15,10 +8,6 @@ import { mapToStudentRequestDTO } from '../interfaces/student-request.js';
|
||||||
import { TeacherRepository } from '../data/users/teacher-repository.js';
|
import { TeacherRepository } from '../data/users/teacher-repository.js';
|
||||||
import { ClassRepository } from '../data/classes/class-repository.js';
|
import { ClassRepository } from '../data/classes/class-repository.js';
|
||||||
import { Class } from '../entities/classes/class.entity.js';
|
import { Class } from '../entities/classes/class.entity.js';
|
||||||
import { LearningObjectRepository } from '../data/content/learning-object-repository.js';
|
|
||||||
import { LearningObject } from '../entities/content/learning-object.entity.js';
|
|
||||||
import { QuestionRepository } from '../data/questions/question-repository.js';
|
|
||||||
import { Question } from '../entities/questions/question.entity.js';
|
|
||||||
import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js';
|
import { ClassJoinRequestRepository } from '../data/classes/class-join-request-repository.js';
|
||||||
import { Student } from '../entities/users/student.entity.js';
|
import { Student } from '../entities/users/student.entity.js';
|
||||||
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
import { NotFoundException } from '../exceptions/not-found-exception.js';
|
||||||
|
@ -26,7 +15,6 @@ import { addClassStudent, fetchClass, getClassStudentsDTO } from './classes.js';
|
||||||
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher';
|
||||||
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
|
import { ClassDTO } from '@dwengo-1/common/interfaces/class';
|
||||||
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
import { StudentDTO } from '@dwengo-1/common/interfaces/student';
|
||||||
import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question';
|
|
||||||
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
|
import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request';
|
||||||
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
import { ClassStatus } from '@dwengo-1/common/util/class-join-request';
|
||||||
import { ConflictException } from '../exceptions/conflict-exception.js';
|
import { ConflictException } from '../exceptions/conflict-exception.js';
|
||||||
|
@ -119,28 +107,6 @@ export async function getStudentsByTeacher(username: string, full: boolean): Pro
|
||||||
return students.map((student) => student.username);
|
return students.map((student) => student.username);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTeacherQuestions(username: string, full: boolean): Promise<QuestionDTO[] | QuestionId[]> {
|
|
||||||
const teacher: Teacher = await fetchTeacher(username);
|
|
||||||
|
|
||||||
// Find all learning objects that this teacher manages
|
|
||||||
const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository();
|
|
||||||
const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher);
|
|
||||||
|
|
||||||
if (!learningObjects || learningObjects.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch all questions related to these learning objects
|
|
||||||
const questionRepository: QuestionRepository = getQuestionRepository();
|
|
||||||
const questions: Question[] = await questionRepository.findAllByLearningObjects(learningObjects);
|
|
||||||
|
|
||||||
if (full) {
|
|
||||||
return questions.map(mapToQuestionDTO);
|
|
||||||
}
|
|
||||||
|
|
||||||
return questions.map(mapToQuestionDTOId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getJoinRequestsByClass(classId: string): Promise<ClassJoinRequestDTO[]> {
|
export async function getJoinRequestsByClass(classId: string): Promise<ClassJoinRequestDTO[]> {
|
||||||
const classRepository: ClassRepository = getClassRepository();
|
const classRepository: ClassRepository = getClassRepository();
|
||||||
const cls: Class | null = await classRepository.findById(classId);
|
const cls: Class | null = await classRepository.findById(classId);
|
||||||
|
|
76
backend/tests/controllers/assignments.test.ts
Normal file
76
backend/tests/controllers/assignments.test.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import { setupTestApp } from '../setup-tests.js';
|
||||||
|
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { getAssignmentHandler, getAllAssignmentsHandler, getAssignmentsSubmissionsHandler } from '../../src/controllers/assignments.js';
|
||||||
|
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||||
|
import { getClass01 } from '../test_assets/classes/classes.testdata';
|
||||||
|
import { getAssignment01 } from '../test_assets/assignments/assignments.testdata';
|
||||||
|
|
||||||
|
function createRequestObject(
|
||||||
|
classid: string,
|
||||||
|
assignmentid: string
|
||||||
|
): {
|
||||||
|
query: { full: string };
|
||||||
|
params: { classid: string; id: string };
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
params: {
|
||||||
|
classid: classid,
|
||||||
|
id: assignmentid,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
full: 'true',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Assignment controllers', () => {
|
||||||
|
let req: Partial<Request>;
|
||||||
|
let res: Partial<Response>;
|
||||||
|
|
||||||
|
let jsonMock: Mock;
|
||||||
|
let statusMock: Mock;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await setupTestApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jsonMock = vi.fn();
|
||||||
|
statusMock = vi.fn().mockReturnThis();
|
||||||
|
|
||||||
|
res = {
|
||||||
|
json: jsonMock,
|
||||||
|
status: statusMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return error non-existing assignment', async () => {
|
||||||
|
req = createRequestObject('doesnotexist', '43000'); // Should not exist
|
||||||
|
|
||||||
|
await expect(async () => getAssignmentHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an assignment', async () => {
|
||||||
|
const assignment = getAssignment01();
|
||||||
|
req = createRequestObject(assignment.within.classId as string, (assignment.id ?? 1).toString());
|
||||||
|
|
||||||
|
await getAssignmentHandler(req as Request, res as Response);
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ assignment: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of assignments', async () => {
|
||||||
|
req = createRequestObject(getClass01().classId as string, 'irrelevant');
|
||||||
|
|
||||||
|
await getAllAssignmentsHandler(req as Request, res as Response);
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ assignments: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of submissions for an assignment', async () => {
|
||||||
|
const assignment = getAssignment01();
|
||||||
|
req = createRequestObject(assignment.within.classId as string, (assignment.id ?? 1).toString());
|
||||||
|
|
||||||
|
await getAssignmentsSubmissionsHandler(req as Request, res as Response);
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() }));
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,8 +1,17 @@
|
||||||
import { setupTestApp } from '../setup-tests.js';
|
import { setupTestApp } from '../setup-tests.js';
|
||||||
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
||||||
|
import {
|
||||||
|
createClassHandler,
|
||||||
|
deleteClassHandler,
|
||||||
|
getAllClassesHandler,
|
||||||
|
getClassHandler,
|
||||||
|
getClassStudentsHandler,
|
||||||
|
getTeacherInvitationsHandler,
|
||||||
|
} from '../../src/controllers/classes.js';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { createClassHandler, deleteClassHandler } from '../../src/controllers/classes';
|
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||||
|
import { BadRequestException } from '../../src/exceptions/bad-request-exception';
|
||||||
|
import { getClass01 } from '../test_assets/classes/classes.testdata';
|
||||||
describe('Class controllers', () => {
|
describe('Class controllers', () => {
|
||||||
let req: Partial<Request>;
|
let req: Partial<Request>;
|
||||||
let res: Partial<Response>;
|
let res: Partial<Response>;
|
||||||
|
@ -44,4 +53,71 @@ describe('Class controllers', () => {
|
||||||
|
|
||||||
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ class: expect.anything() }));
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ class: expect.anything() }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Error class not found', async () => {
|
||||||
|
req = {
|
||||||
|
params: { id: 'doesnotexist' },
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => getClassHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error create a class without name', async () => {
|
||||||
|
req = {
|
||||||
|
body: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => createClassHandler(req as Request, res as Response)).rejects.toThrow(BadRequestException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('return list of students', async () => {
|
||||||
|
req = {
|
||||||
|
params: { id: getClass01().classId as string },
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await getClassStudentsHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ students: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error students on a non-existent class', async () => {
|
||||||
|
req = {
|
||||||
|
params: { id: 'doesnotexist' },
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => getClassStudentsHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 200 and a list of teacher-invitations', async () => {
|
||||||
|
const classId = getClass01().classId as string;
|
||||||
|
req = {
|
||||||
|
params: { id: classId },
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await getTeacherInvitationsHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ invitations: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error teacher-invitations on a non-existent class', async () => {
|
||||||
|
req = {
|
||||||
|
params: { id: 'doesnotexist' },
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => getTeacherInvitationsHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of classes', async () => {
|
||||||
|
req = {
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await getAllClassesHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ classes: expect.anything() }));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
140
backend/tests/controllers/groups.test.ts
Normal file
140
backend/tests/controllers/groups.test.ts
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
import { setupTestApp } from '../setup-tests.js';
|
||||||
|
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import {
|
||||||
|
createGroupHandler,
|
||||||
|
deleteGroupHandler,
|
||||||
|
getAllGroupsHandler,
|
||||||
|
getGroupHandler,
|
||||||
|
getGroupSubmissionsHandler,
|
||||||
|
} from '../../src/controllers/groups.js';
|
||||||
|
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||||
|
import { getClass01 } from '../test_assets/classes/classes.testdata';
|
||||||
|
import { getAssignment01, getAssignment02 } from '../test_assets/assignments/assignments.testdata';
|
||||||
|
import { getTestGroup01 } from '../test_assets/assignments/groups.testdata';
|
||||||
|
|
||||||
|
function createRequestObject(
|
||||||
|
classid: string,
|
||||||
|
assignmentid: string,
|
||||||
|
groupNumber: string
|
||||||
|
): {
|
||||||
|
query: { full: string };
|
||||||
|
params: { classid: string; groupid: string; assignmentid: string };
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
params: {
|
||||||
|
classid: classid,
|
||||||
|
assignmentid: assignmentid,
|
||||||
|
groupid: groupNumber,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
full: 'true',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Group controllers', () => {
|
||||||
|
let req: Partial<Request>;
|
||||||
|
let res: Partial<Response>;
|
||||||
|
|
||||||
|
let jsonMock: Mock;
|
||||||
|
let statusMock: Mock;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await setupTestApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jsonMock = vi.fn();
|
||||||
|
statusMock = vi.fn().mockReturnThis();
|
||||||
|
|
||||||
|
res = {
|
||||||
|
json: jsonMock,
|
||||||
|
status: statusMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Error not found on a non-existing group', async () => {
|
||||||
|
req = {
|
||||||
|
params: {
|
||||||
|
classid: 'id01',
|
||||||
|
assignmentid: '1',
|
||||||
|
groupid: '154981', // Should not exist
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => getGroupHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 404 not found on a non-existing assignment', async () => {
|
||||||
|
req = {
|
||||||
|
params: {
|
||||||
|
classid: 'id01',
|
||||||
|
assignmentid: '1000', // Should not exist
|
||||||
|
groupid: '42000', // Should not exist
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => getGroupHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 404 not found ont a non-existing class', async () => {
|
||||||
|
req = {
|
||||||
|
params: {
|
||||||
|
classid: 'doesnotexist', // Should not exist
|
||||||
|
assignmentid: '1000', // Should not exist
|
||||||
|
groupid: '42000', // Should not exist
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(async () => getGroupHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an existing group', async () => {
|
||||||
|
const group = getTestGroup01();
|
||||||
|
const classId = getClass01().classId as string;
|
||||||
|
req = createRequestObject(classId, (group.assignment.id ?? 1).toString(), (group.groupNumber ?? 1).toString());
|
||||||
|
|
||||||
|
await getGroupHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ group: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Create and delete', async () => {
|
||||||
|
const assignment = getAssignment02();
|
||||||
|
const classId = assignment.within.classId as string;
|
||||||
|
req = createRequestObject(classId, (assignment.id ?? 1).toString(), '1');
|
||||||
|
req.body = {
|
||||||
|
members: ['Noordkaap', 'DireStraits'],
|
||||||
|
};
|
||||||
|
|
||||||
|
await createGroupHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
await deleteGroupHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ group: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the submissions for a group', async () => {
|
||||||
|
const group = getTestGroup01();
|
||||||
|
const classId = getClass01().classId as string;
|
||||||
|
req = createRequestObject(classId, (group.assignment.id ?? 1).toString(), (group.groupNumber ?? 1).toString());
|
||||||
|
|
||||||
|
await getGroupSubmissionsHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() }));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of groups for an assignment', async () => {
|
||||||
|
const assignment = getAssignment01();
|
||||||
|
const classId = assignment.within.classId as string;
|
||||||
|
req = createRequestObject(classId, (assignment.id ?? 1).toString(), '1');
|
||||||
|
|
||||||
|
await getAllGroupsHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ groups: expect.anything() }));
|
||||||
|
});
|
||||||
|
});
|
61
backend/tests/controllers/submissions.test.ts
Normal file
61
backend/tests/controllers/submissions.test.ts
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { setupTestApp } from '../setup-tests.js';
|
||||||
|
import { describe, it, expect, beforeAll, beforeEach, vi, Mock } from 'vitest';
|
||||||
|
import { getSubmissionHandler, getAllSubmissionsHandler } from '../../src/controllers/submissions.js';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { NotFoundException } from '../../src/exceptions/not-found-exception';
|
||||||
|
import { getClass02 } from '../test_assets/classes/classes.testdata';
|
||||||
|
|
||||||
|
function createRequestObject(
|
||||||
|
hruid: string,
|
||||||
|
submissionNumber: string
|
||||||
|
): {
|
||||||
|
query: { language: string; version: string };
|
||||||
|
params: { hruid: string; id: string };
|
||||||
|
} {
|
||||||
|
return {
|
||||||
|
params: {
|
||||||
|
hruid: hruid,
|
||||||
|
id: submissionNumber,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
language: 'en',
|
||||||
|
version: '1',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Submission controllers', () => {
|
||||||
|
let req: Partial<Request>;
|
||||||
|
let res: Partial<Response>;
|
||||||
|
|
||||||
|
let jsonMock: Mock;
|
||||||
|
let statusMock: Mock;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await setupTestApp();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jsonMock = vi.fn();
|
||||||
|
statusMock = vi.fn().mockReturnThis();
|
||||||
|
|
||||||
|
res = {
|
||||||
|
json: jsonMock,
|
||||||
|
status: statusMock,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('error submission is not found', async () => {
|
||||||
|
req = createRequestObject('id01', '1000000');
|
||||||
|
|
||||||
|
await expect(async () => getSubmissionHandler(req as Request, res as Response)).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a list of submissions for a learning object', async () => {
|
||||||
|
req = createRequestObject(getClass02().classId as string, 'irrelevant');
|
||||||
|
|
||||||
|
await getAllSubmissionsHandler(req as Request, res as Response);
|
||||||
|
|
||||||
|
expect(jsonMock).toHaveBeenCalledWith(expect.objectContaining({ submissions: expect.anything() }));
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,13 +6,20 @@ import { testLearningPathWithConditions } from '../content/learning-paths.testda
|
||||||
import { getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata';
|
import { getClassWithTestleerlingAndTestleerkracht } from '../classes/classes.testdata';
|
||||||
|
|
||||||
export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] {
|
export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assignment[] {
|
||||||
|
const futureDate = new Date();
|
||||||
|
futureDate.setDate(futureDate.getDate() + 7);
|
||||||
|
const pastDate = new Date();
|
||||||
|
pastDate.setDate(pastDate.getDate() - 7);
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(23, 59);
|
||||||
assignment01 = em.create(Assignment, {
|
assignment01 = em.create(Assignment, {
|
||||||
id: 21000,
|
id: 21000,
|
||||||
within: classes[0],
|
within: classes[0],
|
||||||
title: 'dire straits',
|
title: 'dire straits',
|
||||||
description: 'reading',
|
description: 'reading',
|
||||||
learningPathHruid: 'id02',
|
learningPathHruid: 'un_ai',
|
||||||
learningPathLanguage: Language.English,
|
learningPathLanguage: Language.English,
|
||||||
|
deadline: today,
|
||||||
groups: [],
|
groups: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -23,6 +30,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
|
||||||
description: 'reading',
|
description: 'reading',
|
||||||
learningPathHruid: 'id01',
|
learningPathHruid: 'id01',
|
||||||
learningPathLanguage: Language.English,
|
learningPathLanguage: Language.English,
|
||||||
|
deadline: futureDate,
|
||||||
groups: [],
|
groups: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -33,6 +41,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
|
||||||
description: 'will be deleted',
|
description: 'will be deleted',
|
||||||
learningPathHruid: 'id02',
|
learningPathHruid: 'id02',
|
||||||
learningPathLanguage: Language.English,
|
learningPathLanguage: Language.English,
|
||||||
|
deadline: pastDate,
|
||||||
groups: [],
|
groups: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -43,6 +52,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
|
||||||
description: 'with a description',
|
description: 'with a description',
|
||||||
learningPathHruid: 'id01',
|
learningPathHruid: 'id01',
|
||||||
learningPathLanguage: Language.English,
|
learningPathLanguage: Language.English,
|
||||||
|
deadline: pastDate,
|
||||||
groups: [],
|
groups: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -53,6 +63,7 @@ export function makeTestAssignemnts(em: EntityManager, classes: Class[]): Assign
|
||||||
description: 'You have to do the testing learning path with a condition.',
|
description: 'You have to do the testing learning path with a condition.',
|
||||||
learningPathHruid: testLearningPathWithConditions.hruid,
|
learningPathHruid: testLearningPathWithConditions.hruid,
|
||||||
learningPathLanguage: testLearningPathWithConditions.language as Language,
|
learningPathLanguage: testLearningPathWithConditions.language as Language,
|
||||||
|
deadline: futureDate,
|
||||||
groups: [],
|
groups: [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ export interface AssignmentDTO {
|
||||||
description: string;
|
description: string;
|
||||||
learningPath: string;
|
learningPath: string;
|
||||||
language: string;
|
language: string;
|
||||||
|
deadline: Date;
|
||||||
groups: GroupDTO[] | string[][];
|
groups: GroupDTO[] | string[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
54
frontend/src/assets/common.css
Normal file
54
frontend/src/assets/common.css
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
.h1 {
|
||||||
|
color: #0e6942;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bolder;
|
||||||
|
font-size: 50px;
|
||||||
|
padding-left: 1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-message {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-weight: bold !important;
|
||||||
|
background-color: #0e6942;
|
||||||
|
color: white;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th:first-child {
|
||||||
|
border-top-left-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th:last-child {
|
||||||
|
border-top-right-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:nth-child(odd) {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:nth-child(even) {
|
||||||
|
background-color: #f6faf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table td,
|
||||||
|
.table th {
|
||||||
|
border-bottom: 1px solid #0e6942;
|
||||||
|
border-top: 1px solid #0e6942;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 90%;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 850px) {
|
||||||
|
.h1 {
|
||||||
|
text-align: center;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,6 +57,22 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-row v-else>
|
<v-row v-else>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
sm="6"
|
||||||
|
md="4"
|
||||||
|
lg="4"
|
||||||
|
class="d-flex"
|
||||||
|
>
|
||||||
|
<ThemeCard
|
||||||
|
path="/learningPath/search"
|
||||||
|
:is-absolute-path="true"
|
||||||
|
:title="t('searchAllLearningPathsTitle')"
|
||||||
|
:description="t('searchAllLearningPathsDescription')"
|
||||||
|
icon="mdi-magnify"
|
||||||
|
class="fill-height grey-bg-card"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
<v-col
|
<v-col
|
||||||
v-for="card in cards"
|
v-for="card in cards"
|
||||||
:key="card.key"
|
:key="card.key"
|
||||||
|
@ -74,24 +90,13 @@
|
||||||
class="fill-height"
|
class="fill-height"
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col
|
|
||||||
cols="12"
|
|
||||||
sm="6"
|
|
||||||
md="4"
|
|
||||||
lg="4"
|
|
||||||
class="d-flex"
|
|
||||||
>
|
|
||||||
<ThemeCard
|
|
||||||
path="/learningPath/search"
|
|
||||||
:is-absolute-path="true"
|
|
||||||
:title="t('searchAllLearningPathsTitle')"
|
|
||||||
:description="t('searchAllLearningPathsDescription')"
|
|
||||||
icon="mdi-magnify"
|
|
||||||
class="fill-height"
|
|
||||||
/>
|
|
||||||
</v-col>
|
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.grey-bg-card {
|
||||||
|
background-color: #f6faf2;
|
||||||
|
border: 2px solid #0e6942;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,49 +1,30 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from "vue";
|
import { ref, watch } from "vue";
|
||||||
import { deadlineRules } from "@/utils/assignment-rules.ts";
|
import { deadlineRules } from "@/utils/assignment-rules.ts";
|
||||||
|
|
||||||
const date = ref("");
|
const emit = defineEmits<(e: "update:deadline", value: Date) => void>();
|
||||||
const time = ref("23:59");
|
|
||||||
const emit = defineEmits(["update:deadline"]);
|
|
||||||
|
|
||||||
const formattedDeadline = computed(() => {
|
const datetime = ref("");
|
||||||
if (!date.value || !time.value) return "";
|
|
||||||
return `${date.value} ${time.value}`;
|
// Watch the datetime value and emit the update
|
||||||
|
watch(datetime, (val) => {
|
||||||
|
const newDate = new Date(val);
|
||||||
|
if (!isNaN(newDate.getTime())) {
|
||||||
|
emit("update:deadline", newDate);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function updateDeadline(): void {
|
|
||||||
if (date.value && time.value) {
|
|
||||||
emit("update:deadline", formattedDeadline.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="date"
|
v-model="datetime"
|
||||||
label="Select Deadline Date"
|
type="datetime-local"
|
||||||
type="date"
|
label="Select Deadline"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
density="compact"
|
density="compact"
|
||||||
:rules="deadlineRules"
|
:rules="deadlineRules"
|
||||||
required
|
required
|
||||||
@update:modelValue="updateDeadline"
|
/>
|
||||||
></v-text-field>
|
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-text>
|
|
||||||
<v-text-field
|
|
||||||
v-model="time"
|
|
||||||
label="Select Deadline Time"
|
|
||||||
type="time"
|
|
||||||
variant="outlined"
|
|
||||||
density="compact"
|
|
||||||
@update:modelValue="updateDeadline"
|
|
||||||
></v-text-field>
|
|
||||||
</v-card-text>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
|
||||||
|
|
|
@ -26,8 +26,8 @@ export class LearningPathController extends BaseController {
|
||||||
});
|
});
|
||||||
return LearningPath.fromDTO(single(dtos));
|
return LearningPath.fromDTO(single(dtos));
|
||||||
}
|
}
|
||||||
async getAllByTheme(theme: string): Promise<LearningPath[]> {
|
async getAllByThemeAndLanguage(theme: string, language: Language): Promise<LearningPath[]> {
|
||||||
const dtos = await this.get<LearningPathDTO[]>("/", { theme });
|
const dtos = await this.get<LearningPathDTO[]>("/", { theme, language });
|
||||||
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { BaseController } from "@/controllers/base-controller.ts";
|
import { BaseController } from "@/controllers/base-controller.ts";
|
||||||
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
||||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
|
||||||
import type { ClassesResponse } from "@/controllers/classes.ts";
|
import type { ClassesResponse } from "@/controllers/classes.ts";
|
||||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||||
|
|
||||||
|
@ -40,10 +39,6 @@ export class TeacherController extends BaseController {
|
||||||
return this.get<StudentsResponse>(`/${username}/students`, { full });
|
return this.get<StudentsResponse>(`/${username}/students`, { full });
|
||||||
}
|
}
|
||||||
|
|
||||||
async getQuestions(username: string, full = false): Promise<QuestionsResponse> {
|
|
||||||
return this.get<QuestionsResponse>(`/${username}/questions`, { full });
|
|
||||||
}
|
|
||||||
|
|
||||||
async getStudentJoinRequests(username: string, classId: string): Promise<JoinRequestsResponse> {
|
async getStudentJoinRequests(username: string, classId: string): Promise<JoinRequestsResponse> {
|
||||||
return this.get<JoinRequestsResponse>(`/${username}/joinRequests/${classId}`);
|
return this.get<JoinRequestsResponse>(`/${username}/joinRequests/${classId}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"JoinClassExplanation": "Geben Sie den Code ein, den Ihnen die Lehrkraft mitgeteilt hat, um der Klasse beizutreten.",
|
"JoinClassExplanation": "Geben Sie den Code ein, den Ihnen die Lehrkraft mitgeteilt hat, um der Klasse beizutreten.",
|
||||||
"invalidFormat": "Ungültiges Format",
|
"invalidFormat": "Ungültiges Format",
|
||||||
"submitCode": "senden",
|
"submitCode": "senden",
|
||||||
|
"submit": "senden",
|
||||||
"members": "Mitglieder",
|
"members": "Mitglieder",
|
||||||
"themes": "Themen",
|
"themes": "Themen",
|
||||||
"choose-theme": "Wählen Sie ein Thema",
|
"choose-theme": "Wählen Sie ein Thema",
|
||||||
|
@ -68,10 +69,10 @@
|
||||||
"pick-class": "Wählen Sie eine klasse",
|
"pick-class": "Wählen Sie eine klasse",
|
||||||
"choose-students": "Studenten auswählen",
|
"choose-students": "Studenten auswählen",
|
||||||
"create-group": "Gruppe erstellen",
|
"create-group": "Gruppe erstellen",
|
||||||
"class": "klasse",
|
"class": "Klasse",
|
||||||
"delete": "löschen",
|
"delete": "löschen",
|
||||||
"view-assignment": "Auftrag anzeigen",
|
"view-assignment": "Auftrag anzeigen",
|
||||||
"code": "code",
|
"code": "Code",
|
||||||
"invitations": "Einladungen",
|
"invitations": "Einladungen",
|
||||||
"createClass": "Klasse erstellen",
|
"createClass": "Klasse erstellen",
|
||||||
"createClassInstructions": "Geben Sie einen Namen für Ihre Klasse ein und klicken Sie auf „Erstellen“. Es erscheint ein Fenster mit einem Code, den Sie kopieren können. Geben Sie diesen Code an Ihre Schüler weiter und sie können Ihrer Klasse beitreten.",
|
"createClassInstructions": "Geben Sie einen Namen für Ihre Klasse ein und klicken Sie auf „Erstellen“. Es erscheint ein Fenster mit einem Code, den Sie kopieren können. Geben Sie diesen Code an Ihre Schüler weiter und sie können Ihrer Klasse beitreten.",
|
||||||
|
@ -83,7 +84,7 @@
|
||||||
"onlyUse": "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden",
|
"onlyUse": "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden",
|
||||||
"close": "schließen",
|
"close": "schließen",
|
||||||
"copied": "kopiert!",
|
"copied": "kopiert!",
|
||||||
"accept": "akzeptieren",
|
"accept": "Akzeptieren",
|
||||||
"deny": "ablehnen",
|
"deny": "ablehnen",
|
||||||
"sent": "sent",
|
"sent": "sent",
|
||||||
"failed": "fehlgeschlagen",
|
"failed": "fehlgeschlagen",
|
||||||
|
@ -110,7 +111,7 @@
|
||||||
"remove": "entfernen",
|
"remove": "entfernen",
|
||||||
"students": "Studenten",
|
"students": "Studenten",
|
||||||
"classJoinRequests": "Beitrittsanfragen",
|
"classJoinRequests": "Beitrittsanfragen",
|
||||||
"reject": "ablehnen",
|
"reject": "Ablehnen",
|
||||||
"areusure": "Sind Sie sicher?",
|
"areusure": "Sind Sie sicher?",
|
||||||
"yes": "ja",
|
"yes": "ja",
|
||||||
"teachers": "Lehrer",
|
"teachers": "Lehrer",
|
||||||
|
@ -121,5 +122,18 @@
|
||||||
"invite": "einladen",
|
"invite": "einladen",
|
||||||
"assignmentIndicator": "AUFGABE",
|
"assignmentIndicator": "AUFGABE",
|
||||||
"searchAllLearningPathsTitle": "Alle Lernpfade durchsuchen",
|
"searchAllLearningPathsTitle": "Alle Lernpfade durchsuchen",
|
||||||
"searchAllLearningPathsDescription": "Nicht gefunden, was Sie gesucht haben? Klicken Sie hier, um unsere gesamte Lernpfad-Datenbank zu durchsuchen."
|
"searchAllLearningPathsDescription": "Nicht gefunden, was Sie gesucht haben? Klicken Sie hier, um unsere gesamte Lernpfad-Datenbank zu durchsuchen.",
|
||||||
|
"no-students-found": "Diese Klasse hat keine Schüler.",
|
||||||
|
"no-invitations-found": "Sie haben keine ausstehenden Einladungen.",
|
||||||
|
"no-join-requests-found": "Es gibt keine ausstehenden Beitrittsanfragen für diese Klasse.",
|
||||||
|
"no-classes-found": "Sie sind noch keinem Kurs beigetreten.",
|
||||||
|
"classCreated": "Klasse erstellt!",
|
||||||
|
"success": "Erfolg",
|
||||||
|
"submitted": "eingereicht",
|
||||||
|
"see-submission": "Einsendung anzeigen",
|
||||||
|
"view-submissions": "Einsendungen anzeigen",
|
||||||
|
"valid-username": "Bitte geben Sie einen gültigen Benutzernamen ein",
|
||||||
|
"creationFailed": "Erstellung fehlgeschlagen, bitte versuchen Sie es erneut",
|
||||||
|
"no-assignments": "Derzeit gibt es keine Zuweisungen.",
|
||||||
|
"deadline": "deadline"
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
|
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
|
||||||
"invalidFormat": "Invalid format.",
|
"invalidFormat": "Invalid format.",
|
||||||
"submitCode": "submit",
|
"submitCode": "submit",
|
||||||
|
"submit": "submit",
|
||||||
"members": "Members",
|
"members": "Members",
|
||||||
"themes": "Themes",
|
"themes": "Themes",
|
||||||
"choose-theme": "Select a theme",
|
"choose-theme": "Select a theme",
|
||||||
|
@ -68,21 +69,21 @@
|
||||||
"pick-class": "Pick a class",
|
"pick-class": "Pick a class",
|
||||||
"choose-students": "Select students",
|
"choose-students": "Select students",
|
||||||
"create-group": "Create group",
|
"create-group": "Create group",
|
||||||
"class": "class",
|
"class": "Class",
|
||||||
"delete": "delete",
|
"delete": "delete",
|
||||||
"view-assignment": "View assignment",
|
"view-assignment": "View assignment",
|
||||||
"code": "code",
|
"code": "Code",
|
||||||
"invitations": "invitations",
|
"invitations": "Invitations",
|
||||||
"createClass": "create class",
|
"createClass": "Create class",
|
||||||
"classname": "classname",
|
"classname": "classname",
|
||||||
"EnterNameOfClass": "Enter a classname.",
|
"EnterNameOfClass": "Enter a classname.",
|
||||||
"create": "create",
|
"create": "create",
|
||||||
"sender": "sender",
|
"sender": "Sender",
|
||||||
"nameIsMandatory": "classname is mandatory",
|
"nameIsMandatory": "classname is mandatory",
|
||||||
"onlyUse": "only use letters, numbers, dashes (-) and underscores (_)",
|
"onlyUse": "only use letters, numbers, dashes (-) and underscores (_)",
|
||||||
"close": "close",
|
"close": "close",
|
||||||
"copied": "copied!",
|
"copied": "copied!",
|
||||||
"accept": "accept",
|
"accept": "Accept",
|
||||||
"deny": "deny",
|
"deny": "deny",
|
||||||
"createClassInstructions": "Enter a name for your class and click on create. A window will appear with a code that you can copy. Give this code to your students and they will be able to join.",
|
"createClassInstructions": "Enter a name for your class and click on create. A window will appear with a code that you can copy. Give this code to your students and they will be able to join.",
|
||||||
"sent": "sent",
|
"sent": "sent",
|
||||||
|
@ -108,12 +109,12 @@
|
||||||
"progress": "Progress",
|
"progress": "Progress",
|
||||||
"created": "created",
|
"created": "created",
|
||||||
"remove": "remove",
|
"remove": "remove",
|
||||||
"students": "students",
|
"students": "Students",
|
||||||
"classJoinRequests": "join requests",
|
"classJoinRequests": "Join requests",
|
||||||
"reject": "reject",
|
"reject": "Reject",
|
||||||
"areusure": "Are you sure?",
|
"areusure": "Are you sure?",
|
||||||
"yes": "yes",
|
"yes": "yes",
|
||||||
"teachers": "teachers",
|
"teachers": "Teachers",
|
||||||
"accepted": "accepted",
|
"accepted": "accepted",
|
||||||
"rejected": "rejected",
|
"rejected": "rejected",
|
||||||
"enterUsername": "enter the username of the teacher you would like to invite",
|
"enterUsername": "enter the username of the teacher you would like to invite",
|
||||||
|
@ -121,5 +122,18 @@
|
||||||
"invite": "invite",
|
"invite": "invite",
|
||||||
"assignmentIndicator": "ASSIGNMENT",
|
"assignmentIndicator": "ASSIGNMENT",
|
||||||
"searchAllLearningPathsTitle": "Search all learning paths",
|
"searchAllLearningPathsTitle": "Search all learning paths",
|
||||||
"searchAllLearningPathsDescription": "You didn't find what you were looking for? Click here to search our whole database of available learning paths."
|
"searchAllLearningPathsDescription": "You didn't find what you were looking for? Click here to search our whole database of available learning paths.",
|
||||||
|
"no-students-found": "This class has no students.",
|
||||||
|
"no-invitations-found": "You have no pending invitations.",
|
||||||
|
"no-join-requests-found": "There are no pending join requests for this class.",
|
||||||
|
"no-classes-found": "You are not yet part of a class.",
|
||||||
|
"classCreated": "class created!",
|
||||||
|
"success": "success",
|
||||||
|
"submitted": "submitted",
|
||||||
|
"see-submission": "view submission",
|
||||||
|
"view-submissions": "view submissions",
|
||||||
|
"valid-username": "please enter a valid username",
|
||||||
|
"creationFailed": "creation failed, please try again",
|
||||||
|
"no-assignments": "There are currently no assignments.",
|
||||||
|
"deadline": "deadline"
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
|
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
|
||||||
"invalidFormat": "Format non valide.",
|
"invalidFormat": "Format non valide.",
|
||||||
"submitCode": "envoyer",
|
"submitCode": "envoyer",
|
||||||
|
"submit": "envoyer",
|
||||||
"members": "Membres",
|
"members": "Membres",
|
||||||
"themes": "Thèmes",
|
"themes": "Thèmes",
|
||||||
"choose-theme": "Choisis un thème",
|
"choose-theme": "Choisis un thème",
|
||||||
|
@ -68,22 +69,22 @@
|
||||||
"pick-class": "Choisissez une classe",
|
"pick-class": "Choisissez une classe",
|
||||||
"choose-students": "Sélectionnez des élèves",
|
"choose-students": "Sélectionnez des élèves",
|
||||||
"create-group": "Créer un groupe",
|
"create-group": "Créer un groupe",
|
||||||
"class": "classe",
|
"class": "Classe",
|
||||||
"delete": "supprimer",
|
"delete": "supprimer",
|
||||||
"view-assignment": "Voir le travail",
|
"view-assignment": "Voir le travail",
|
||||||
"code": "code",
|
"code": "Code",
|
||||||
"invitations": "invitations",
|
"invitations": "Invitations",
|
||||||
"createClass": "créer une classe",
|
"createClass": "Créer une classe",
|
||||||
"createClassInstructions": "Entrez un nom pour votre classe et cliquez sur créer. Une fenêtre apparaît avec un code que vous pouvez copier. Donnez ce code à vos élèves et ils pourront rejoindre votre classe.",
|
"createClassInstructions": "Entrez un nom pour votre classe et cliquez sur créer. Une fenêtre apparaît avec un code que vous pouvez copier. Donnez ce code à vos élèves et ils pourront rejoindre votre classe.",
|
||||||
"classname": "nom de classe",
|
"classname": "nom de classe",
|
||||||
"EnterNameOfClass": "saisir un nom de classe.",
|
"EnterNameOfClass": "saisir un nom de classe.",
|
||||||
"create": "créer",
|
"create": "créer",
|
||||||
"sender": "expéditeur",
|
"sender": "Expéditeur",
|
||||||
"nameIsMandatory": "le nom de classe est obligatoire",
|
"nameIsMandatory": "le nom de classe est obligatoire",
|
||||||
"onlyUse": "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)",
|
"onlyUse": "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)",
|
||||||
"close": "fermer",
|
"close": "fermer",
|
||||||
"copied": "copié!",
|
"copied": "copié!",
|
||||||
"accept": "accepter",
|
"accept": "Accepter",
|
||||||
"deny": "refuser",
|
"deny": "refuser",
|
||||||
"sent": "envoyé",
|
"sent": "envoyé",
|
||||||
"failed": "échoué",
|
"failed": "échoué",
|
||||||
|
@ -108,12 +109,13 @@
|
||||||
"submission": "Soumission",
|
"submission": "Soumission",
|
||||||
"progress": "Progrès",
|
"progress": "Progrès",
|
||||||
"remove": "supprimer",
|
"remove": "supprimer",
|
||||||
"students": "étudiants",
|
"students": "Étudiants",
|
||||||
"classJoinRequests": "demandes d'adhésion",
|
|
||||||
"reject": "rejeter",
|
"classJoinRequests": "Demandes d'adhésion",
|
||||||
|
"reject": "Rejeter",
|
||||||
"areusure": "Êtes-vous sûr?",
|
"areusure": "Êtes-vous sûr?",
|
||||||
"yes": "oui",
|
"yes": "oui",
|
||||||
"teachers": "enseignants",
|
"teachers": "Enseignants",
|
||||||
"accepted": "acceptée",
|
"accepted": "acceptée",
|
||||||
"rejected": "rejetée",
|
"rejected": "rejetée",
|
||||||
"enterUsername": "entrez le nom d'utilisateur de l'enseignant que vous souhaitez inviter",
|
"enterUsername": "entrez le nom d'utilisateur de l'enseignant que vous souhaitez inviter",
|
||||||
|
@ -121,5 +123,18 @@
|
||||||
"invite": "inviter",
|
"invite": "inviter",
|
||||||
"assignmentIndicator": "DEVOIR",
|
"assignmentIndicator": "DEVOIR",
|
||||||
"searchAllLearningPathsTitle": "Rechercher tous les parcours d'apprentissage",
|
"searchAllLearningPathsTitle": "Rechercher tous les parcours d'apprentissage",
|
||||||
"searchAllLearningPathsDescription": "Vous n'avez pas trouvé ce que vous cherchiez ? Cliquez ici pour rechercher dans toute notre base de données de parcours d'apprentissage disponibles."
|
"searchAllLearningPathsDescription": "Vous n'avez pas trouvé ce que vous cherchiez ? Cliquez ici pour rechercher dans toute notre base de données de parcours d'apprentissage disponibles.",
|
||||||
|
"no-students-found": "Cette classe n'a pas d'élèves.",
|
||||||
|
"no-invitations-found": "Vous n'avez aucune invitation en attente.",
|
||||||
|
"no-join-requests-found": "Il n'y a aucune demande d'adhésion en attente pour cette classe.",
|
||||||
|
"no-classes-found": "Vous ne faites pas encore partie d'une classe.",
|
||||||
|
"classCreated": "Classe créée !",
|
||||||
|
"success": "succès",
|
||||||
|
"submitted": "soumis",
|
||||||
|
"see-submission": "voir la soumission",
|
||||||
|
"view-submissions": "voir les soumissions",
|
||||||
|
"valid-username": "veuillez entrer un nom d'utilisateur valide",
|
||||||
|
"creationFailed": "échec de la création, veuillez réessayer",
|
||||||
|
"no-assignments": "Il n'y a actuellement aucun travail.",
|
||||||
|
"deadline": "délai"
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@
|
||||||
"JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.",
|
"JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.",
|
||||||
"invalidFormat": "Ongeldig formaat.",
|
"invalidFormat": "Ongeldig formaat.",
|
||||||
"submitCode": "verzenden",
|
"submitCode": "verzenden",
|
||||||
|
"submit": "verzenden",
|
||||||
"members": "Leden",
|
"members": "Leden",
|
||||||
"themes": "Lesthema's",
|
"themes": "Lesthema's",
|
||||||
"choose-theme": "Kies een thema",
|
"choose-theme": "Kies een thema",
|
||||||
|
@ -68,22 +69,22 @@
|
||||||
"pick-class": "Kies een klas",
|
"pick-class": "Kies een klas",
|
||||||
"choose-students": "Studenten selecteren",
|
"choose-students": "Studenten selecteren",
|
||||||
"create-group": "Groep aanmaken",
|
"create-group": "Groep aanmaken",
|
||||||
"class": "klas",
|
"class": "Klas",
|
||||||
"delete": "verwijderen",
|
"delete": "verwijderen",
|
||||||
"view-assignment": "Opdracht bekijken",
|
"view-assignment": "Opdracht bekijken",
|
||||||
"code": "code",
|
"code": "Code",
|
||||||
"invitations": "uitnodigingen",
|
"invitations": "Uitnodigingen",
|
||||||
"createClass": "klas aanmaken",
|
"createClass": "Klas aanmaken",
|
||||||
"createClassInstructions": "Voer een naam in voor je klas en klik op create. Er verschijnt een venster met een code die je kunt kopiëren. Geef deze code aan je leerlingen en ze kunnen deelnemen aan je klas.",
|
"createClassInstructions": "Voer een naam in voor je klas en klik op create. Er verschijnt een venster met een code die je kunt kopiëren. Geef deze code aan je leerlingen en ze kunnen deelnemen aan je klas.",
|
||||||
"classname": "klasnaam",
|
"classname": "klasnaam",
|
||||||
"EnterNameOfClass": "Geef een klasnaam op.",
|
"EnterNameOfClass": "Geef een klasnaam op.",
|
||||||
"create": "aanmaken",
|
"create": "aanmaken",
|
||||||
"sender": "afzender",
|
"sender": "Afzender",
|
||||||
"nameIsMandatory": "klasnaam is verplicht",
|
"nameIsMandatory": "klasnaam is verplicht",
|
||||||
"onlyUse": "gebruik enkel letters, cijfers, dashes (-) en underscores (_)",
|
"onlyUse": "gebruik enkel letters, cijfers, dashes (-) en underscores (_)",
|
||||||
"close": "sluiten",
|
"close": "sluiten",
|
||||||
"copied": "gekopieerd!",
|
"copied": "gekopieerd!",
|
||||||
"accept": "accepteren",
|
"accept": "Accepteren",
|
||||||
"deny": "weigeren",
|
"deny": "weigeren",
|
||||||
"sent": "verzonden",
|
"sent": "verzonden",
|
||||||
"failed": "mislukt",
|
"failed": "mislukt",
|
||||||
|
@ -108,12 +109,12 @@
|
||||||
"submission": "Indiening",
|
"submission": "Indiening",
|
||||||
"progress": "Vooruitgang",
|
"progress": "Vooruitgang",
|
||||||
"remove": "verwijder",
|
"remove": "verwijder",
|
||||||
"students": "studenten",
|
"students": "Studenten",
|
||||||
"classJoinRequests": "deelname verzoeken",
|
"classJoinRequests": "Deelname verzoeken",
|
||||||
"reject": "weiger",
|
"reject": "Weiger",
|
||||||
"areusure": "Bent u zeker?",
|
"areusure": "Bent u zeker?",
|
||||||
"yes": "ja",
|
"yes": "ja",
|
||||||
"teachers": "leerkrachten",
|
"teachers": "Leerkrachten",
|
||||||
"accepted": "geaccepteerd",
|
"accepted": "geaccepteerd",
|
||||||
"rejected": "geweigerd",
|
"rejected": "geweigerd",
|
||||||
"enterUsername": "vul de gebruikersnaam van de leerkracht die je wilt uitnodigen in",
|
"enterUsername": "vul de gebruikersnaam van de leerkracht die je wilt uitnodigen in",
|
||||||
|
@ -121,5 +122,18 @@
|
||||||
"invite": "uitnodigen",
|
"invite": "uitnodigen",
|
||||||
"assignmentIndicator": "OPDRACHT",
|
"assignmentIndicator": "OPDRACHT",
|
||||||
"searchAllLearningPathsTitle": "Alle leerpaden doorzoeken",
|
"searchAllLearningPathsTitle": "Alle leerpaden doorzoeken",
|
||||||
"searchAllLearningPathsDescription": "Niet gevonden waar je naar op zoek was? Klik hier om onze volledige databank van beschikbare leerpaden te doorzoeken."
|
"searchAllLearningPathsDescription": "Niet gevonden waar je naar op zoek was? Klik hier om onze volledige databank van beschikbare leerpaden te doorzoeken.",
|
||||||
|
"no-students-found": "Deze klas heeft geen leerlingen.",
|
||||||
|
"no-invitations-found": "U heeft geen openstaande uitnodigingen.",
|
||||||
|
"no-join-requests-found": "Er zijn geen openstaande verzoeken om lid te worden van deze klas.",
|
||||||
|
"no-classes-found": "U maakt nog geen deel uit van een klas.",
|
||||||
|
"classCreated": "Klas aangemaakt!",
|
||||||
|
"success": "succes",
|
||||||
|
"submitted": "ingediend",
|
||||||
|
"see-submission": "inzending bekijken",
|
||||||
|
"view-submissions": "inzendingen bekijken",
|
||||||
|
"valid-username": "voer een geldige gebruikersnaam in",
|
||||||
|
"creationFailed": "aanmaak mislukt, probeer het opnieuw",
|
||||||
|
"no-assignments": "Er zijn momenteel geen opdrachten.",
|
||||||
|
"deadline": "deadline"
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,12 +22,13 @@ export function useGetLearningPathQuery(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGetAllLearningPathsByThemeQuery(
|
export function useGetAllLearningPathsByThemeAndLanguageQuery(
|
||||||
theme: MaybeRefOrGetter<string>,
|
theme: MaybeRefOrGetter<string>,
|
||||||
|
language: MaybeRefOrGetter<Language>,
|
||||||
): UseQueryReturnType<LearningPath[], Error> {
|
): UseQueryReturnType<LearningPath[], Error> {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme],
|
queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme, language],
|
||||||
queryFn: async () => learningPathController.getAllByTheme(toValue(theme)),
|
queryFn: async () => learningPathController.getAllByThemeAndLanguage(toValue(theme), toValue(language)),
|
||||||
enabled: () => Boolean(toValue(theme)),
|
enabled: () => Boolean(toValue(theme)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts";
|
import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts";
|
||||||
import type { ClassesResponse } from "@/controllers/classes.ts";
|
import type { ClassesResponse } from "@/controllers/classes.ts";
|
||||||
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
||||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
|
||||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||||
import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts";
|
import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts";
|
||||||
|
|
||||||
|
@ -33,10 +32,6 @@ function teacherStudentsQueryKey(username: string, full: boolean): [string, stri
|
||||||
return ["teacher-students", username, full];
|
return ["teacher-students", username, full];
|
||||||
}
|
}
|
||||||
|
|
||||||
function teacherQuestionsQueryKey(username: string, full: boolean): [string, string, boolean] {
|
|
||||||
return ["teacher-questions", username, full];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function teacherClassJoinRequests(classId: string): [string, string] {
|
export function teacherClassJoinRequests(classId: string): [string, string] {
|
||||||
return ["teacher-class-join-requests", classId];
|
return ["teacher-class-join-requests", classId];
|
||||||
}
|
}
|
||||||
|
@ -80,17 +75,6 @@ export function useTeacherStudentsQuery(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTeacherQuestionsQuery(
|
|
||||||
username: MaybeRefOrGetter<string | undefined>,
|
|
||||||
full: MaybeRefOrGetter<boolean> = false,
|
|
||||||
): UseQueryReturnType<QuestionsResponse, Error> {
|
|
||||||
return useQuery({
|
|
||||||
queryKey: computed(() => teacherQuestionsQueryKey(toValue(username)!, toValue(full))),
|
|
||||||
queryFn: async () => teacherController.getQuestions(toValue(username)!, toValue(full)),
|
|
||||||
enabled: () => Boolean(toValue(username)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useTeacherJoinRequestsQuery(
|
export function useTeacherJoinRequestsQuery(
|
||||||
username: MaybeRefOrGetter<string | undefined>,
|
username: MaybeRefOrGetter<string | undefined>,
|
||||||
classId: MaybeRefOrGetter<string | undefined>,
|
classId: MaybeRefOrGetter<string | undefined>,
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
alt="Dwengo logo"
|
alt="Dwengo logo"
|
||||||
style="align-self: center"
|
style="align-self: center"
|
||||||
/>
|
/>
|
||||||
<h1>{{ t("homeTitle") }}</h1>
|
<h1 class="h1">{{ t("homeTitle") }}</h1>
|
||||||
<p class="info">
|
<p class="info">
|
||||||
{{ t("homeIntroduction1") }}
|
{{ t("homeIntroduction1") }}
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -2,10 +2,11 @@
|
||||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||||
import LearningPathsGrid from "@/components/LearningPathsGrid.vue";
|
import LearningPathsGrid from "@/components/LearningPathsGrid.vue";
|
||||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||||
import { useGetAllLearningPathsByThemeQuery } from "@/queries/learning-paths.ts";
|
import { useGetAllLearningPathsByThemeAndLanguageQuery } from "@/queries/learning-paths.ts";
|
||||||
import { computed, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { useThemeQuery } from "@/queries/themes.ts";
|
import { useThemeQuery } from "@/queries/themes.ts";
|
||||||
|
import type { Language } from "@/data-objects/language";
|
||||||
|
|
||||||
const props = defineProps<{ theme: string }>();
|
const props = defineProps<{ theme: string }>();
|
||||||
|
|
||||||
|
@ -16,7 +17,10 @@
|
||||||
|
|
||||||
const currentThemeInfo = computed(() => themeQueryResult.data.value?.find((it) => it.key === props.theme));
|
const currentThemeInfo = computed(() => themeQueryResult.data.value?.find((it) => it.key === props.theme));
|
||||||
|
|
||||||
const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeQuery(() => props.theme);
|
const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeAndLanguageQuery(
|
||||||
|
() => props.theme,
|
||||||
|
() => locale.value as Language,
|
||||||
|
);
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const searchFilter = ref("");
|
const searchFilter = ref("");
|
||||||
|
|
|
@ -48,7 +48,7 @@
|
||||||
|
|
||||||
// Disable combobox when learningPath prop is passed
|
// Disable combobox when learningPath prop is passed
|
||||||
const lpIsSelected = route.query.hruid !== undefined;
|
const lpIsSelected = route.query.hruid !== undefined;
|
||||||
const deadline = ref(null);
|
const deadline = ref(new Date());
|
||||||
const description = ref("");
|
const description = ref("");
|
||||||
const groups = ref<string[][]>([]);
|
const groups = ref<string[][]>([]);
|
||||||
|
|
||||||
|
@ -86,6 +86,7 @@
|
||||||
title: assignmentTitle.value,
|
title: assignmentTitle.value,
|
||||||
description: description.value,
|
description: description.value,
|
||||||
learningPath: lp || "",
|
learningPath: lp || "",
|
||||||
|
deadline: deadline.value,
|
||||||
language: language.value,
|
language: language.value,
|
||||||
groups: groups.value,
|
groups: groups.value,
|
||||||
};
|
};
|
||||||
|
@ -96,7 +97,7 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<h1 class="title">{{ t("new-assignment") }}</h1>
|
<h1 class="h1">{{ t("new-assignment") }}</h1>
|
||||||
<v-card class="form-card">
|
<v-card class="form-card">
|
||||||
<v-form
|
<v-form
|
||||||
ref="form"
|
ref="form"
|
||||||
|
|
|
@ -9,8 +9,9 @@
|
||||||
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
import type { ClassDTO } from "@dwengo-1/common/interfaces/class";
|
||||||
import { asyncComputed } from "@vueuse/core";
|
import { asyncComputed } from "@vueuse/core";
|
||||||
import { useDeleteAssignmentMutation } from "@/queries/assignments.ts";
|
import { useDeleteAssignmentMutation } from "@/queries/assignments.ts";
|
||||||
|
import "../../assets/common.css";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const role = ref(auth.authState.activeRole);
|
const role = ref(auth.authState.activeRole);
|
||||||
|
@ -27,13 +28,13 @@
|
||||||
classesQueryResults = useStudentClassesQuery(username, true);
|
classesQueryResults = useStudentClassesQuery(username, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: remove later
|
|
||||||
const classController = new ClassController();
|
const classController = new ClassController();
|
||||||
|
|
||||||
//TODO: replace by query that fetches all user's assignment
|
const assignments = asyncComputed(
|
||||||
const assignments = asyncComputed(async () => {
|
async () => {
|
||||||
const classes = classesQueryResults?.data?.value?.classes;
|
const classes = classesQueryResults?.data?.value?.classes;
|
||||||
if (!classes) return [];
|
if (!classes) return [];
|
||||||
|
|
||||||
const result = await Promise.all(
|
const result = await Promise.all(
|
||||||
(classes as ClassDTO[]).map(async (cls) => {
|
(classes as ClassDTO[]).map(async (cls) => {
|
||||||
const { assignments } = await classController.getAssignments(cls.id);
|
const { assignments } = await classController.getAssignments(cls.id);
|
||||||
|
@ -44,13 +45,30 @@
|
||||||
description: a.description,
|
description: a.description,
|
||||||
learningPath: a.learningPath,
|
learningPath: a.learningPath,
|
||||||
language: a.language,
|
language: a.language,
|
||||||
|
deadline: a.deadline,
|
||||||
groups: a.groups,
|
groups: a.groups,
|
||||||
}));
|
}));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
return result.flat();
|
// Order the assignments by deadline
|
||||||
}, []);
|
return result.flat().sort((a, b) => {
|
||||||
|
const now = Date.now();
|
||||||
|
const aTime = new Date(a.deadline).getTime();
|
||||||
|
const bTime = new Date(b.deadline).getTime();
|
||||||
|
|
||||||
|
const aIsPast = aTime < now;
|
||||||
|
const bIsPast = bTime < now;
|
||||||
|
|
||||||
|
if (aIsPast && !bIsPast) return 1;
|
||||||
|
if (!aIsPast && bIsPast) return -1;
|
||||||
|
|
||||||
|
return aTime - bTime;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
{ evaluating: true },
|
||||||
|
);
|
||||||
|
|
||||||
async function goToCreateAssignment(): Promise<void> {
|
async function goToCreateAssignment(): Promise<void> {
|
||||||
await router.push("/assignment/create");
|
await router.push("/assignment/create");
|
||||||
|
@ -72,6 +90,35 @@
|
||||||
mutate({ cid: clsId, an: num });
|
mutate({ cid: clsId, an: num });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatDate(date?: string | Date): string {
|
||||||
|
if (!date) return "–";
|
||||||
|
const d = new Date(date);
|
||||||
|
|
||||||
|
// Choose locale based on selected language
|
||||||
|
const currentLocale = locale.value;
|
||||||
|
|
||||||
|
return d.toLocaleDateString(currentLocale, {
|
||||||
|
weekday: "short",
|
||||||
|
day: "2-digit",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "numeric",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDeadlineClass(deadline?: string | Date): string {
|
||||||
|
if (!deadline) return "";
|
||||||
|
|
||||||
|
const date = new Date(deadline);
|
||||||
|
const now = new Date();
|
||||||
|
const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
if (date.getTime() < now.getTime()) return "deadline-passed";
|
||||||
|
if (date.getTime() <= in24Hours.getTime()) return "deadline-in24hours";
|
||||||
|
return "deadline-upcoming";
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const user = await auth.loadUser();
|
const user = await auth.loadUser();
|
||||||
username.value = user?.profile?.preferred_username ?? "";
|
username.value = user?.profile?.preferred_username ?? "";
|
||||||
|
@ -80,7 +127,7 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="assignments-container">
|
<div class="assignments-container">
|
||||||
<h1>{{ t("assignments") }}</h1>
|
<h1 class="h1">{{ t("assignments") }}</h1>
|
||||||
|
|
||||||
<v-btn
|
<v-btn
|
||||||
v-if="isTeacher"
|
v-if="isTeacher"
|
||||||
|
@ -107,6 +154,13 @@
|
||||||
{{ assignment.class.displayName }}
|
{{ assignment.class.displayName }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="assignment-deadline"
|
||||||
|
:class="getDeadlineClass(assignment.deadline)"
|
||||||
|
>
|
||||||
|
{{ t("deadline") }}:
|
||||||
|
<span>{{ formatDate(assignment.deadline) }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
|
@ -131,6 +185,13 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
<v-row v-if="assignments.length === 0">
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="no-assignments">
|
||||||
|
{{ t("no-assignments") }}
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -139,25 +200,32 @@
|
||||||
.assignments-container {
|
.assignments-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2% 4%;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #0e6942;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bolder;
|
|
||||||
font-size: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center-btn {
|
.center-btn {
|
||||||
display: block;
|
display: block;
|
||||||
margin-left: auto;
|
margin: 0 auto 2rem auto;
|
||||||
margin-right: auto;
|
font-weight: 600;
|
||||||
|
background-color: #10ad61;
|
||||||
|
color: white;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.center-btn:hover {
|
||||||
|
background-color: #0e6942;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assignment-card {
|
.assignment-card {
|
||||||
padding: 1rem;
|
padding: 1.25rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
background-color: white;
|
||||||
|
transition:
|
||||||
|
transform 0.2s,
|
||||||
|
box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
.assignment-card:hover {
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.top-content {
|
.top-content {
|
||||||
|
@ -165,6 +233,35 @@
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assignment-title {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
color: #0e6942;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-class,
|
||||||
|
.assignment-deadline {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #444;
|
||||||
|
margin-bottom: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #097180;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-deadline.deadline-passed {
|
||||||
|
color: #d32f2f;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assignment-deadline.deadline-in24hours {
|
||||||
|
color: #f57c00;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
@ -172,24 +269,14 @@
|
||||||
.button-row {
|
.button-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 0.5rem;
|
gap: 0.75rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assignment-title {
|
.no-assignments {
|
||||||
font-weight: bold;
|
text-align: center;
|
||||||
font-size: 1.5rem;
|
font-size: 1.2rem;
|
||||||
margin-bottom: 0.1rem;
|
color: #777;
|
||||||
word-break: break-word;
|
padding: 3rem 0;
|
||||||
}
|
|
||||||
|
|
||||||
.assignment-class {
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.class-name {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
import { useCreateTeacherInvitationMutation } from "@/queries/teacher-invitations";
|
import { useCreateTeacherInvitationMutation } from "@/queries/teacher-invitations";
|
||||||
import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation";
|
import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation";
|
||||||
import { useDisplay } from "vuetify";
|
import { useDisplay } from "vuetify";
|
||||||
|
import "../../assets/common.css";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
@ -112,7 +113,7 @@
|
||||||
|
|
||||||
function sentInvite(): void {
|
function sentInvite(): void {
|
||||||
if (!usernameTeacher.value) {
|
if (!usernameTeacher.value) {
|
||||||
showSnackbar(t("please enter a valid username"), "error");
|
showSnackbar(t("valid-username"), "error");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data: TeacherInvitationData = {
|
const data: TeacherInvitationData = {
|
||||||
|
@ -186,7 +187,7 @@
|
||||||
v-slot="classResponse: { data: ClassResponse }"
|
v-slot="classResponse: { data: ClassResponse }"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="title">{{ classResponse.data.class.displayName }}</h1>
|
<h1 class="h1">{{ classResponse.data.class.displayName }}</h1>
|
||||||
<using-query-result
|
<using-query-result
|
||||||
:query-result="getStudents"
|
:query-result="getStudents"
|
||||||
v-slot="studentsResponse: { data: StudentsResponse }"
|
v-slot="studentsResponse: { data: StudentsResponse }"
|
||||||
|
@ -211,19 +212,34 @@
|
||||||
<th class="header"></th>
|
<th class="header"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
|
||||||
|
<tbody v-if="studentsResponse.data.students.length">
|
||||||
<tr
|
<tr
|
||||||
v-for="s in studentsResponse.data.students as StudentDTO[]"
|
v-for="s in studentsResponse.data.students as StudentDTO[]"
|
||||||
:key="s.id"
|
:key="s.id"
|
||||||
>
|
>
|
||||||
<td>
|
<td>{{ s.firstName + " " + s.lastName }}</td>
|
||||||
{{ s.firstName + " " + s.lastName }}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<v-btn @click="showPopup(s)">{{ t("remove") }}</v-btn>
|
<v-btn @click="showPopup(s)">{{ t("remove") }}</v-btn>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
|
<tbody v-else>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colspan="2"
|
||||||
|
class="empty-message"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-information-outline"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
</v-icon>
|
||||||
|
{{ t("no-students-found") }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</v-table>
|
</v-table>
|
||||||
</v-col>
|
</v-col>
|
||||||
<using-query-result
|
<using-query-result
|
||||||
|
@ -242,7 +258,7 @@
|
||||||
<th class="header">{{ t("accept") + "/" + t("reject") }}</th>
|
<th class="header">{{ t("accept") + "/" + t("reject") }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody v-if="joinRequests.data.joinRequests.length">
|
||||||
<tr
|
<tr
|
||||||
v-for="jr in joinRequests.data.joinRequests as ClassJoinRequestDTO[]"
|
v-for="jr in joinRequests.data.joinRequests as ClassJoinRequestDTO[]"
|
||||||
:key="(jr.class, jr.requester, jr.status)"
|
:key="(jr.class, jr.requester, jr.status)"
|
||||||
|
@ -287,6 +303,21 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tbody v-else>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colspan="2"
|
||||||
|
class="empty-message"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-information-outline"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
</v-icon>
|
||||||
|
{{ t("no-join-requests-found") }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</v-table>
|
</v-table>
|
||||||
</v-col>
|
</v-col>
|
||||||
</using-query-result>
|
</using-query-result>
|
||||||
|
@ -356,49 +387,6 @@
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
|
||||||
font-weight: bold !important;
|
|
||||||
background-color: #0e6942;
|
|
||||||
color: white;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table thead th:first-child {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table thead th:last-child {
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tbody tr:nth-child(odd) {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tbody tr:nth-child(even) {
|
|
||||||
background-color: #f6faf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
border-bottom: 1px solid #0e6942;
|
|
||||||
border-top: 1px solid #0e6942;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
width: 90%;
|
|
||||||
padding-top: 10px;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #0e6942;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bolder;
|
|
||||||
padding-top: 2%;
|
|
||||||
font-size: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: #0e6942;
|
color: #0e6942;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
|
@ -407,6 +395,7 @@
|
||||||
.join {
|
.join {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
margin-left: 1%;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
@ -416,16 +405,7 @@
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px) {
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join {
|
.join {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes";
|
import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes";
|
||||||
import type { StudentsResponse } from "@/controllers/students";
|
import type { StudentsResponse } from "@/controllers/students";
|
||||||
import type { TeachersResponse } from "@/controllers/teachers";
|
import type { TeachersResponse } from "@/controllers/teachers";
|
||||||
|
import "../../assets/common.css";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
@ -135,7 +136,7 @@
|
||||||
></v-empty-state>
|
></v-empty-state>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h1 class="title">{{ t("classes") }}</h1>
|
<h1 class="h1">{{ t("classes") }}</h1>
|
||||||
<using-query-result
|
<using-query-result
|
||||||
:query-result="classesQuery"
|
:query-result="classesQuery"
|
||||||
v-slot="classResponse: { data: ClassesResponse }"
|
v-slot="classResponse: { data: ClassesResponse }"
|
||||||
|
@ -161,7 +162,7 @@
|
||||||
<th class="header">{{ t("members") }}</th>
|
<th class="header">{{ t("members") }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody v-if="classResponse.data.classes.length">
|
||||||
<tr
|
<tr
|
||||||
v-for="c in classResponse.data.classes as ClassDTO[]"
|
v-for="c in classResponse.data.classes as ClassDTO[]"
|
||||||
:key="c.id"
|
:key="c.id"
|
||||||
|
@ -181,6 +182,21 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tbody v-else>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colspan="3"
|
||||||
|
class="empty-message"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-information-outline"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
</v-icon>
|
||||||
|
{{ t("no-classes-found") }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</v-table>
|
</v-table>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
@ -271,49 +287,6 @@
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
|
||||||
font-weight: bold !important;
|
|
||||||
background-color: #0e6942;
|
|
||||||
color: white;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table thead th:first-child {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table thead th:last-child {
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tbody tr:nth-child(odd) {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tbody tr:nth-child(even) {
|
|
||||||
background-color: #f6faf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
border-bottom: 1px solid #0e6942;
|
|
||||||
border-top: 1px solid #0e6942;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
width: 90%;
|
|
||||||
padding-top: 10px;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #0e6942;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bolder;
|
|
||||||
padding-top: 2%;
|
|
||||||
font-size: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: #0e6942;
|
color: #0e6942;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
|
@ -321,6 +294,7 @@
|
||||||
|
|
||||||
.join {
|
.join {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
margin-left: 1%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
|
@ -331,16 +305,7 @@
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px) {
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join {
|
.join {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
useTeacherInvitationsReceivedQuery,
|
useTeacherInvitationsReceivedQuery,
|
||||||
} from "@/queries/teacher-invitations";
|
} from "@/queries/teacher-invitations";
|
||||||
import { useDisplay } from "vuetify";
|
import { useDisplay } from "vuetify";
|
||||||
|
import "../../assets/common.css";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
@ -112,7 +113,7 @@
|
||||||
dialog.value = true;
|
dialog.value = true;
|
||||||
}
|
}
|
||||||
if (!className.value || className.value === "") {
|
if (!className.value || className.value === "") {
|
||||||
showSnackbar(t("name is mandatory"), "error");
|
showSnackbar(t("nameIsMandatory"), "error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +138,13 @@
|
||||||
copied.value = true;
|
copied.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyCode(selectedCode: string): Promise<void> {
|
||||||
|
code.value = selectedCode;
|
||||||
|
await copyToClipboard();
|
||||||
|
showSnackbar(t("copied"), "white");
|
||||||
|
copied.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
// Custom breakpoints
|
// Custom breakpoints
|
||||||
const customBreakpoints = {
|
const customBreakpoints = {
|
||||||
xs: 0,
|
xs: 0,
|
||||||
|
@ -183,7 +191,7 @@
|
||||||
></v-empty-state>
|
></v-empty-state>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<h1 class="title">{{ t("classes") }}</h1>
|
<h1 class="h1">{{ t("classes") }}</h1>
|
||||||
<using-query-result
|
<using-query-result
|
||||||
:query-result="classesQuery"
|
:query-result="classesQuery"
|
||||||
v-slot="classesResponse: { data: ClassesResponse }"
|
v-slot="classesResponse: { data: ClassesResponse }"
|
||||||
|
@ -212,7 +220,7 @@
|
||||||
<th class="header">{{ t("members") }}</th>
|
<th class="header">{{ t("members") }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody v-if="classesResponse.data.classes.length">
|
||||||
<tr
|
<tr
|
||||||
v-for="c in classesResponse.data.classes as ClassDTO[]"
|
v-for="c in classesResponse.data.classes as ClassDTO[]"
|
||||||
:key="c.id"
|
:key="c.id"
|
||||||
|
@ -227,7 +235,14 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span v-if="!isMdAndDown">{{ c.id }}</span>
|
<v-btn
|
||||||
|
v-if="!isMdAndDown"
|
||||||
|
variant="text"
|
||||||
|
append-icon="mdi-content-copy"
|
||||||
|
@click="copyCode(c.id)"
|
||||||
|
>
|
||||||
|
{{ c.id }}
|
||||||
|
</v-btn>
|
||||||
<span
|
<span
|
||||||
v-else
|
v-else
|
||||||
style="cursor: pointer"
|
style="cursor: pointer"
|
||||||
|
@ -239,6 +254,21 @@
|
||||||
<td>{{ c.students.length }}</td>
|
<td>{{ c.students.length }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
<tbody v-else>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colspan="3"
|
||||||
|
class="empty-message"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-information-outline"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
</v-icon>
|
||||||
|
{{ t("no-classes-found") }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
</v-table>
|
</v-table>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col
|
<v-col
|
||||||
|
@ -318,7 +348,7 @@
|
||||||
</v-container>
|
</v-container>
|
||||||
</using-query-result>
|
</using-query-result>
|
||||||
|
|
||||||
<h1 class="title">
|
<h1 class="h1">
|
||||||
{{ t("invitations") }}
|
{{ t("invitations") }}
|
||||||
</h1>
|
</h1>
|
||||||
<v-container
|
<v-container
|
||||||
|
@ -342,6 +372,7 @@
|
||||||
:query-result="allClassesQuery"
|
:query-result="allClassesQuery"
|
||||||
v-slot="classesResponse: { data: ClassesResponse }"
|
v-slot="classesResponse: { data: ClassesResponse }"
|
||||||
>
|
>
|
||||||
|
<template v-if="invitationsResponse.data.invitations.length">
|
||||||
<tr
|
<tr
|
||||||
v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]"
|
v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]"
|
||||||
:key="i.classId"
|
:key="i.classId"
|
||||||
|
@ -355,7 +386,9 @@
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{
|
{{
|
||||||
(i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName
|
(i.sender as TeacherDTO).firstName +
|
||||||
|
" " +
|
||||||
|
(i.sender as TeacherDTO).lastName
|
||||||
}}
|
}}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
|
@ -397,6 +430,22 @@
|
||||||
></span>
|
></span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<tr>
|
||||||
|
<td
|
||||||
|
colspan="3"
|
||||||
|
class="empty-message"
|
||||||
|
>
|
||||||
|
<v-icon
|
||||||
|
icon="mdi-information-outline"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
</v-icon>
|
||||||
|
{{ t("no-invitations-found") }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
</using-query-result>
|
</using-query-result>
|
||||||
</using-query-result>
|
</using-query-result>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -449,49 +498,6 @@
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header {
|
|
||||||
font-weight: bold !important;
|
|
||||||
background-color: #0e6942;
|
|
||||||
color: white;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
table thead th:first-child {
|
|
||||||
border-top-left-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table thead th:last-child {
|
|
||||||
border-top-right-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tbody tr:nth-child(odd) {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table tbody tr:nth-child(even) {
|
|
||||||
background-color: #f6faf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
td,
|
|
||||||
th {
|
|
||||||
border-bottom: 1px solid #0e6942;
|
|
||||||
border-top: 1px solid #0e6942;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table {
|
|
||||||
width: 90%;
|
|
||||||
padding-top: 10px;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #0e6942;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bolder;
|
|
||||||
padding-top: 2%;
|
|
||||||
font-size: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
color: #0e6942;
|
color: #0e6942;
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
|
@ -509,16 +515,7 @@
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
|
||||||
margin-left: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 850px) {
|
@media screen and (max-width: 850px) {
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.join {
|
.join {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -541,10 +538,6 @@
|
||||||
flex-direction: column !important;
|
flex-direction: column !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.responsive-col {
|
.responsive-col {
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
flex-basis: 100% !important;
|
flex-basis: 100% !important;
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { THEMESITEMS, AGE_TO_THEMES } from "@/utils/constants.ts";
|
import { THEMESITEMS, AGE_TO_THEMES } from "@/utils/constants.ts";
|
||||||
import BrowseThemes from "@/components/BrowseThemes.vue";
|
import BrowseThemes from "@/components/BrowseThemes.vue";
|
||||||
|
import "../../assets/common.css";
|
||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
|
|
||||||
|
@ -46,7 +47,7 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main-container">
|
<div class="main-container">
|
||||||
<h1 class="title">{{ t("themes") }}</h1>
|
<h1 class="h1">{{ t("themes") }}</h1>
|
||||||
<v-container class="dropdowns">
|
<v-container class="dropdowns">
|
||||||
<v-select
|
<v-select
|
||||||
class="v-select"
|
class="v-select"
|
||||||
|
@ -77,31 +78,6 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main-container {
|
|
||||||
min-height: 100vh;
|
|
||||||
min-width: 100vw;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
max-width: 50rem;
|
|
||||||
margin-left: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
text-align: center;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
color: #0e6942;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: bolder;
|
|
||||||
font-size: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropdowns {
|
.dropdowns {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
@ -114,12 +90,6 @@
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.main-container {
|
|
||||||
padding: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
.dropdowns {
|
.dropdowns {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -17,32 +17,52 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="search-field-container">
|
<v-container class="search-page-container">
|
||||||
<learning-path-search-field class="search-field"></learning-path-search-field>
|
<v-row
|
||||||
</div>
|
justify="center"
|
||||||
|
class="mb-6"
|
||||||
|
>
|
||||||
|
<v-col
|
||||||
|
cols="12"
|
||||||
|
sm="8"
|
||||||
|
md="6"
|
||||||
|
lg="4"
|
||||||
|
>
|
||||||
|
<learning-path-search-field class="search-field" />
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row justify="center">
|
||||||
|
<v-col cols="12">
|
||||||
<using-query-result
|
<using-query-result
|
||||||
:query-result="searchQueryResults"
|
:query-result="searchQueryResults"
|
||||||
v-slot="{ data }: { data: LearningPath[] }"
|
v-slot="{ data }: { data: LearningPath[] }"
|
||||||
>
|
>
|
||||||
<learning-paths-grid :learning-paths="data"></learning-paths-grid>
|
<learning-paths-grid :learning-paths="data" />
|
||||||
</using-query-result>
|
</using-query-result>
|
||||||
<div content="empty-state-container">
|
|
||||||
<v-empty-state
|
<div
|
||||||
v-if="!query"
|
v-if="!query"
|
||||||
|
class="empty-state-container"
|
||||||
|
>
|
||||||
|
<v-empty-state
|
||||||
icon="mdi-magnify"
|
icon="mdi-magnify"
|
||||||
:title="t('enterSearchTerm')"
|
:title="t('enterSearchTerm')"
|
||||||
:text="t('enterSearchTermDescription')"
|
:text="t('enterSearchTermDescription')"
|
||||||
></v-empty-state>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.search-field-container {
|
.search-page-container {
|
||||||
display: block;
|
padding-top: 40px;
|
||||||
margin: 20px;
|
padding-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-field {
|
.search-field {
|
||||||
max-width: 300px;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -15,7 +15,7 @@ describe("Test controller learning paths", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Can get learning path by id", async () => {
|
it("Can get learning path by id", async () => {
|
||||||
const data = await controller.getAllByTheme("kiks");
|
const data = await controller.getAllByThemeAndLanguage("kiks", Language.Dutch);
|
||||||
expect(data).to.have.length.greaterThan(0);
|
expect(data).to.have.length.greaterThan(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
132
package-lock.json
generated
132
package-lock.json
generated
|
@ -1902,15 +1902,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.11.2",
|
"version": "1.11.3",
|
||||||
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz",
|
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.3.tgz",
|
||||||
"integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==",
|
"integrity": "sha512-rmOWVRUbUJD7iSvJugjUbFZshTAuJ48MXoZ80Osx1GM0K/H1w7rSEvmw8m6vdWxNASgtaHIhAgre4H/E9GJiYQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"content-type": "^1.0.5",
|
"content-type": "^1.0.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.5",
|
||||||
"eventsource": "^3.0.2",
|
"eventsource": "^3.0.2",
|
||||||
"express": "^5.0.1",
|
"express": "^5.0.1",
|
||||||
"express-rate-limit": "^7.5.0",
|
"express-rate-limit": "^7.5.0",
|
||||||
|
@ -3465,16 +3465,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-core": {
|
"node_modules/@vue/compiler-core": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.14.tgz",
|
||||||
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
|
"integrity": "sha512-k7qMHMbKvoCXIxPhquKQVw3Twid3Kg4s7+oYURxLGRd56LiuHJVrvFKI4fm2AM3c8apqODPfVJGoh8nePbXMRA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.25.3",
|
"@babel/parser": "^7.27.2",
|
||||||
"@vue/shared": "3.5.13",
|
"@vue/shared": "3.5.14",
|
||||||
"entities": "^4.5.0",
|
"entities": "^4.5.0",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"source-map-js": "^1.2.0"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-core/node_modules/entities": {
|
"node_modules/@vue/compiler-core/node_modules/entities": {
|
||||||
|
@ -3490,40 +3490,40 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-dom": {
|
"node_modules/@vue/compiler-dom": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.14.tgz",
|
||||||
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
|
"integrity": "sha512-1aOCSqxGOea5I80U2hQJvXYpPm/aXo95xL/m/mMhgyPUsKe9jhjwWpziNAw7tYRnbz1I61rd9Mld4W9KmmRoug==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-core": "3.5.13",
|
"@vue/compiler-core": "3.5.14",
|
||||||
"@vue/shared": "3.5.13"
|
"@vue/shared": "3.5.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-sfc": {
|
"node_modules/@vue/compiler-sfc": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.14.tgz",
|
||||||
"integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
|
"integrity": "sha512-9T6m/9mMr81Lj58JpzsiSIjBgv2LiVoWjIVa7kuXHICUi8LiDSIotMpPRXYJsXKqyARrzjT24NAwttrMnMaCXA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.25.3",
|
"@babel/parser": "^7.27.2",
|
||||||
"@vue/compiler-core": "3.5.13",
|
"@vue/compiler-core": "3.5.14",
|
||||||
"@vue/compiler-dom": "3.5.13",
|
"@vue/compiler-dom": "3.5.14",
|
||||||
"@vue/compiler-ssr": "3.5.13",
|
"@vue/compiler-ssr": "3.5.14",
|
||||||
"@vue/shared": "3.5.13",
|
"@vue/shared": "3.5.14",
|
||||||
"estree-walker": "^2.0.2",
|
"estree-walker": "^2.0.2",
|
||||||
"magic-string": "^0.30.11",
|
"magic-string": "^0.30.17",
|
||||||
"postcss": "^8.4.48",
|
"postcss": "^8.5.3",
|
||||||
"source-map-js": "^1.2.0"
|
"source-map-js": "^1.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-ssr": {
|
"node_modules/@vue/compiler-ssr": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.14.tgz",
|
||||||
"integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
|
"integrity": "sha512-Y0G7PcBxr1yllnHuS/NxNCSPWnRGH4Ogrp0tsLA5QemDZuJLs99YjAKQ7KqkHE0vCg4QTKlQzXLKCMF7WPSl7Q==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.13",
|
"@vue/compiler-dom": "3.5.14",
|
||||||
"@vue/shared": "3.5.13"
|
"@vue/shared": "3.5.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/compiler-vue2": {
|
"node_modules/@vue/compiler-vue2": {
|
||||||
|
@ -3654,53 +3654,53 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/reactivity": {
|
"node_modules/@vue/reactivity": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.14.tgz",
|
||||||
"integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
|
"integrity": "sha512-7cK1Hp343Fu/SUCCO52vCabjvsYu7ZkOqyYu7bXV9P2yyfjUMUXHZafEbq244sP7gf+EZEz+77QixBTuEqkQQw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/shared": "3.5.13"
|
"@vue/shared": "3.5.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-core": {
|
"node_modules/@vue/runtime-core": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.14.tgz",
|
||||||
"integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
|
"integrity": "sha512-w9JWEANwHXNgieAhxPpEpJa+0V5G0hz3NmjAZwlOebtfKyp2hKxKF0+qSh0Xs6/PhfGihuSdqMprMVcQU/E6ag==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.13",
|
"@vue/reactivity": "3.5.14",
|
||||||
"@vue/shared": "3.5.13"
|
"@vue/shared": "3.5.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/runtime-dom": {
|
"node_modules/@vue/runtime-dom": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.14.tgz",
|
||||||
"integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
|
"integrity": "sha512-lCfR++IakeI35TVR80QgOelsUIdcKjd65rWAMfdSlCYnaEY5t3hYwru7vvcWaqmrK+LpI7ZDDYiGU5V3xjMacw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/reactivity": "3.5.13",
|
"@vue/reactivity": "3.5.14",
|
||||||
"@vue/runtime-core": "3.5.13",
|
"@vue/runtime-core": "3.5.14",
|
||||||
"@vue/shared": "3.5.13",
|
"@vue/shared": "3.5.14",
|
||||||
"csstype": "^3.1.3"
|
"csstype": "^3.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/server-renderer": {
|
"node_modules/@vue/server-renderer": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.14.tgz",
|
||||||
"integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
|
"integrity": "sha512-Rf/ISLqokIvcySIYnv3tNWq40PLpNLDLSJwwVWzG6MNtyIhfbcrAxo5ZL9nARJhqjZyWWa40oRb2IDuejeuv6w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-ssr": "3.5.13",
|
"@vue/compiler-ssr": "3.5.14",
|
||||||
"@vue/shared": "3.5.13"
|
"@vue/shared": "3.5.14"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"vue": "3.5.13"
|
"vue": "3.5.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vue/shared": {
|
"node_modules/@vue/shared": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.14.tgz",
|
||||||
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
|
"integrity": "sha512-oXTwNxVfc9EtP1zzXAlSlgARLXNC84frFYkS0HHz0h3E4WZSP9sywqjqzGCP9Y34M8ipNmd380pVgmMuwELDyQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@vue/test-utils": {
|
"node_modules/@vue/test-utils": {
|
||||||
|
@ -5106,9 +5106,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.153",
|
"version": "1.5.155",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.153.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
|
||||||
"integrity": "sha512-4bwluTFwjXZ0/ei1qDpHDGzVveuBfx4wiZ9VQ8j/30+T2JxSF2TfZ00d1X+wNMeDyUdZXgLkJFbarJdAMtd+/w==",
|
"integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
@ -11516,16 +11516,16 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vue": {
|
"node_modules/vue": {
|
||||||
"version": "3.5.13",
|
"version": "3.5.14",
|
||||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
|
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.14.tgz",
|
||||||
"integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
|
"integrity": "sha512-LbOm50/vZFG6Mhy6KscQYXZMQ0LMCC/y40HDJPPvGFQ+i/lUH+PJHR6C3assgOQiXdl6tAfsXHbXYVBZZu65ew==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vue/compiler-dom": "3.5.13",
|
"@vue/compiler-dom": "3.5.14",
|
||||||
"@vue/compiler-sfc": "3.5.13",
|
"@vue/compiler-sfc": "3.5.14",
|
||||||
"@vue/runtime-dom": "3.5.13",
|
"@vue/runtime-dom": "3.5.14",
|
||||||
"@vue/server-renderer": "3.5.13",
|
"@vue/server-renderer": "3.5.14",
|
||||||
"@vue/shared": "3.5.13"
|
"@vue/shared": "3.5.14"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"typescript": "*"
|
"typescript": "*"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue