diff --git a/staff/agustin-birman/project/.gitignore b/staff/agustin-birman/project/.gitignore new file mode 100644 index 000000000..b512c09d4 --- /dev/null +++ b/staff/agustin-birman/project/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/staff/agustin-birman/project/api/.env b/staff/agustin-birman/project/api/.env new file mode 100644 index 000000000..7f4ee0596 --- /dev/null +++ b/staff/agustin-birman/project/api/.env @@ -0,0 +1,4 @@ +PORT = 8080 +JWT_SECRET = hola +MONGODB_URL = mongodb://localhost:27017/project +MONGODB_URL_TEST = mongodb://localhost:27017/test diff --git a/staff/agustin-birman/project/api/.gitignore b/staff/agustin-birman/project/api/.gitignore new file mode 100644 index 000000000..81d8e9630 --- /dev/null +++ b/staff/agustin-birman/project/api/.gitignore @@ -0,0 +1,4 @@ +.node_modules +.coverage +coverage +node_modules diff --git a/staff/agustin-birman/project/api/data/Activity.js b/staff/agustin-birman/project/api/data/Activity.js new file mode 100644 index 000000000..8c299a4b1 --- /dev/null +++ b/staff/agustin-birman/project/api/data/Activity.js @@ -0,0 +1,23 @@ +import { Schema, model, Types } from 'mongoose' + +const { ObjectId } = Types + +const activity = new Schema({ + teacher: { + type: ObjectId, + required: true, + ref: 'User' + }, + title: { + type: String, + required: true + }, + description: { + type: String, + required: false + } +}) + +const Activity = model('Activity', activity) + +export default Activity \ No newline at end of file diff --git a/staff/agustin-birman/project/api/data/Answer.js b/staff/agustin-birman/project/api/data/Answer.js new file mode 100644 index 000000000..12723b285 --- /dev/null +++ b/staff/agustin-birman/project/api/data/Answer.js @@ -0,0 +1,28 @@ +import { Schema, model, Types } from 'mongoose' + +const { ObjectId } = Types + +const answer = new Schema({ + student: { + type: ObjectId, + required: true + }, + exercise: { + type: ObjectId, + required: true, + unique: true + }, + activity: { + type: ObjectId, + required: true + }, + answer: { + type: String, + required: true + } +}) + +const Answer = model('Answer', answer) + +export default Answer + diff --git a/staff/agustin-birman/project/api/data/Exercise.js b/staff/agustin-birman/project/api/data/Exercise.js new file mode 100644 index 000000000..8a1ce4b74 --- /dev/null +++ b/staff/agustin-birman/project/api/data/Exercise.js @@ -0,0 +1,69 @@ +import { Schema, model, Types } from 'mongoose' + +const { ObjectId } = Types + +const exercise = new Schema({ + activity: { + type: ObjectId, + required: true, + ref: 'Activity' + }, + index: { + type: Number, + required: true + }, + type: { + type: String, + enum: ['completeSentence', 'orderSentence', 'vocabulary'] + } +}, { discriminatorKey: 'type' }) + +const Exercise = model('Exercise', exercise) + +const completeSentenceSchema = new Schema({ + sentence: { + type: String, + required: true + }, + answer: { + type: String, + required: true + } +}); + +const CompleteSentenceExercise = Exercise.discriminator('completeSentence', completeSentenceSchema); + +const orderSentenceSchema = new Schema({ + sentence: { + type: String, + required: true + }, + translate: { + type: String, + required: true + }, +}); + +const OrderSentenceExercise = Exercise.discriminator('orderSentence', orderSentenceSchema); + +const vocabularyExerciseSchema = new Schema({ + word: { + type: String, + required: true + }, + answer: { + type: [String], + required: true + } +}); + +const VocabularyExercise = Exercise.discriminator('vocabulary', vocabularyExerciseSchema); + +export { + Exercise, + CompleteSentenceExercise, + OrderSentenceExercise, + VocabularyExercise +} + +export default Exercise \ No newline at end of file diff --git a/staff/agustin-birman/project/api/data/User.js b/staff/agustin-birman/project/api/data/User.js new file mode 100644 index 000000000..567df6963 --- /dev/null +++ b/staff/agustin-birman/project/api/data/User.js @@ -0,0 +1,39 @@ +import { Schema, Types, model } from 'mongoose' +const { ObjectId } = Types + +const user = new Schema({ + name: { + type: String, + required: true + }, + surname: { + type: String, + required: true + }, + email: { + type: String, + required: true, + unique: true + }, + username: { + type: String, + required: true, + unique: true + }, + password: { + type: String, + required: true + }, + userType: { + type: String, + required: true + }, + student: [{ + type: ObjectId, + ref: "User" + }] +}) + +const User = model('User', user) + +export default User \ No newline at end of file diff --git a/staff/agustin-birman/project/api/data/index.js b/staff/agustin-birman/project/api/data/index.js new file mode 100644 index 000000000..088117983 --- /dev/null +++ b/staff/agustin-birman/project/api/data/index.js @@ -0,0 +1,11 @@ +import User from './User.js' +import Activity from './Activity.js' +import Exercise from './Exercise.js' +import Answer from './Answer.js' + +export { + User, + Activity, + Answer, + Exercise, +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/addStudentHandler.js b/staff/agustin-birman/project/api/handlers/addStudentHandler.js new file mode 100644 index 000000000..53ad5ea9c --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/addStudentHandler.js @@ -0,0 +1,31 @@ +import logic from '../logic/index.js' +import jwt from '../util/jsonwebtoken-promised.js' +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + + const { sub: userId } = payload + + const { studentId } = req.body + + try { + logic.addStudent(userId, studentId) + .then(() => res.status(201).send()) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/authenticateUserHandler.js b/staff/agustin-birman/project/api/handlers/authenticateUserHandler.js new file mode 100644 index 000000000..32936dcd2 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/authenticateUserHandler.js @@ -0,0 +1,27 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { SystemError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + const { username, password } = req.body + + try { + logic.authenticateUser(username, password) + .then(user => { + const { id: userId, role } = user + + jwt.sign({ sub: userId, role }, JWT_SECRET, { expiresIn: '1h' }) + .then(token => res.json(token)) + .catch(error => { + next(new SystemError(error.message)) + }) + }) + .catch(error => next(error)) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/checkCompleteActivityHandler.js b/staff/agustin-birman/project/api/handlers/checkCompleteActivityHandler.js new file mode 100644 index 000000000..ab9e578ac --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/checkCompleteActivityHandler.js @@ -0,0 +1,31 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { activityId } = req.params + + try { + logic.checkCompleteActivity(userId, activityId) + .then(result => res.json(result)) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/createActivityHandler.js b/staff/agustin-birman/project/api/handlers/createActivityHandler.js new file mode 100644 index 000000000..2db714c03 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/createActivityHandler.js @@ -0,0 +1,30 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { title, description } = req.body + try { + logic.createActivity(userId, title, description) + .then(activityId => res.status(201).send({ activityId })) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/createCompleteSentenceExerciseHandler.js b/staff/agustin-birman/project/api/handlers/createCompleteSentenceExerciseHandler.js new file mode 100644 index 000000000..c72671d9b --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/createCompleteSentenceExerciseHandler.js @@ -0,0 +1,31 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + + const { activityId, sentence } = req.body + try { + logic.createCompleteSentenceExercise(userId, activityId, sentence) + .then(() => res.status(201).send()) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/createOrderSentenceHandler.js b/staff/agustin-birman/project/api/handlers/createOrderSentenceHandler.js new file mode 100644 index 000000000..8deaf40e0 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/createOrderSentenceHandler.js @@ -0,0 +1,31 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + + const { activityId, sentence, translate } = req.body + try { + logic.createOrderSentence(userId, activityId, sentence, translate) + .then(() => res.status(201).send()) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/createVocabularyHandler.js b/staff/agustin-birman/project/api/handlers/createVocabularyHandler.js new file mode 100644 index 000000000..4f1e9a4c6 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/createVocabularyHandler.js @@ -0,0 +1,29 @@ +import { CredentialsError } from "com/errors.js" +import logic from "../logic/index.js" +import jwt from "../util/jsonwebtoken-promised.js" +import 'dotenv/config' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { activityId, word, answers } = req.body + try { + logic.createVocabulary(userId, activityId, word, answers) + .then(() => res.status(201).send()) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/deleteActivityHandler.js b/staff/agustin-birman/project/api/handlers/deleteActivityHandler.js new file mode 100644 index 000000000..949630b2c --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/deleteActivityHandler.js @@ -0,0 +1,30 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { activityId } = req.params + try { + logic.deleteActivity(userId, activityId) + .then(() => res.status(204).json()) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/deleteAnswersHandler.js b/staff/agustin-birman/project/api/handlers/deleteAnswersHandler.js new file mode 100644 index 000000000..5dd5273db --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/deleteAnswersHandler.js @@ -0,0 +1,30 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { activityId } = req.params + try { + logic.deleteAnswers(userId, activityId) + .then(() => res.status(204).json()) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/deleteExerciseHandler.js b/staff/agustin-birman/project/api/handlers/deleteExerciseHandler.js new file mode 100644 index 000000000..00bc6e7bd --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/deleteExerciseHandler.js @@ -0,0 +1,30 @@ +import { CredentialsError } from 'com/errors.js' + +import logic from '../logic/index.js' + +import jwt from '../util/jsonwebtoken-promised.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { exerciseId } = req.params + try { + logic.deleteExercise(userId, exerciseId) + .then(() => res.status(204).json()) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/editActivityHandler.js b/staff/agustin-birman/project/api/handlers/editActivityHandler.js new file mode 100644 index 000000000..a03c76c5c --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/editActivityHandler.js @@ -0,0 +1,30 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { activityId, title, description } = req.body + try { + logic.editActivity(userId, activityId, title, description) + .then(() => res.status(200).send()) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/editExerciseHandler.js b/staff/agustin-birman/project/api/handlers/editExerciseHandler.js new file mode 100644 index 000000000..034ec6e16 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/editExerciseHandler.js @@ -0,0 +1,31 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { exerciseId, ...updateData } = req.body + + try { + logic.editExercise(userId, exerciseId, updateData) + .then(() => res.status(200).send()) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} diff --git a/staff/agustin-birman/project/api/handlers/errorHandler.js b/staff/agustin-birman/project/api/handlers/errorHandler.js new file mode 100644 index 000000000..eee6d935a --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/errorHandler.js @@ -0,0 +1,18 @@ +import { ContentError, CredentialsError, DuplicityError, MatchError, NotFoundError } from 'com/errors.js' + +export default (error, req, res, next) => { + let status = 500 + + if (error instanceof DuplicityError) + status = 409 + else if (error instanceof ContentError) + status = 400 + else if (error instanceof MatchError) + status = 412 + else if (error instanceof CredentialsError) + status = 401 + else if (error instanceof NotFoundError) + status = 404 + + res.status(status).json({ error: error.constructor.name, message: error.message }) +} diff --git a/staff/agustin-birman/project/api/handlers/getActivitiesHandler.js b/staff/agustin-birman/project/api/handlers/getActivitiesHandler.js new file mode 100644 index 000000000..7ac6eb501 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/getActivitiesHandler.js @@ -0,0 +1,29 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + try { + logic.getActivities(userId) + .then(activities => { res.json(activities) }) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/getActivityHandler.js b/staff/agustin-birman/project/api/handlers/getActivityHandler.js new file mode 100644 index 000000000..2ed2b6d72 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/getActivityHandler.js @@ -0,0 +1,33 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + + const { sub: userId } = payload + + const { activityId } = req.params + + try { + logic.getActivity(userId, activityId) + .then(activity => res.json(activity)) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} diff --git a/staff/agustin-birman/project/api/handlers/getAnswersHandler.js b/staff/agustin-birman/project/api/handlers/getAnswersHandler.js new file mode 100644 index 000000000..44f382fb1 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/getAnswersHandler.js @@ -0,0 +1,29 @@ +import jwt from '../util/jsonwebtoken-promised.js' +import { CredentialsError } from 'com/errors.js' +import logic from '../logic/index.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { exerciseId } = req.params + + try { + logic.getAnswers(userId, exerciseId) + .then(answers => res.json(answers)) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/getExerciseTypeHandler.js b/staff/agustin-birman/project/api/handlers/getExerciseTypeHandler.js new file mode 100644 index 000000000..38b6b2a94 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/getExerciseTypeHandler.js @@ -0,0 +1,30 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { activityId } = req.params + + try { + logic.getExerciseType(userId, activityId) + .then(exercises => res.json(exercises)) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/getExercisesCountHandler.js b/staff/agustin-birman/project/api/handlers/getExercisesCountHandler.js new file mode 100644 index 000000000..f265cdf57 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/getExercisesCountHandler.js @@ -0,0 +1,30 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { activityId } = req.params + + try { + logic.getExercisesCount(userId, activityId) + .then(count => res.json(count)) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/getExercisesHandler.js b/staff/agustin-birman/project/api/handlers/getExercisesHandler.js new file mode 100644 index 000000000..3b1ecde4a --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/getExercisesHandler.js @@ -0,0 +1,30 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { activityId } = req.params + + try { + logic.getExercises(userId, activityId) + .then(exercises => res.json(exercises)) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/getStudentsHandler.js b/staff/agustin-birman/project/api/handlers/getStudentsHandler.js new file mode 100644 index 000000000..d70262940 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/getStudentsHandler.js @@ -0,0 +1,28 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + try { + logic.getStudents(userId) + .then(students => res.json(students)) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/getTeachersActivitiesHandler.js b/staff/agustin-birman/project/api/handlers/getTeachersActivitiesHandler.js new file mode 100644 index 000000000..55b54e72d --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/getTeachersActivitiesHandler.js @@ -0,0 +1,29 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + try { + logic.getTeachersActivities(userId) + .then(activities => { res.json(activities) }) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/getTeachersHandler.js b/staff/agustin-birman/project/api/handlers/getTeachersHandler.js new file mode 100644 index 000000000..b33318760 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/getTeachersHandler.js @@ -0,0 +1,28 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + try { + logic.getTeachers(userId) + .then(teachers => res.json(teachers)) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/getUserInfoHandler.js b/staff/agustin-birman/project/api/handlers/getUserInfoHandler.js new file mode 100644 index 000000000..2ad3d45aa --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/getUserInfoHandler.js @@ -0,0 +1,31 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { userId: userInfoId } = req.params + + try { + logic.getUserInfo(userId, userInfoId) + .then(userInfo => res.status(200).json(userInfo)) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/getUserNameHandler.js b/staff/agustin-birman/project/api/handlers/getUserNameHandler.js new file mode 100644 index 000000000..c1458dcae --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/getUserNameHandler.js @@ -0,0 +1,33 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { targetUserId } = req.params + + try { + logic.getUserName(userId, targetUserId) + .then(name => res.json(name)) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => { + next(new CredentialsError(error.message)) + }) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/getUserStatsHandler.js b/staff/agustin-birman/project/api/handlers/getUserStatsHandler.js new file mode 100644 index 000000000..1f2801b17 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/getUserStatsHandler.js @@ -0,0 +1,33 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { targetUserId } = req.params + + try { + logic.getUserStats(userId, targetUserId) + .then(stats => res.json(stats)) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => { + next(new CredentialsError(error.message)) + }) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/index.js b/staff/agustin-birman/project/api/handlers/index.js new file mode 100644 index 000000000..1acbb668b --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/index.js @@ -0,0 +1,65 @@ +import registerUserHandler from './registerUserHandler.js' +import authenticateUserHandler from './authenticateUserHandler.js' +import getUserNameHandler from './getUserNameHandler.js' +import getUserInfoHandler from './getUserInfoHandler.js' +import addStudentHandler from './addStudentHandler.js' +import removeStudentHandler from './removeStudentHandler.js' +import getStudentsHandler from './getStudentsHandler.js' +import getUserStatsHandler from './getUserStatsHandler.js' + +import errorHandler from './errorHandler.js' +import createCompleteSentenceExerciseHandler from './createCompleteSentenceExerciseHandler.js' + +import createActivityHandler from './createActivityHandler.js' +import getActivitiesHandler from './getActivitiesHandler.js' +import getActivityHandler from './getActivityHandler.js' +import deleteActivityHandler from './deleteActivityHandler.js' +import editActivityHandler from './editActivityHandler.js' +import checkCompleteActivityHandler from './checkCompleteActivityHandler.js' + +import getExercisesHandler from './getExercisesHandler.js' +import deleteExerciseHandler from './deleteExerciseHandler.js' +import editExerciseHandler from './editExerciseHandler.js' +import getExercisesCountHandler from './getExercisesCountHandler.js' +import getExerciseTypeHandler from './getExerciseTypeHandler.js' +import createVocabularyHandler from './createVocabularyHandler.js' + +import submitAnswerHandler from './submitAnswerHandler.js' +import getAnswersHandler from './getAnswersHandler.js' +import deleteAnswersHandler from './deleteAnswersHandler.js' +import getTeachersActivitiesHandler from './getTeachersActivitiesHandler.js' +import getTeachersHandler from './getTeachersHandler.js' + +export { + registerUserHandler, + authenticateUserHandler, + getUserNameHandler, + getUserInfoHandler, + addStudentHandler, + removeStudentHandler, + getStudentsHandler, + getTeachersHandler, + getUserStatsHandler, + + errorHandler, + + createActivityHandler, + getActivitiesHandler, + getActivityHandler, + deleteActivityHandler, + editActivityHandler, + getTeachersActivitiesHandler, + checkCompleteActivityHandler, + + createCompleteSentenceExerciseHandler, + getExercisesHandler, + deleteExerciseHandler, + editExerciseHandler, + getExercisesCountHandler, + getExerciseTypeHandler, + createVocabularyHandler, + + submitAnswerHandler, + getAnswersHandler, + deleteAnswersHandler +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/registerUserHandler.js b/staff/agustin-birman/project/api/handlers/registerUserHandler.js new file mode 100644 index 000000000..584f4a27d --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/registerUserHandler.js @@ -0,0 +1,15 @@ +import logic from '../logic/index.js' + +export default (req, res, next) => { + const { name, surname, email, username, password, passwordRepeat, userType } = req.body + + try { + logic.registerUser(name, surname, email, username, password, passwordRepeat, userType) + .then(() => res.status(201).send()) + .catch(error => { + next(error) + }) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/removeStudentHandler.js b/staff/agustin-birman/project/api/handlers/removeStudentHandler.js new file mode 100644 index 000000000..c08f369c5 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/removeStudentHandler.js @@ -0,0 +1,31 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { studentId } = req.params + + try { + logic.removeStudent(userId, studentId) + .then(() => res.status(204).send()) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/removeTeacherHandler.js b/staff/agustin-birman/project/api/handlers/removeTeacherHandler.js new file mode 100644 index 000000000..21dec081f --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/removeTeacherHandler.js @@ -0,0 +1,31 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { teacherId } = req.params + + try { + logic.removeTeacher(userId, teacherId) + .then(() => res.status(204).send()) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/handlers/submitAnswerHandler.js b/staff/agustin-birman/project/api/handlers/submitAnswerHandler.js new file mode 100644 index 000000000..735b4b0d6 --- /dev/null +++ b/staff/agustin-birman/project/api/handlers/submitAnswerHandler.js @@ -0,0 +1,31 @@ +import jwt from '../util/jsonwebtoken-promised.js' + +import logic from '../logic/index.js' + +import { CredentialsError } from 'com/errors.js' + +const { JWT_SECRET } = process.env + +export default (req, res, next) => { + try { + const token = req.headers.authorization.slice(7) + + jwt.verify(token, JWT_SECRET) + .then(payload => { + const { sub: userId } = payload + + const { activityId, exerciseId, answer } = req.body + + try { + logic.submitAnswer(userId, activityId, exerciseId, answer) + .then(() => res.status(201).json()) + .catch(error => next(error)) + } catch (error) { + next(error) + } + }) + .catch(error => next(new CredentialsError(error.message))) + } catch (error) { + next(error) + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/index.js b/staff/agustin-birman/project/api/index.js new file mode 100644 index 000000000..44116cfae --- /dev/null +++ b/staff/agustin-birman/project/api/index.js @@ -0,0 +1,116 @@ +import 'dotenv/config' +import express from 'express' +import cors from 'cors' +import mongoose from 'mongoose' + +import { + registerUserHandler, + authenticateUserHandler, + getUserNameHandler, + errorHandler, + createActivityHandler, + createCompleteSentenceExerciseHandler, + getActivitiesHandler, + getActivityHandler, + deleteActivityHandler, + getExercisesHandler, + deleteExerciseHandler, + editActivityHandler, + editExerciseHandler, + + submitAnswerHandler, + getAnswersHandler, + deleteAnswersHandler, + getUserInfoHandler, + addStudentHandler, + removeStudentHandler, + getStudentsHandler, + getTeachersActivitiesHandler, + getTeachersHandler, + checkCompleteActivityHandler, + getExercisesCountHandler, + getExerciseTypeHandler, + getUserStatsHandler, + createVocabularyHandler +} from './handlers/index.js' +import removeTeacherHandler from './handlers/removeTeacherHandler.js' +import createOrderSentenceHandler from './handlers/createOrderSentenceHandler.js' + +const { MONGODB_URL, PORT } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + + const api = express() + + api.use(express.static('public')) + + api.use(cors()) + + // api.use(express.json()) + + const jsonBodyParser = express.json({ strict: true, type: 'application/json' }) + + api.get('/', (req, res) => res.send('Hello, World!')) + + api.post('/users', jsonBodyParser, registerUserHandler) + + api.post('/users/auth', jsonBodyParser, authenticateUserHandler) + + api.get('/users/:targetUserId', getUserNameHandler) + + api.get('/users/:userId/info', getUserInfoHandler) + + api.get('/users/:userId/students', getStudentsHandler) + + api.patch('/users', jsonBodyParser, addStudentHandler) + + api.delete('/users/teacher/:studentId/', removeStudentHandler) + + api.delete('/users/student/:teacherId/', removeTeacherHandler) + + api.get('/users/:userId/teachers', getTeachersHandler) + + api.get('/users/student/:targetUserId/stats', getUserStatsHandler) + + api.post('/activity', jsonBodyParser, createActivityHandler) + + api.get('/activity', getActivitiesHandler) + + api.get('/activity/:activityId', getActivityHandler) + + api.get('/activity/:activityId/result', checkCompleteActivityHandler) + + api.get('/activity/:activityId/type', getExerciseTypeHandler) + + api.get('/activity/student/:userId', getTeachersActivitiesHandler) + + api.delete('/activity/:activityId', deleteActivityHandler) + + api.patch('/activity/:activityId', jsonBodyParser, editActivityHandler) + + api.post('/exercise/complete-sentence', jsonBodyParser, createCompleteSentenceExerciseHandler) + + api.post('/exercise/order-sentence', jsonBodyParser, createOrderSentenceHandler) + + api.post('/exercise/vocabulary', jsonBodyParser, createVocabularyHandler) + + api.get('/exercise/:activityId/count', getExercisesCountHandler) + + api.get('/exercise/:activityId', getExercisesHandler) + + api.delete('/exercise/:exerciseId', deleteExerciseHandler) + + api.patch('/exercise/:exerciseId', jsonBodyParser, editExerciseHandler) + + api.post('/answer', jsonBodyParser, submitAnswerHandler) + + api.get('/answer/:exerciseId', getAnswersHandler) + + api.delete('/answer/:activityId', deleteAnswersHandler) + + api.use(errorHandler) + + api.listen(PORT, () => console.log(`API is running on PORT ${PORT}`)) + }) + .catch(error => console.error(error)) diff --git a/staff/agustin-birman/project/api/logic/activity/checkCompleteActivity.js b/staff/agustin-birman/project/api/logic/activity/checkCompleteActivity.js new file mode 100644 index 000000000..3464f6883 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/checkCompleteActivity.js @@ -0,0 +1,33 @@ +import validate from 'com/validate.js' +import { Activity, User, Answer, Exercise } from '../../data/index.js' +import { NotFoundError, SystemError } from 'com/errors.js' + +const checkCompleteActivity = (userId, activityId) => { + validate.id(userId, 'userId') + validate.id(activityId, 'activityId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) throw new NotFoundError('user not found') + + return Activity.findById(activityId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + if (!activity) throw new NotFoundError('activity not found') + + return Exercise.countDocuments({ activity: activityId }) + .catch(error => { throw new SystemError(error.message) }) + .then(exerciseCount => { + + return Answer.countDocuments({ student: userId, activity: activityId }) + .catch(error => { throw new SystemError(error.message) }) + .then(answerCount => { + return exerciseCount === answerCount + }) + }) + }) + }) +} + +export default checkCompleteActivity \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/checkCompleteActivity.spec.js b/staff/agustin-birman/project/api/logic/activity/checkCompleteActivity.spec.js new file mode 100644 index 000000000..71187dec6 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/checkCompleteActivity.spec.js @@ -0,0 +1,87 @@ +import 'dotenv/config' +import bcrypt from 'bcryptjs' +import mongoose, { Types } from 'mongoose' +import checkCompleteActivity from './checkCompleteActivity.js' +import { expect } from 'chai' +import { User, Activity, Exercise, Answer } from '../../data/index.js' +import { ContentError, NotFoundError } from 'com/errors.js' +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + +debugger + +describe('checkCompleteActivity', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany(), Answer.deleteMany()])) + + it('suceeds on deleting answers', () => { + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => Answer.create({ student: user.id, exercise: exercise.id, activity: activity.id, answer: 'hat' }) + .then(() => checkCompleteActivity(user.id, activity.id))))) + .then(result => { + expect(result).to.be.true + }) + }) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => checkCompleteActivity(new ObjectId().toString(), activity.id))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => checkCompleteActivity(user.id, new ObjectId().toString())) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('activity not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + + try { + checkCompleteActivity(12345, new ObjectId().toString()) + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid activityId', () => { + let errorThrown + + try { + checkCompleteActivity(new ObjectId().toString(), 12345) + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('activityId is not valid') + } + }) + + after(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany(), Answer.deleteMany()]).then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/checkCompleteActivity.test.js b/staff/agustin-birman/project/api/logic/activity/checkCompleteActivity.test.js new file mode 100644 index 000000000..bc8c22617 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/checkCompleteActivity.test.js @@ -0,0 +1,17 @@ +import mongoose from 'mongoose' +import 'dotenv/config' +import checkCompleteActivity from './checkCompleteActivity.js' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + checkCompleteActivity('66a94dcb34505782bcd8cfd0', '66c1fbba04735a9cfdd94859') + .then((result) => console.log('actvity completed', result)) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/createActivity.js b/staff/agustin-birman/project/api/logic/activity/createActivity.js new file mode 100644 index 000000000..b805c3c0e --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/createActivity.js @@ -0,0 +1,34 @@ +import { NotFoundError, SystemError } from 'com/errors.js' +import { User, Activity } from '../../data/index.js' +import validate from 'com/validate.js' + +const createActivity = (userId, title, description) => { + validate.id(userId, 'userId') + validate.text(title, 'title', 50) + validate.text(description, 'description', 200) + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + const newActivity = { + teacher: userId, + title, + description + } + + return Activity.create(newActivity) + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + activity.id = activity._id.toString() + + delete activity._id + + return activity.id + }) + }) +} + +export default createActivity \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/createActivity.spec.js b/staff/agustin-birman/project/api/logic/activity/createActivity.spec.js new file mode 100644 index 000000000..2ee0b8fcf --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/createActivity.spec.js @@ -0,0 +1,89 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' +import bcrypt from 'bcryptjs' + +import { expect } from 'chai' + +import { User, Activity } from '../../data/index.js' +import { ContentError } from 'com/errors.js' + +import createActivity from './createActivity.js' + +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + +describe('createActivity', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany()])) + + it('succeds on creating activity', () => + bcrypt.hash('123123123', 8) + .then(hash => User.create({ name: 'Mac', surname: 'Book', email: 'mac@book.com', username: 'macbook', password: hash, userType: 'teacher' })) + .then(user => + createActivity(user.id, 'Hola', 'Pepe') + .then(() => Activity.findOne()) + .then(activity => { + expect(activity.title).to.equal('Hola') + expect(activity.description).to.equal('Pepe') + }) + ) + ) + + it('fails on non-existing user', () => { + let errorThrown + + createActivity(new ObjectId().toString(), 'title', 'description') + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + + try { + createActivity(12345, 'title', 'description') + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid title', () => { + let errorThrown + + try { + createActivity(new ObjectId().toString(), 1234, 'description') + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('title is not valid') + } + }) + + it('fails on invalid description', () => { + let errorThrown + + try { + createActivity(new ObjectId().toString(), 'title', 123) + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('description is not valid') + } + }) + + + after(() => User.deleteMany().then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/createActivity.test.js b/staff/agustin-birman/project/api/logic/activity/createActivity.test.js new file mode 100644 index 000000000..b19bff93f --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/createActivity.test.js @@ -0,0 +1,17 @@ +import createActivity from './createActivity.js' +import mongoose from 'mongoose' +import 'dotenv/config' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + createActivity('66abce851a0dc4acbe205e41', 'hello world', 'console.log') + .then(() => console.log('actvity created')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/deleteActivity.js b/staff/agustin-birman/project/api/logic/activity/deleteActivity.js new file mode 100644 index 000000000..b22aa0088 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/deleteActivity.js @@ -0,0 +1,43 @@ +import validate from 'com/validate.js'; +import { Activity, Answer, Exercise, User } from '../../data/index.js'; +import { MatchError, NotFoundError, SystemError } from 'com/errors.js'; +import { Types } from 'mongoose'; + +const { ObjectId } = Types + +const deleteActivity = (userId, activityId) => { + validate.id(userId, 'userId') + validate.id(activityId, 'activityId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return Activity.findById(activityId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + if (!activity) + throw new NotFoundError('activity not found') + + if (activity.teacher.toString() !== userId) + throw new MatchError('you are not the owner of the activity') + + return Activity.deleteOne({ _id: new ObjectId(activityId) }) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { + + return Exercise.deleteMany({ activity: new ObjectId(activityId) }) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { + return Answer.deleteMany({ activity: new ObjectId(activityId) }) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { }) + }) + }) + }) + }) +} + +export default deleteActivity \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/deleteActivity.spec.js b/staff/agustin-birman/project/api/logic/activity/deleteActivity.spec.js new file mode 100644 index 000000000..77c0f0ea0 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/deleteActivity.spec.js @@ -0,0 +1,118 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' +import bcrypt from 'bcryptjs' + +import { expect } from 'chai' + +import { User, Activity } from '../../data/index.js' + +import deleteActivity from './deleteActivity.js' +import { ContentError, MatchError, NotFoundError } from 'com/errors.js' + +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + +describe('deleteActivity', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany()])) + + it('succeeds on deleting activity', () => { + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => ({ user, activity }))) + .then(({ user, activity }) => deleteActivity(user.id, activity.id) + .then(() => Activity.findById(activity.id))) + .then(activity => { + expect(activity).to.be.null + }) + }) + + it('fails on non-matching user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => Promise.all([User.create({ + name: 'Mocha', + surname: 'Chai', + email: 'Mocha@Chai.com', + username: 'MochaChai', + password: hash, + userType: 'teacher' + }), User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + })])) + .then(([user1, user2]) => Activity.create({ teacher: user1.id, title: 'title', description: 'description' }) + .then(activity => ({ user1, user2, activity }))) + .then(({ user2, activity }) => deleteActivity(user2.id, activity.id) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(MatchError) + expect(errorThrown.message).to.equal('you are not the owner of the activity') + })) + }) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => deleteActivity(new ObjectId().toString(), activity.id)) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + })) + }) + + it('fails on invalid user', () => { + let errorThrown + try { + deleteActivity(123, new ObjectId().toString()) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on non-existing activity', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(() => deleteActivity(user.id, new ObjectId().toString())) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('activity not found') + })) + }) + + it('fails on invalid activity', () => { + let errorThrown + try { + deleteActivity(new ObjectId().toString(), 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('activityId is not valid') + } + }) + + + + after(() => Promise.all([User.deleteMany(), Activity.deleteMany()]).then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/deleteActivity.test.js b/staff/agustin-birman/project/api/logic/activity/deleteActivity.test.js new file mode 100644 index 000000000..7e089019a --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/deleteActivity.test.js @@ -0,0 +1,17 @@ +import deleteActivity from './deleteActivity.js' +import mongoose from 'mongoose' +import 'dotenv/config' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + deleteActivity('66a94dcb34505782bcd8cfd0', '66afa3a0a5ccc42af55a6f25') + .then(() => console.log('activity deleted')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/editActivity.js b/staff/agustin-birman/project/api/logic/activity/editActivity.js new file mode 100644 index 000000000..c44721e3a --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/editActivity.js @@ -0,0 +1,37 @@ +import validate from 'com/validate.js' +import { Activity, User } from '../../data/index.js' +import { NotFoundError, SystemError } from 'com/errors.js' +import { Types } from 'mongoose' + +const { ObjectId } = Types + +const editActivity = (userId, activityId, title, description) => { + validate.id(userId, 'userId') + validate.id(activityId, 'activityId') + if (title !== '') validate.text(title, 'title', 50) + if (description !== '') validate.text(description, 'description', 200) + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return Activity.findById(activityId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + if (!activity) + throw new NotFoundError('activity not found') + + const updates = {} + if (title !== '') updates.title = title + if (description !== '') updates.description = description + + return Activity.updateOne({ _id: new ObjectId(activityId) }, { $set: updates }) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { }) + }) + }) +} + +export default editActivity \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/editActivity.spec.js b/staff/agustin-birman/project/api/logic/activity/editActivity.spec.js new file mode 100644 index 000000000..6a7ad2656 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/editActivity.spec.js @@ -0,0 +1,112 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' +import editActivity from './editActivity.js' +import bcrypt from 'bcryptjs' +import { expect } from 'chai' + +import { Activity, User } from '../../data/index.js' +import { ContentError, NotFoundError } from 'com/errors.js' +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + +describe('editActivity', () => { + + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany()])) + + it('succeeds on editing activity', () => { + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => editActivity(user.id, activity.id, 'title2', 'description2') + .then(() => Activity.findById(activity.id)))) + .then(activityEditted => { + expect(activityEditted.title).to.equal('title2') + expect(activityEditted.description).to.equal('description2') + }) + }) + + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' })) + .then(activity => editActivity(new ObjectId().toString(), activity.id, 'title2', 'description2')) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + + it('fails on non-existing activity', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(() => editActivity(user.id, new ObjectId().toString(), 'title2', 'description2'))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('activity not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + try { + editActivity(123, new ObjectId().toString(), 'title2', 'description2') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid activityId', () => { + let errorThrown + try { + editActivity(new ObjectId().toString(), 123, 'title2', 'description2') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('activityId is not valid') + } + }) + + it('fails on invalid title', () => { + let errorThrown + try { + editActivity(new ObjectId().toString(), new ObjectId().toString(), 123, 'description2') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('title is not valid') + } + }) + + it('fails on invalid description', () => { + let errorThrown + try { + editActivity(new ObjectId().toString(), new ObjectId().toString(), 'title', 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('description is not valid') + } + }) + + after(() => Promise.all([User.deleteMany(), Activity.deleteMany()]).then(() => mongoose.disconnect())) +}) + diff --git a/staff/agustin-birman/project/api/logic/activity/editActivity.test.js b/staff/agustin-birman/project/api/logic/activity/editActivity.test.js new file mode 100644 index 000000000..6b1e6d032 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/editActivity.test.js @@ -0,0 +1,17 @@ +import 'dotenv/config' +import mongoose from 'mongoose' +import editActivity from './editActivity.js' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + editActivity('66a94dcb34505782bcd8cfd0', '66afc3b7f25abf38240bc9ac', 'helloo world', 'console.log') + .then(() => console.log('actvity edited')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/getActivities.js b/staff/agustin-birman/project/api/logic/activity/getActivities.js new file mode 100644 index 000000000..ba57594e5 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/getActivities.js @@ -0,0 +1,29 @@ +import { NotFoundError, SystemError } from 'com/errors.js' +import validate from 'com/validate.js' +import { Activity, User } from '../../data/index.js' + +const getActivities = (userId) => { + validate.id(userId, 'userId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) { + throw new NotFoundError('user not found') + } + + return Activity.find({ teacher: userId }) + .catch(error => { throw new SystemError(error.message) }) + .then(activities => { + const transformedActivities = activities.map(activity => { + const activityObj = activity.toObject() + activityObj.id = activityObj._id + delete activityObj._id + return activityObj + }) + return transformedActivities + }) + }) +} + +export default getActivities \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/getActivities.spec.js b/staff/agustin-birman/project/api/logic/activity/getActivities.spec.js new file mode 100644 index 000000000..9d75f916a --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/getActivities.spec.js @@ -0,0 +1,71 @@ +import 'dotenv/config' +import bcrypt from 'bcryptjs' +import mongoose, { Types } from 'mongoose' +import { expect } from 'chai' + +import { User, Activity } from '../../data/index.js' +import getActivities from './getActivities.js' +import { ContentError, NotFoundError } from 'com/errors.js' + +const { ObjectId } = Types + +const { MONGODB_URL_TEST } = process.env + +describe('getActivities', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany()])) + + it('succeeds on getting activities', () => { + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => + Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(() => Activity.create({ teacher: user.id, title: 'title2', description: 'description2' })) + .then(() => getActivities(user.id))) + .then(activities => { + expect(activities).to.be.an('array') + expect(activities).to.have.lengthOf(2) + + expect(activities[0].teacher).to.equal(activities[0].teacher) + expect(activities[0].title).to.equal('title') + expect(activities[0].description).to.equal('description') + + expect(activities[1].teacher).to.equal(activities[1].teacher) + expect(activities[1].title).to.equal('title2') + expect(activities[1].description).to.equal('description2') + }) + }) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Promise.all([ + Activity.create({ teacher: user.id, title: 'title', description: 'description' }), + Activity.create({ teacher: user.id, title: 'title2', description: 'description2' }) + ])) + .then(() => getActivities(new ObjectId().toString())) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + try { + getActivities(123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + after(() => Promise.all([User.deleteMany(), Activity.deleteMany()]).then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/getActivities.test.js b/staff/agustin-birman/project/api/logic/activity/getActivities.test.js new file mode 100644 index 000000000..d87cc1e15 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/getActivities.test.js @@ -0,0 +1,17 @@ +import mongoose from 'mongoose' +import 'dotenv/config' +import getActivities from './getActivities.js' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + getActivities('66a94dcb34505782bcd8cfd0') + .then(activities => console.log('activities retrieved', activities)) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) diff --git a/staff/agustin-birman/project/api/logic/activity/getActivity.js b/staff/agustin-birman/project/api/logic/activity/getActivity.js new file mode 100644 index 000000000..eaa55035f --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/getActivity.js @@ -0,0 +1,29 @@ +import validate from 'com/validate.js' +import { Activity, User } from '../../data/index.js' +import { NotFoundError, SystemError } from 'com/errors.js' + +const getActivity = (userId, activityId) => { + validate.id(userId, 'userId') + validate.id(activityId, 'activityId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return Activity.findById(activityId) + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + if (!activity) + throw new NotFoundError('activity not found') + + const activityObj = activity.toObject() + activityObj.id = activityObj._id + delete activityObj._id + return activityObj + }) + }) +} + +export default getActivity \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/getActivity.spec.js b/staff/agustin-birman/project/api/logic/activity/getActivity.spec.js new file mode 100644 index 000000000..5e516227d --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/getActivity.spec.js @@ -0,0 +1,87 @@ +import 'dotenv/config' +import bcrypt from 'bcryptjs' +import mongoose, { Types } from 'mongoose' +import { expect } from 'chai' + +import { User, Activity } from '../../data/index.js' +import getActivity from './getActivity.js' +import { ContentError, NotFoundError } from 'com/errors.js' + +const { ObjectId } = Types + +const { MONGODB_URL_TEST } = process.env + +describe('getActivity', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany()])) + + it('succeeds on getting activity', () => { + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => getActivity(user.id, activity.id) + .then(activityInfo => { + expect(activityInfo.teacher.toString()).to.equal(activity.teacher.toString()) + expect(activityInfo.title).to.equal('title') + expect(activityInfo.description).to.equal('description') + }))) + }) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => getActivity(new ObjectId().toString(), activity.id)) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + })) + }) + + it('fails on non-existing activity', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(() => getActivity(user.id, new ObjectId().toString()))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('activity not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + try { + getActivity(123, new ObjectId().toString()) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid activityId', () => { + let errorThrown + try { + getActivity(new ObjectId().toString(), 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('activityId is not valid') + } + }) + + + + after(() => Promise.all([User.deleteMany(), Activity.deleteMany()]).then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/getActivity.test.js b/staff/agustin-birman/project/api/logic/activity/getActivity.test.js new file mode 100644 index 000000000..fe8501f00 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/getActivity.test.js @@ -0,0 +1,17 @@ +import mongoose from 'mongoose' +import 'dotenv/config' +import getActivity from './getActivity.js' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + getActivity('66a94dcb34505782bcd8cfd0', '66c1fbba04735a9cfdd94859') + .then(activities => console.log('activity retrieved', activities)) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) diff --git a/staff/agustin-birman/project/api/logic/activity/getTeachersActivities.js b/staff/agustin-birman/project/api/logic/activity/getTeachersActivities.js new file mode 100644 index 000000000..26982f36a --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/getTeachersActivities.js @@ -0,0 +1,43 @@ +import { MatchError, NotFoundError, SystemError } from 'com/errors.js' +import validate from 'com/validate.js' +import { Activity, User } from '../../data/index.js' + +const getTeachersActivities = (userId) => { + validate.id(userId, 'userId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) { + throw new NotFoundError('user not found') + } + + return User.find({ student: userId }).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(teachers => { + + const teacherIds = teachers.map(teacher => teacher._id) + + return Activity.find({ teacher: { $in: teacherIds } }) + .populate({ path: 'teacher', select: 'username' }) + .catch(error => { throw new SystemError(error.message) }) + .then(activities => { + + const transformedActivities = activities.map(activity => { + const activityObj = activity.toObject() + activityObj.id = activityObj._id + delete activityObj._id + + if (activityObj.teacher) { + activityObj.teacherUsername = activityObj.teacher.username + } + + return activityObj + }) + return transformedActivities + }) //TODO chequear como devolver esto correctamente + }) + }) +} + +export default getTeachersActivities diff --git a/staff/agustin-birman/project/api/logic/activity/getTeachersActivities.spec.js b/staff/agustin-birman/project/api/logic/activity/getTeachersActivities.spec.js new file mode 100644 index 000000000..8d46bd036 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/getTeachersActivities.spec.js @@ -0,0 +1,87 @@ +import 'dotenv/config' +import bcrypt from 'bcryptjs' +import mongoose, { Types } from 'mongoose' +import { expect } from 'chai' + +import { User, Activity } from '../../data/index.js' +import getTeachersActivities from './getTeachersActivities.js' +import { ContentError, NotFoundError } from 'com/errors.js' + +const { ObjectId } = Types + +const { MONGODB_URL_TEST } = process.env + +describe('getTeachersActivities', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany()])) + + it('succeeds on getting activity', () => { + return bcrypt.hash('12345678', 8) + .then(hash => Promise.all([ + User.create({ + name: 'Mocha', + surname: 'Chai', + email: 'Mocha@Chai.com', + username: 'MochaChai', + password: hash, + userType: 'teacher' + }), + User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'student' + }) + ])) + .then(([teacher, student]) => { + teacher.student.push(student._id) + return teacher.save() + .then(() => Activity.create({ teacher: teacher.id, title: 'title', description: 'description' })) + .then(() => getTeachersActivities(student.id)) + .then(activities => { + expect(activities).to.be.an('array').that.is.not.empty + const activityInfo = activities[0] + expect(activityInfo.title).to.equal('title') + expect(activityInfo.description).to.equal('description') + }) + }) + }) + + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => getTeachersActivities(new ObjectId().toString(), activity.id)) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + })) + }) + + + + it('fails on invalid userId', () => { + let errorThrown + try { + getTeachersActivities(123, new ObjectId().toString()) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + + + + + after(() => Promise.all([User.deleteMany(), Activity.deleteMany()]).then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/activity/getTeachersActivities.test.js b/staff/agustin-birman/project/api/logic/activity/getTeachersActivities.test.js new file mode 100644 index 000000000..a8482b922 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/activity/getTeachersActivities.test.js @@ -0,0 +1,17 @@ +import mongoose from 'mongoose' +import 'dotenv/config' +import getTeachersActivities from './getTeachersActivities.js' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + getTeachersActivities('66a94dcb34505782bcd8cfd0') + .then(activities => console.log('activity retrieved', activities)) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) diff --git a/staff/agustin-birman/project/api/logic/answer/deleteAnswers.js b/staff/agustin-birman/project/api/logic/answer/deleteAnswers.js new file mode 100644 index 000000000..5ccc69a4d --- /dev/null +++ b/staff/agustin-birman/project/api/logic/answer/deleteAnswers.js @@ -0,0 +1,44 @@ +import validate from "com/validate.js" +import { Activity, Answer, Exercise, User } from "../../data/index.js" +import { NotFoundError, SystemError } from "com/errors.js" + +const deleteAnswers = (userId, activityId) => { + validate.id(userId, 'userId') + validate.id(activityId, 'activityId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return Activity.findById(activityId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + if (!activity) + throw new NotFoundError('activity not found') + + return Exercise.find({ activity: activityId }) + .catch(error => { throw new SystemError(error.message) }) + .then(exercises => { + if (!exercises || exercises.length === 0) + throw new NotFoundError('exercise not found') + + const exerciseIds = exercises.map(exercise => exercise._id) + + return Answer.find({ exercise: exerciseIds }) + .catch(error => { throw new SystemError(error.message) }) + .then(answers => { + if (!answers || answers.length === 0) + throw new NotFoundError('answer not found') + + return Answer.deleteMany({ exercise: { $in: exerciseIds } }) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { }) + }) + }) + }) + }) +} + +export default deleteAnswers \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/answer/deleteAnswers.spec.js b/staff/agustin-birman/project/api/logic/answer/deleteAnswers.spec.js new file mode 100644 index 000000000..d8fe460d8 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/answer/deleteAnswers.spec.js @@ -0,0 +1,116 @@ +import 'dotenv/config' +import bcrypt from 'bcryptjs' +import mongoose, { Types } from 'mongoose' +import deleteAnswers from './deleteAnswers.js' +import { expect } from 'chai' +import { User, Activity, Exercise, Answer } from '../../data/index.js' +import { ContentError, NotFoundError } from 'com/errors.js' +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + + +describe('deleteAnswers', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany(), Answer.deleteMany()])) + + it('suceeds on deleting answers', () => { + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise1 => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise2 => Answer.create({ student: user.id, exercise: exercise1.id, activity: activity.id, answer: 'hat' }) + .then(answer1 => Answer.create({ student: user.id, exercise: exercise2.id, activity: activity.id, answer: 'haben' }) + .then(answer2 => deleteAnswers(user.id, activity.id) + .then(() => Answer.find({ activity: activity.id }) + .then(answers => expect(answers).to.be.an('array').that.is.empty)))))))) + }) + + it('fails non-existing user', () => { + let errorThrown + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise1 => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise2 => Answer.create({ student: user.id, exercise: exercise1.id, activity: activity.id, answer: 'hat' }) + .then(answer1 => Answer.create({ student: user.id, exercise: exercise2.id, activity: activity.id, answer: 'haben' }) + .then(answer2 => deleteAnswers(new ObjectId().toString(), activity.id)) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + })))))) + }) + + it('fails non-existing activity', () => { + let errorThrown + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise1 => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise2 => Answer.create({ student: user.id, exercise: exercise1.id, activity: activity.id, answer: 'hat' }) + .then(answer1 => Answer.create({ student: user.id, exercise: exercise2.id, activity: activity.id, answer: 'haben' }) + .then(answer2 => deleteAnswers(user.id, new ObjectId().toString())) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('activity not found') + })))))) + }) + + it('fails non-existing exercise', () => { + let errorThrown + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => deleteAnswers(user.id, activity.id)) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('exercise not found') + })) + }) + + it('fails non-existing answer', () => { + let errorThrown + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(() => deleteAnswers(user.id, activity.id)) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('answer not found') + }))) + }) + + it('fails on invalid userId', () => { + let errorThrown + try { + deleteAnswers(123, new ObjectId().toString()) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid activityId', () => { + let errorThrown + try { + deleteAnswers(new ObjectId().toString(), 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('activityId is not valid') + } + }) + after(() => () => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany(), Answer.deleteMany()]).then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/answer/deleteAnswers.test.js b/staff/agustin-birman/project/api/logic/answer/deleteAnswers.test.js new file mode 100644 index 000000000..ad94945bc --- /dev/null +++ b/staff/agustin-birman/project/api/logic/answer/deleteAnswers.test.js @@ -0,0 +1,17 @@ +import deleteAnswers from './deleteAnswers.js' +import mongoose from 'mongoose' +import 'dotenv/config' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + deleteAnswers('66a94dcb34505782bcd8cfd0', '66afc3b7f25abf38240bc9ac') + .then(() => console.log('answers deleted')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/answer/getAnswers.js b/staff/agustin-birman/project/api/logic/answer/getAnswers.js new file mode 100644 index 000000000..27781d9d2 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/answer/getAnswers.js @@ -0,0 +1,37 @@ +import validate from "com/validate.js"; +import { Answer, Exercise, User } from "../../data/index.js"; +import { NotFoundError, SystemError } from "com/errors.js"; +import { Types } from 'mongoose' + +const { ObjectId } = Types + +const getAnswers = (userId, exerciseId) => { + validate.id(userId, 'userId') + validate.id(exerciseId, 'exerciseId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return Exercise.findById(exerciseId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(exercise => { + if (!exercise) + throw new NotFoundError('exercise not found') + + return Answer.find({ exercise: new ObjectId(exerciseId) }) + .catch(error => { throw new SystemError(error.message) }) + .then(answers => { + return answers.map(answer => { + const answerObj = answer.toObject() + answerObj.id = answerObj._id + delete answerObj._id + return answerObj + }) + }) + }) + }) +} +export default getAnswers \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/answer/getAnswers.spec.js b/staff/agustin-birman/project/api/logic/answer/getAnswers.spec.js new file mode 100644 index 000000000..addd69d56 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/answer/getAnswers.spec.js @@ -0,0 +1,87 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' +import bcrypt from 'bcryptjs' +import { expect } from 'chai' +import { Activity, Answer, Exercise, User } from '../../data/index.js' +import { ContentError, NotFoundError } from 'com/errors.js' +import getAnswers from './getAnswers.js' + +const { MONGODB_URL_TEST } = process.env +const { ObjectId } = Types + +describe('getAnswers', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany(), Answer.deleteMany()])) + + it('succeds on getting answers', () => { + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => Answer.create({ student: user.id, exercise: exercise.id, activity: activity.id, answer: 'hat' }) + .then(() => getAnswers(user.id, exercise.id) + .then(answers => { + const answer = answers[0] + expect(answer).to.be.an('object').that.is.not.empty + expect(answer.student.toString()).to.equal(user.id) + expect(answer.exercise.toString()).to.equal(exercise.id) + expect(answer.activity.toString()).to.equal(activity.id) + expect(answer.answer.toString()).to.equal('hat') + }))))) + }) + + it('fails on non-existing user', () => { + let errorThrown + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => Answer.create({ student: user.id, exercise: exercise.id, activity: activity.id, answer: 'hat' }) + .then(() => getAnswers(new ObjectId().toString(), exercise.id)) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + })))) + }) + + it('fails on non-existing exercise', () => { + let errorThrown + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => Answer.create({ student: user.id, exercise: exercise.id, activity: activity.id, answer: 'hat' }) + .then(() => getAnswers(user.id, new ObjectId().toString())) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('exercise not found') + })))) + }) + + it('fails on invalid userId', () => { + let errorThrown + try { + getAnswers(123, new ObjectId().toString()) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid exerciseId', () => { + let errorThrown + try { + getAnswers(new ObjectId().toString(), 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('exerciseId is not valid') + } + }) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/answer/getAnswers.test.js b/staff/agustin-birman/project/api/logic/answer/getAnswers.test.js new file mode 100644 index 000000000..982bf9a3e --- /dev/null +++ b/staff/agustin-birman/project/api/logic/answer/getAnswers.test.js @@ -0,0 +1,16 @@ +import mongoose from 'mongoose' +import 'dotenv/config' +import getAnswers from './getAnswers.js' +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + getAnswers('66a94dcb34505782bcd8cfd0', '66b3aa771e8b00799a51db9a', 'hat') + .then(answers => console.log(answers)) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) diff --git a/staff/agustin-birman/project/api/logic/answer/submitAnswer.js b/staff/agustin-birman/project/api/logic/answer/submitAnswer.js new file mode 100644 index 000000000..610568ee2 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/answer/submitAnswer.js @@ -0,0 +1,57 @@ +import validate from 'com/validate.js'; +import { User, Answer, Exercise, Activity } from '../../data/index.js'; +import { DuplicityError, NotFoundError, SystemError } from 'com/errors.js'; + +const submitAnswer = (userId, activityId, exerciseId, answer) => { + validate.id(userId, 'userId') + validate.id(activityId, 'activityId') + validate.id(exerciseId, 'exerciseId') + validate.text(answer, 'answer') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return Activity.findById(activityId) + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + if (!activity) + throw new NotFoundError('activity not found') + + return Exercise.findById(exerciseId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(exercise => { + if (!exercise) + throw new NotFoundError('exercise not found') + + return Answer.findOne({ student: userId, exercise: exerciseId }).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(existingAnswer => { + if (existingAnswer) + throw new DuplicityError('Answer already exists for this exercise') + + const newAnswer = { + student: userId, + activity: activityId, + exercise: exerciseId, + answer + } + + return Answer.create(newAnswer) + .catch(error => { throw new SystemError(error.message) }) + .then(answer => { + answer.id = answer._id.toString() + + delete answer._id + + return answer + }) + }) + }) + }) + }) +} + +export default submitAnswer \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/answer/submitAnswer.spec.js b/staff/agustin-birman/project/api/logic/answer/submitAnswer.spec.js new file mode 100644 index 000000000..a2a472f1e --- /dev/null +++ b/staff/agustin-birman/project/api/logic/answer/submitAnswer.spec.js @@ -0,0 +1,143 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' + +import bcrypt from 'bcryptjs' +import submitAnswer from './submitAnswer.js' + +import { User, Activity, Exercise, Answer } from '../../data/index.js' + +import { expect } from 'chai' +import { ContentError, DuplicityError, NotFoundError } from 'com/errors.js' + +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + +describe('submitAnswer', () => { + before(() => mongoose.connect(MONGODB_URL_TEST).then(() => User.deleteMany())) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany()])) + + it('succeeds on creating answer', () => { + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => submitAnswer(user.id, activity.id, exercise.id, 'hat')))) + .then(answer => Answer.findById(answer.id)) + .then(answer => { + expect(answer.answer).to.equal('hat') + }) + }) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => submitAnswer(new ObjectId().toString(), activity.id, exercise.id, 'hat')))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on non-existing activity', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => submitAnswer(user.id, new ObjectId().toString(), exercise.id, 'hat')))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('activity not found') + }) + }) + + it('fails on non-existing exercise', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(() => submitAnswer(user.id, activity.id, new ObjectId().toString(), 'hat')))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('exercise not found') + }) + }) + + it('fails on existing answer', () => { + let errorThrown + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => Answer.create({ student: user.id, exercise: exercise.id, activity: activity.id, answer: 'hat' }) + .then(() => submitAnswer(user.id, activity.id, exercise.id, 'hat'))))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(DuplicityError) + expect(errorThrown.message).to.equal('Answer already exists for this exercise') + }) + }) + + it('fails on invalid answer', () => { + let errorThrown + try { + submitAnswer(new ObjectId().toString(), new ObjectId().toString(), new ObjectId().toString(), 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('answer is not valid') + } + }) + + it('fails on invalid userId', () => { + let errorThrown + try { + submitAnswer(123, new ObjectId().toString(), new ObjectId().toString(), '123') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid activityId', () => { + let errorThrown + try { + submitAnswer(new ObjectId().toString(), 123, new ObjectId().toString(), 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('activityId is not valid') + } + }) + + it('fails on invalid exerciseId', () => { + let errorThrown + try { + submitAnswer(new ObjectId().toString(), new ObjectId().toString(), 123, 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('exerciseId is not valid') + } + }) + + after(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany()]).then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/answer/submitAnswer.test.js b/staff/agustin-birman/project/api/logic/answer/submitAnswer.test.js new file mode 100644 index 000000000..435163e05 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/answer/submitAnswer.test.js @@ -0,0 +1,17 @@ +import 'dotenv/config' +import mongoose from 'mongoose' +import submitAnswer from './submitAnswer.js' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + submitAnswer('66a94dcb34505782bcd8cfd0', '66b1cdc7debd28877917c736', 'hat') + .then(() => console.log('answer submitted')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/createCompleteSentenceExercise.js b/staff/agustin-birman/project/api/logic/exercise/createCompleteSentenceExercise.js new file mode 100644 index 000000000..185c42da2 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/createCompleteSentenceExercise.js @@ -0,0 +1,53 @@ +import validate from 'com/validate.js' +import { Activity, Exercise, User } from '../../data/index.js' +import { NotFoundError, SystemError } from 'com/errors.js' +import { CompleteSentenceExercise } from '../../data/Exercise.js'; + +const ANSWER_REGEX = /\(([^)]+)\)/; + +const createCompleteSentenceExercise = (userId, activityId, sentence) => { + validate.id(userId, 'userId') + validate.id(activityId, 'activityId') + validate.textCompleteSentence(sentence, 'sentence', 200) + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + + return Activity.findById(activityId) + .catch(error => { throw new SystemError(error.message) }) + .then(activities => { + if (!activities) + throw new NotFoundError('activity not found') + + const removeAnswer = sentence.match(ANSWER_REGEX) + + if (!removeAnswer[1]) + throw new NotFoundError('Answer not found') + + return Exercise.countDocuments({ activity: activityId }) + .catch(error => { throw new SystemError(error.message) }) + .then(index => { + + const newExercise = { + activity: activityId, + sentence, + answer: removeAnswer[1], + index + } + + return CompleteSentenceExercise.create(newExercise) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { }) + }) + + + }) + }) + +} + +export default createCompleteSentenceExercise \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/createCompleteSentenceExercise.spec.js b/staff/agustin-birman/project/api/logic/exercise/createCompleteSentenceExercise.spec.js new file mode 100644 index 000000000..b1a99a2b2 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/createCompleteSentenceExercise.spec.js @@ -0,0 +1,101 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' + +import bcrypt from 'bcryptjs' + +import createCompleteSentenceExercise from './createCompleteSentenceExercise.js' + +import { User, Activity, Exercise } from '../../data/index.js' + +import { expect } from 'chai' +import { ContentError, NotFoundError } from 'com/errors.js' + +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + +describe('createCompleteSentenceExercise', () => { + before(() => mongoose.connect(MONGODB_URL_TEST).then(() => User.deleteMany())) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany()])) + + it('succeeds on creating exercise', () => { + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => createCompleteSentenceExercise(user.id, activity.id, 'alan (hat) es gegessen'))) + .then(() => Exercise.findOne()) + .then(exercise => { + expect(exercise.sentence).to.equal('alan (hat) es gegessen') + expect(exercise.answer).to.equal('hat') + expect(exercise.index).to.be.a('number').and.to.equal(0) + }) + }) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => createCompleteSentenceExercise(new ObjectId().toString(), activity.id, 'alan (hat) es gegessen'))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on non-existing activity', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(() => createCompleteSentenceExercise(user.id, new ObjectId().toString(), 'alan (hat) es gegessen'))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('activity not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + try { + createCompleteSentenceExercise(123, new ObjectId().toString(), 'alan (hat) es gegessen') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid activityId', () => { + let errorThrown + try { + createCompleteSentenceExercise(new ObjectId().toString(), 123, 'alan (hat) es gegessen') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('activityId is not valid') + } + }) + + it('fails on invalid sentence', () => { + let errorThrown + try { + createCompleteSentenceExercise(new ObjectId().toString(), new ObjectId().toString(), 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('sentence is not valid') + } + }) + + after(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany()]).then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/createCompleteSentenceExercise.test.js b/staff/agustin-birman/project/api/logic/exercise/createCompleteSentenceExercise.test.js new file mode 100644 index 000000000..c24196ce2 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/createCompleteSentenceExercise.test.js @@ -0,0 +1,16 @@ +import mongoose from 'mongoose' +import 'dotenv/config' +import createCompleteSentenceExercise from './createCompleteSentenceExercise.js' +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + createCompleteSentenceExercise('66a94dcb34505782bcd8cfd0', '66afc3b7f25abf38240bc9ac', 'alan (hat) es gegessen') + .then(() => console.log('exercise created')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/createOrderSentence.js b/staff/agustin-birman/project/api/logic/exercise/createOrderSentence.js new file mode 100644 index 000000000..b0cc10b6c --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/createOrderSentence.js @@ -0,0 +1,41 @@ +import validate from "com/validate.js" +import { Activity, Exercise, User } from "../../data/index.js" +import { NotFoundError, SystemError } from "com/errors.js" +import { OrderSentenceExercise } from "../../data/Exercise.js" + +const createOrderSentence = (userId, activityId, sentence, translate) => { + validate.id(userId, 'userId') + validate.id(activityId, 'activityId') + validate.text(sentence, 'sentence') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return Activity.findById(activityId) + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + if (!activity) + throw new NotFoundError('activity not found') + + return Exercise.countDocuments({ activity: activityId }) + .catch(error => { throw new SystemError(error.message) }) + .then(index => { + + const newExercise = { + activity: activityId, + sentence, + translate, + index + } + + return OrderSentenceExercise.create(newExercise) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { }) + }) + }) + }) +} +export default createOrderSentence \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/createOrderSentence.test.js b/staff/agustin-birman/project/api/logic/exercise/createOrderSentence.test.js new file mode 100644 index 000000000..c5e9745b4 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/createOrderSentence.test.js @@ -0,0 +1,16 @@ +import mongoose from 'mongoose' +import 'dotenv/config' +import createOrderSentence from './createOrderSentence.js' +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + createOrderSentence('66a94dcb34505782bcd8cfd0', '66c49703e8fbc188b5db0e7d', 'alan hat es gegessen') + .then(() => console.log('exercise created')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/createVocabulary.js b/staff/agustin-birman/project/api/logic/exercise/createVocabulary.js new file mode 100644 index 000000000..995c4fe7c --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/createVocabulary.js @@ -0,0 +1,44 @@ +import validate from "com/validate.js" +import { Activity, Exercise, User } from "../../data/index.js" +import { NotFoundError, SystemError } from "com/errors.js" +import { VocabularyExercise } from "../../data/Exercise.js" + +const createVocabulary = (userId, activityId, word, answers) => { + validate.id(userId, 'userId') + validate.id(activityId, 'activityId') + validate.text(word, 'word', 60) + const filteredArray = answers.filter(element => element !== undefined && element !== null && element !== ''); + + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return Activity.findById(activityId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + if (!activity) + throw new NotFoundError('activity not found') + + return Exercise.countDocuments({ activity: activityId }) + .catch(error => { throw new SystemError(error.message) }) + .then(index => { + + const newExercise = { + activity: activityId, + word, + answer: filteredArray, + index + } + + return VocabularyExercise.create(newExercise) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { }) + }) + }) + }) +} + +export default createVocabulary \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/deleteExercise.js b/staff/agustin-birman/project/api/logic/exercise/deleteExercise.js new file mode 100644 index 000000000..8be6e811a --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/deleteExercise.js @@ -0,0 +1,57 @@ +import validate from 'com/validate.js' +import { Activity, Answer, Exercise, User } from '../../data/index.js' +import { NotFoundError, SystemError } from 'com/errors.js' +import { Types } from 'mongoose' + +const { ObjectId } = Types +const deleteExercise = (userId, exerciseId) => { + validate.id(userId, 'userId') + validate.id(exerciseId, 'exerciseId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return Exercise.findById(new ObjectId(exerciseId)) + .catch(error => { throw new SystemError(error.message) }) + .then(exercise => { + if (!exercise) + throw new NotFoundError('exercise not found') + + return Activity.findById(exercise.activity) + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + if (!activity) + throw new NotFoundError('activity not found') + + if (activity.teacher.toString() !== userId) + throw new MatchError('you are not the owner of the exercise') + + return Exercise.deleteOne({ _id: new ObjectId(exerciseId) }) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { + + return Exercise.find({ activity: exercise.activity, index: { $gt: exercise.index } }).sort({ index: 1 }) + .catch(error => { throw new SystemError(error.message) }) + .then(exercises => { + const updatePromises = exercises.map((_exercise, index) => { + return Exercise.updateOne({ _id: new ObjectId(_exercise._id) }, { $set: { index: exercise.index + index } }) + }) + return Promise.all(updatePromises) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { + + return Answer.deleteMany({ exercise: exerciseId }) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { }) + }) + }) + }) + }) + }) + }) +} + +export default deleteExercise \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/deleteExercise.spec.js b/staff/agustin-birman/project/api/logic/exercise/deleteExercise.spec.js new file mode 100644 index 000000000..efade8b2e --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/deleteExercise.spec.js @@ -0,0 +1,128 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' +import bcrypt from 'bcryptjs' +import { expect } from 'chai' + +import { User, Activity, Exercise } from '../../data/index.js' +import deleteExercise from './deleteExercise.js' +import { ContentError, MatchError, NotFoundError } from 'com/errors.js' + +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + +describe('deleteExercise', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany()])) + + it('succeeds on deleting exercise', () => { + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => deleteExercise(user.id, exercise.id) + .then(() => Exercise.findById(exercise.id))))) + .then(exercise => { + expect(exercise).to.be.null + }) + }) + + it('fails on non-existing activity', () => { + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Exercise.create({ teacher: user.id, activity: new ObjectId(), sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => deleteExercise(user.id, exercise.id) + .catch(error => { + expect(error).to.be.instanceOf(NotFoundError) + expect(error.message).to.equal('activity not found') + }) + )) + }) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => deleteExercise(new ObjectId().toString(), exercise.id))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + })) + }) + + it('fails on non-existing exercise', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(() => deleteExercise(user.id, new ObjectId().toString()))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('exercise not found') + })) + }) + + it('fails on different user', () => { + let errorThrown + + bcrypt.hash('1234', 8) + .then(hash => Promise.all([User.create({ + name: 'Mocha', + surname: 'Chai', + email: 'Mocha@Chai.com', + username: 'MochaChai', + password: hash, + userType: 'teacher' + }), User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + })])) + .then(([user1, user2]) => Activity.create({ teacher: user1.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user1.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => deleteExercise(user2.id, exercise.id)))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(MatchError) + expect(errorThrown.message).to.equal('you are not the owner of the exercise') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + try { + deleteExercise(123, new ObjectId().toString()) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid exerciseId', () => { + let errorThrown + try { + deleteExercise(new ObjectId().toString(), 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('exerciseId is not valid') + } + }) + + after(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany()]).then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/deleteExercise.test.js b/staff/agustin-birman/project/api/logic/exercise/deleteExercise.test.js new file mode 100644 index 000000000..a999b59fd --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/deleteExercise.test.js @@ -0,0 +1,17 @@ +import mongoose from 'mongoose' +import 'dotenv/config' +import deleteExercise from './deleteExercise.js' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + deleteExercise('66a94dcb34505782bcd8cfd0', '66b259c0ec1e53453e51a4be') + .then(() => console.log('exercise deleted')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/editExercise.js b/staff/agustin-birman/project/api/logic/exercise/editExercise.js new file mode 100644 index 000000000..a6e95e779 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/editExercise.js @@ -0,0 +1,75 @@ +import validate from 'com/validate.js' +import { Exercise, User } from '../../data/index.js' +import { NotFoundError, SystemError } from 'com/errors.js' +import { Types } from 'mongoose' +import { CompleteSentenceExercise, OrderSentenceExercise, VocabularyExercise } from '../../data/Exercise.js' + +const { ObjectId } = Types + +const ANSWER_REGEX = /\(([^)]+)\)/; + +const editExercise = (userId, exerciseId, updateData = {}) => { + validate.id(userId, 'userId') + validate.id(exerciseId, 'exerciseId') + + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return Exercise.findById(exerciseId) + .catch(error => { throw new SystemError(error.message) }) + .then(exercise => { + if (!exercise) + throw new NotFoundError('exercise not found') + + const exerciseType = exercise.type + + const update = {} + + switch (exerciseType) { + case 'completeSentence': + if (updateData.sentence !== undefined) { + validate.textCompleteSentence(updateData.sentence, 'sentence', 50) + const removeAnswer = updateData.sentence.match(ANSWER_REGEX) + + update.sentence = updateData.sentence + update.answer = removeAnswer[1] + + return CompleteSentenceExercise.updateOne({ _id: exerciseId }, { $set: update }) + //TODO catch and then + } + case 'orderSentence': + if (updateData.sentence !== undefined) { + validate.text(updateData.sentence, 'sentence', 50) + update.sentence = updateData.sentence + } + + if (updateData.translate !== undefined) { + validate.text(updateData.translate, 'translate', 50) + update.translate = updateData.translate + } + + return OrderSentenceExercise.updateOne({ _id: exerciseId }, { $set: update }) + + case 'vocabulary': + if (updateData.word !== undefined) { + validate.text(updateData.word, 'word', 50) + update.word = updateData.word + } + + if (updateData.answers !== undefined) { + validate.text(updateData.answers, 'word', 50) + update.answer = updateData.answers + } + + return VocabularyExercise.updateOne({ _id: exerciseId }, { $set: update }) + } + }) + }) +} + +export default editExercise + diff --git a/staff/agustin-birman/project/api/logic/exercise/editExercise.spec.js b/staff/agustin-birman/project/api/logic/exercise/editExercise.spec.js new file mode 100644 index 000000000..4d895f8f5 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/editExercise.spec.js @@ -0,0 +1,113 @@ +import 'dotenv/config' +import bcrypt from 'bcryptjs' +import mongoose, { Types } from 'mongoose' +import { expect } from 'chai' + +import { User, Activity, Exercise } from '../../data/index.js' + +import { NotFoundError, ContentError } from 'com/errors.js' + +import editExercise from './editExercise.js' + +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + +debugger + +describe('editExercise', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany()])) + + it('succeeds on editing exercise', () => { + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => editExercise(user.id, exercise.id, 'ich (habe) es gesagt') + .then(() => Exercise.findById(exercise.id))))) + + .then(exerciseResult => { + expect(exerciseResult.sentence).to.equal('ich (habe) es gesagt') + expect(exerciseResult.answer).to.equal('habe') + expect(exerciseResult.index).to.be.a('number').and.to.equal(0) + }) + }) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => ({ user, activity }))) + .then(({ user, activity }) => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 })) + .then(exercise => editExercise(new ObjectId().toString(), exercise.id, 'ich (habe) es gesagt')) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on non-existing exercise', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => ({ user, activity }))) + .then(({ user, activity }) => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(() => user)) + .then(user => editExercise(user.id, new ObjectId().toString(), 'ich (habe) es gesagt')) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('exercise not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + try { + editExercise(123, new ObjectId().toString(), 'ich (habe) es gesagt') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid exerciseId', () => { + let errorThrown + try { + editExercise(new ObjectId().toString(), 123, 'ich (habe) es gesagt') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('exerciseId is not valid') + } + }) + + it('fails on invalid sentence', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => ({ user, activity }))) + .then(({ user, activity }) => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(exercise => ({ user, exercise }))) + .then(({ user, exercise }) => editExercise(user.id, exercise.id, 12345)) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(ContentError) + expect(errorThrown.message).to.equal('sentence is not valid') + }) + }) + + after(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany()]).then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/editExercise.test.js b/staff/agustin-birman/project/api/logic/exercise/editExercise.test.js new file mode 100644 index 000000000..d3b2b7f90 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/editExercise.test.js @@ -0,0 +1,19 @@ + +import mongoose from 'mongoose' + +import 'dotenv/config' +import editExercise from './editExercise.js' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + editExercise('66a94dcb34505782bcd8cfd0', '66b259e1ec1e53453e51a4ce', 'alex (eats) bananaaaaa') + .then(() => console.log('actvity edited')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/getExerciseType.js b/staff/agustin-birman/project/api/logic/exercise/getExerciseType.js new file mode 100644 index 000000000..96ef3df54 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/getExerciseType.js @@ -0,0 +1,36 @@ +import validate from 'com/validate.js' +import { Activity, Answer, Exercise, User } from '../../data/index.js' +import { NotFoundError, SystemError } from 'com/errors.js' +import { Types } from 'mongoose' + +const { ObjectId } = Types + +const getExerciseType = (userId, activityId) => { + validate.id(userId, 'userId') + validate.id(activityId, 'activityId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return Activity.findById(activityId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + if (!activity) + throw new NotFoundError('activity not found') + + return Exercise.findOne({ activity: activityId }).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(exercise => { + return exercise.type + + }) + }) + }) +} + + +export default getExerciseType + diff --git a/staff/agustin-birman/project/api/logic/exercise/getExerciseType.test.js b/staff/agustin-birman/project/api/logic/exercise/getExerciseType.test.js new file mode 100644 index 000000000..6dc4030c3 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/getExerciseType.test.js @@ -0,0 +1,16 @@ +import mongoose, { connect } from 'mongoose'; +import 'dotenv/config' +import getExerciseType from './getExerciseType.js'; + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + getExerciseType('66a94dcb34505782bcd8cfd0', '66c8e62aa9af79afcc6f9f9c') + .then((exercises => console.log('retrieved ', exercises))) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/getExercises.js b/staff/agustin-birman/project/api/logic/exercise/getExercises.js new file mode 100644 index 000000000..fbb7061f8 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/getExercises.js @@ -0,0 +1,55 @@ +import validate from 'com/validate.js' +import { Activity, Answer, Exercise, User } from '../../data/index.js' +import { NotFoundError, SystemError } from 'com/errors.js' +import { Types } from 'mongoose' + +const { ObjectId } = Types + +const getExercises = (userId, activityId) => { + validate.id(userId, 'userId') + validate.id(activityId, 'activityId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return Activity.findById(activityId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + if (!activity) + throw new NotFoundError('activity not found') + + return Answer.countDocuments({ activity: activityId }).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(count => { + + return Exercise.find({ activity: activityId }) + .catch(error => { throw new SystemError(error.message) }) + .then(exercises => { + const transformedExercises = exercises.map(exercise => { + const exerciseObj = exercise.toObject() + exerciseObj.id = exerciseObj._id + delete exerciseObj._id + return exerciseObj + }) + + return { + exercises: transformedExercises, + count: count + } + }) + }) + }) + }) +} + + +export default getExercises + +// exercises.forEach(exercise => { +// exercise.id = exercise._id.toString() + +// delete exercise._id +// }) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/getExercises.spec.js b/staff/agustin-birman/project/api/logic/exercise/getExercises.spec.js new file mode 100644 index 000000000..08273c266 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/getExercises.spec.js @@ -0,0 +1,200 @@ +import 'dotenv/config' +import bcrypt from 'bcryptjs' +import mongoose, { Types } from 'mongoose' +import { expect } from 'chai' + +import { User, Activity, Exercise, Answer } from '../../data/index.js' +import getExercises from './getExercises.js' +import { ContentError, NotFoundError } from 'com/errors.js' +import { CompleteSentenceExercise } from '../../data/Exercise.js' + +const { ObjectId } = Types +const { MONGODB_URL_TEST } = process.env + +debugger + +describe('getExercises', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany(), Answer.deleteMany()])) + + it('succeeds on getting exercises', () => { + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => CompleteSentenceExercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(() => CompleteSentenceExercise.create({ teacher: user.id, activity: activity.id, sentence: 'pepe (hat) es gesagt', answer: 'hat', index: 1 }) + .then(() => getExercises(user.id, activity.id) + .then(result => { + console.log(result) + const { exercises, count } = result + console.log(exercises, count) + expect(exercises).to.be.an('array') + expect(count).to.be.a('number') + + expect(exercises).to.have.lengthOf(2) + + expect(exercises[0].activity.toString()).to.equal(activity.id) + expect(exercises[0].sentence).to.equal('alan (hat) es gegessen') + expect(exercises[0].answer).to.equal('hat') + expect(exercises[0].index).to.equal(0) + + expect(exercises[1].activity.toString()).to.equal(activity.id) + expect(exercises[1].sentence).to.equal('pepe (hat) es gesagt') + expect(exercises[1].answer).to.equal('hat') + expect(exercises[1].index).to.equal(1) + }))))) + }) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(() => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'pepe (hat) es gesagt', answer: 'hat', index: 1 }) + .then(() => getExercises(new ObjectId().toString(), activity.id))))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on non-existing activity', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(() => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'pepe (hat) es gesagt', answer: 'hat', index: 1 }) + .then(() => getExercises(user.id, new ObjectId().toString()))))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('activity not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + try { + getExercises(123, new ObjectId().toString()) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid activityId', () => { + let errorThrown + try { + getExercises(new ObjectId().toString(), 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('activityId is not valid') + } + }) + + after(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany(), Answer.deleteMany()]).then(() => mongoose.disconnect())) +}) + + +// import 'dotenv/config' +// import bcrypt from 'bcryptjs' +// import mongoose, { Types } from 'mongoose' +// import { expect } from 'chai' + +// import { User, Activity, Exercise } from '../../data/index.js' +// import getExercises from './getExercises.js' +// import { NotFoundError } from 'com/errors.js' + +// const { ObjectId } = Types + +// const { MONGODB_URL_TEST } = process.env + +// describe('getExercises', () => { +// before(() => mongoose.connect(MONGODB_URL_TEST)) + +// beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany()])) + +// it('succeeds on getting exercises', () => { + +// return bcrypt.hash('12345678', 8) +// .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) +// .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) +// .then(activity => ({ user, activity }))) +// .then(({ user, activity }) => +// Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) +// .then(() => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'pepe (hat) es gesagt', answer: 'hat', index: 1 })) +// .then(() => ({ user, activity }))) +// .then(({ user, activity }) => getExercises(user.id, activity.id) +// .then(result => ({ user, activity, result }))) +// .then(({ result, user, activity }) => { +// const { exercises, count } = result + +// expect(exercises).to.be.an('array') +// expect(count).to.be.a('number') + +// expect(exercises[0].teacher).to.equal(user.id) +// expect(exercises[0].activity).to.equal(activity.id) +// expect(exercises[0].sentence).to.equal('alan (hat) es gegessen') +// expect(exercises[0].answer).to.equal('hat') +// expect(exercises[0].index).to.equal(0) + +// expect(exercises[1].teacher).to.equal(user.id) +// expect(exercises[1].activity).to.equal(activity.id) +// expect(exercises[1].sentence).to.equal('pepe (hat) es gesagt') +// expect(exercises[1].answer).to.equal('hat') +// expect(exercises[1].index).to.equal(1) +// }) +// }) + +// it('fails on non-existing user', () => { +// let errorThrown + +// return bcrypt.hash('12345678', 8) +// .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) +// .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) +// .then(activity => ({ user, activity }))) +// .then(({ user, activity }) => +// Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) +// .then(() => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'pepe (hat) es gesagt', answer: 'hat', index: 1 })) +// .then(() => activity)) +// .then(activity => getExercises(new ObjectId().toString(), activity.id)) +// .catch(error => errorThrown = error) +// .finally(() => { +// expect(errorThrown).to.be.an.instanceOf(NotFoundError) +// expect(errorThrown.message).to.equal('user not found') +// }) +// }) + +// it('fails on non-existing activity', () => { +// let errorThrown + +// return bcrypt.hash('12345678', 8) +// .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) +// .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) +// .then(activity => ({ user, activity }))) +// .then(({ user, activity }) => +// Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) +// .then(() => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'pepe (hat) es gesagt', answer: 'hat', index: 1 })) +// .then(() => user)) +// .then(user => getExercises(user.id, new ObjectId().toString())) +// .catch(error => errorThrown = error) +// .finally(() => { +// expect(errorThrown).to.be.an.instanceOf(NotFoundError) +// expect(errorThrown.message).to.equal('activity not found') +// }) +// }) + + +// after(() => Promise.all([User.deleteMany(), Activity.deleteMany()]).then(() => mongoose.disconnect())) +// }) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/getExercises.test.js b/staff/agustin-birman/project/api/logic/exercise/getExercises.test.js new file mode 100644 index 000000000..cb968a34f --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/getExercises.test.js @@ -0,0 +1,16 @@ +import getExercises from './getExercises.js'; +import mongoose, { connect } from 'mongoose'; +import 'dotenv/config' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + getExercises('66a94dcb34505782bcd8cfd0', '66c8c1742d29b9711d7c8a41') + .then((exercises => console.log('retrieved exercises', exercises))) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/exercise/getExercisesCount.js b/staff/agustin-birman/project/api/logic/exercise/getExercisesCount.js new file mode 100644 index 000000000..f559925dc --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/getExercisesCount.js @@ -0,0 +1,26 @@ +import { Activity, Exercise, User } from "../../data/index.js" +import { NotFoundError, SystemError } from 'com/errors.js' +import validate from 'com/validate.js' + +const getExercisesCount = (userId, activityId) => { + validate.id(userId, 'userId') + validate.id(activityId, 'activityId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) throw new NotFoundError('user not found') + + return Activity.findById(activityId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(activity => { + if (!activity) throw new NotFoundError('activity not found') + + return Exercise.countDocuments({ activity: activityId }) + .catch(error => { throw new SystemError(error.message) }) + .then(count => count) + }) + }) +} + +export default getExercisesCount diff --git a/staff/agustin-birman/project/api/logic/exercise/getExercisesCount.spec.js b/staff/agustin-birman/project/api/logic/exercise/getExercisesCount.spec.js new file mode 100644 index 000000000..ffd31775a --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/getExercisesCount.spec.js @@ -0,0 +1,90 @@ +import 'dotenv/config' +import bcrypt from 'bcryptjs' +import mongoose, { Types } from 'mongoose' +import { expect } from 'chai' + +import { User, Activity, Exercise, Answer } from '../../data/index.js' +import getExercisesCount from './getExercisesCount.js' +import { ContentError, NotFoundError } from 'com/errors.js' + +const { ObjectId } = Types +const { MONGODB_URL_TEST } = process.env + +describe('getExercisesCount', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany(), Answer.deleteMany()])) + + it('succeeds on getting exercisesCount', () => { + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'alan (hat) es gegessen', answer: 'hat', index: 0 }) + .then(() => Exercise.create({ teacher: user.id, activity: activity.id, sentence: 'pepe (hat) es gesagt', answer: 'hat', index: 1 }) + .then(() => getExercisesCount(user.id, activity.id) + .then(count => { + + expect(count).to.be.a('number') + + expect(count).to.be.equal(2) + + + }))))) + }) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => getExercisesCount(new ObjectId().toString(), activity.id))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on non-existing activity', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => Activity.create({ teacher: user.id, title: 'title', description: 'description' }) + .then(activity => getExercisesCount(user.id, new ObjectId().toString()))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('activity not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + try { + getExercisesCount(123, new ObjectId().toString()) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid activityId', () => { + let errorThrown + try { + getExercisesCount(new ObjectId().toString(), 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('activityId is not valid') + } + }) + + after(() => Promise.all([User.deleteMany(), Activity.deleteMany(), Exercise.deleteMany(), Answer.deleteMany()]).then(() => mongoose.disconnect())) +}) + diff --git a/staff/agustin-birman/project/api/logic/exercise/getExercisesCount.test.js b/staff/agustin-birman/project/api/logic/exercise/getExercisesCount.test.js new file mode 100644 index 000000000..5e66b7205 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/exercise/getExercisesCount.test.js @@ -0,0 +1,16 @@ +import mongoose, { connect } from 'mongoose'; +import 'dotenv/config' +import getExercisesCount from './getExercisesCount.js'; + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + getExercisesCount('66a94dcb34505782bcd8cfd0', '66c1fbba04735a9cfdd94859') + .then((exercises => console.log('retrieved exercises', exercises))) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/index.js b/staff/agustin-birman/project/api/logic/index.js new file mode 100644 index 000000000..675f5ebbb --- /dev/null +++ b/staff/agustin-birman/project/api/logic/index.js @@ -0,0 +1,68 @@ +import registerUser from './user/registerUser.js' +import authenticateUser from './user/authenticateUser.js' +import getUserName from './user/getUserName.js' +import getUserInfo from './user/getUserInfo.js' +import addStudent from './user/addStudent.js' +import removeStudent from './user/removeStudent.js' +import getTeachers from './user/getTeachers.js' +import removeTeacher from './user/removeTeacher.js' +import getStudents from './user/getStudents.js' + +import createActivity from './activity/createActivity.js' +import getActivities from './activity/getActivities.js' +import getActivity from './activity/getActivity.js' +import deleteActivity from './activity/deleteActivity.js' +import editActivity from './activity/editActivity.js' +import checkCompleteActivity from './activity/checkCompleteActivity.js' +import getTeachersActivities from './activity/getTeachersActivities.js' + +import createCompleteSentenceExercise from './exercise/createCompleteSentenceExercise.js' +import getExercises from './exercise/getExercises.js' +import deleteExercise from './exercise/deleteExercise.js' +import editExercise from './exercise/editExercise.js' +import getExercisesCount from './exercise/getExercisesCount.js' +import createOrderSentence from './exercise/createOrderSentence.js' +import getExerciseType from './exercise/getExerciseType.js' +import createVocabulary from './exercise/createVocabulary.js' + +import submitAnswer from './answer/submitAnswer.js' +import getAnswers from './answer/getAnswers.js' +import deleteAnswers from './answer/deleteAnswers.js' +import getUserStats from './user/getUserStats.js' + + +const logic = { + registerUser, + authenticateUser, + getUserName, + getUserInfo, + addStudent, + removeStudent, + getStudents, + removeTeacher, + getUserStats, + + createActivity, + getActivities, + getActivity, + editActivity, + deleteActivity, + getTeachersActivities, + getTeachers, + checkCompleteActivity, + + createCompleteSentenceExercise, + getExercises, + deleteExercise, + editExercise, + getExercisesCount, + createOrderSentence, + createVocabulary, + getExerciseType, + + submitAnswer, + getAnswers, + deleteAnswers +} + +export default logic \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/addStudent.js b/staff/agustin-birman/project/api/logic/user/addStudent.js new file mode 100644 index 000000000..2da461b7e --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/addStudent.js @@ -0,0 +1,33 @@ +import { DuplicityError, NotFoundError, SystemError } from "com/errors.js" +import validate from "com/validate.js" +import { User } from '../../data/index.js' + +const addStudent = (userId, studentId) => { + validate.id(userId, 'userId') + validate.id(studentId, 'studentId') + + return User.findById(userId) + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return User.findById(studentId) + .catch(error => { throw new SystemError(error.message) }) + .then(student => { + if (!student) + throw new NotFoundError('student not found') + + if (user.student.includes(studentId)) + throw new DuplicityError('student already exists for this user') + + user.student.push(studentId) + + return User.updateOne({ _id: user._id }, { student: user.student }) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { }) + }) + }) +} + +export default addStudent \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/addStudent.spec.js b/staff/agustin-birman/project/api/logic/user/addStudent.spec.js new file mode 100644 index 000000000..b1f632c1c --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/addStudent.spec.js @@ -0,0 +1,98 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' +import bcrypt from 'bcryptjs' +import { expect } from 'chai' +import { User } from '../../data/index.js' + +import addStudent from './addStudent.js' +import { ContentError, DuplicityError, NotFoundError } from 'com/errors.js' + +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + +describe('addStudent', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => User.deleteMany()) + + + it('succeds on adding a student', () => { + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mac', surname: 'Book', email: 'mac@book.com', username: 'macbook', password: hash, userType: 'teacher' }) + .then(user => User.create({ name: 'Wind', surname: 'Book', email: 'wind@book.com', username: 'windbook', password: hash, userType: 'teacher' }) + .then(student => addStudent(user.id, student.id) + .then(() => User.findById(user.id)) + .then(newUser => { + expect(newUser.student).to.include(student.id) + })))) + }) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Wind', surname: 'Book', email: 'wind@book.com', username: 'windbook', password: hash, userType: 'teacher' })) + .then(student => addStudent(new ObjectId().toString(), student.id)) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on non-existing student', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Wind', surname: 'Book', email: 'wind@book.com', username: 'windbook', password: hash, userType: 'teacher' })) + .then(user => addStudent(user.id, new ObjectId().toString())) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('student not found') + }) + }) + + it('fails on existing student', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mac', surname: 'Book', email: 'mac@book.com', username: 'macbook', password: hash, userType: 'teacher' }) + .then(user => User.create({ name: 'Wind', surname: 'Book', email: 'wind@book.com', username: 'windbook', password: hash, userType: 'teacher' }) + .then(student => addStudent(user.id, student.id) + .then(() => User.findById(user.id) + .then(() => addStudent(user.id, student.id)))))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(DuplicityError) + expect(errorThrown.message).to.equal('student already exists for this user') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + try { + addStudent(123, new ObjectId().toString()) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid studentId', () => { + let errorThrown + try { + addStudent(new ObjectId().toString(), 123) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('studentId is not valid') + } + }) + + after(() => User.deleteMany().then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/addStudent.test.js b/staff/agustin-birman/project/api/logic/user/addStudent.test.js new file mode 100644 index 000000000..246369e86 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/addStudent.test.js @@ -0,0 +1,16 @@ +import addStudent from './addStudent.js' +import mongoose from 'mongoose' +import 'dotenv/config' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + addStudent('66a94dcb34505782bcd8cfd0', '66abce851a0dc4acbe205e41') + .then(() => console.log('user add')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }).catch(error => { console.error(error) }) diff --git a/staff/agustin-birman/project/api/logic/user/authenticateUser.js b/staff/agustin-birman/project/api/logic/user/authenticateUser.js new file mode 100644 index 000000000..23211483e --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/authenticateUser.js @@ -0,0 +1,31 @@ +import { User } from '../../data/index.js' +import { CredentialsError, SystemError } from 'com/errors.js' +import validate from 'com/validate.js' +import bcrypt from 'bcryptjs' + +const authenticateUser = (username, password) => { + validate.username(username) + validate.password(password) + + return User.findOne({ username }).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) { + throw new CredentialsError('user not found') + } + + return bcrypt.compare(password, user.password) + .catch(error => { throw new SystemError(error.message) }) + .then(match => { + if (!match) { + throw new CredentialsError('wrong password') + } + + return { id: user._id.toString(), role: user.userType } + + }) + }) +} + + +export default authenticateUser \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/authenticateUser.spec.js b/staff/agustin-birman/project/api/logic/user/authenticateUser.spec.js new file mode 100644 index 000000000..59b9fb375 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/authenticateUser.spec.js @@ -0,0 +1,81 @@ +import 'dotenv/config' +import mongoose from 'mongoose' +import bcrypt from 'bcryptjs' + +import { expect } from 'chai' + +import { User } from '../../data/index.js' + +import authenticateUser from './authenticateUser.js' +import { ContentError, CredentialsError } from 'com/errors.js' + +const { MONGODB_URL_TEST } = process.env + + +describe('authenticateUser', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => User.deleteMany()) + + it('succeds on existing user', () => + bcrypt.hash('123123123', 8) + .then(hash => User.create({ name: 'Mac', surname: 'Book', email: 'mac@book.com', username: 'macbook', password: hash, userType: 'teacher' })) + .then(() => authenticateUser('macbook', '123123123')) + .then(userAuth => { + expect(userAuth).to.be.an('object') + + }) + ) + + it('fails on non-existing user', () => { + let errorThrown + authenticateUser('RandomName', '12345678') + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.instanceOf(CredentialsError) + expect(errorThrown.message).to.equal('user not found') + }) + } + ) + + it('fails on existing user by wrong password', () => { + let errorThrown + + return bcrypt.hash('234234234', 8) + .then(hash => User.create({ name: 'Mac', surname: 'Book', email: 'mac@book.com', username: 'macbook', password: hash, userType: 'teacher' })) + .then(() => authenticateUser('macbook', '123123123')) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.instanceOf(CredentialsError) + expect(errorThrown.message).to.equal('wrong password') + }) + }) + + it('fails on invalid username', () => { + let errorThrown + + try { + authenticateUser(1234567890, '123123123') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('username is not valid') + } + }) + + it('fails on invalid password', () => { + let errorThrown + + try { + authenticateUser('pepe', '12312') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('password is not valid') + } + }) + + after(() => User.deleteMany().then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/authenticateUser.test.js b/staff/agustin-birman/project/api/logic/user/authenticateUser.test.js new file mode 100644 index 000000000..0bb014fca --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/authenticateUser.test.js @@ -0,0 +1,19 @@ +import authenticateUser from './authenticateUser.js' +import mongoose from 'mongoose' +import 'dotenv/config' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + authenticateUser('pepa', '123123123') + .then(userId => console.log('user authenticated', userId)) + .catch(error => console.error(error)) + } catch (error) { + + } + }) + .catch(error => console.error(error)) + + diff --git a/staff/agustin-birman/project/api/logic/user/getStudents.js b/staff/agustin-birman/project/api/logic/user/getStudents.js new file mode 100644 index 000000000..584a71d9c --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getStudents.js @@ -0,0 +1,27 @@ +import validate from "com/validate.js" +import { User } from "../../data/index.js" +import { NotFoundError, SystemError } from "com/errors.js" + +const getStudents = userId => { + validate.id(userId, 'userId') + + return User.findById(userId).populate('student').exec() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return User.find({ _id: { $in: user.student } }) + .catch(error => { throw new SystemError(error.message) }) + .then(students => { + return students.map(student => { + const studentObj = student.toObject() + studentObj.id = studentObj._id + delete studentObj._id + return studentObj + }) + }) + }) +} + +export default getStudents diff --git a/staff/agustin-birman/project/api/logic/user/getStudents.spec.js b/staff/agustin-birman/project/api/logic/user/getStudents.spec.js new file mode 100644 index 000000000..360fad427 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getStudents.spec.js @@ -0,0 +1,87 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' + +import bcrypt from 'bcryptjs' + +import getStudents from './getStudents.js' +import { User } from '../../data/index.js' +import { expect } from 'chai' +import addStudent from './addStudent.js' +import { ContentError, NotFoundError } from 'com/errors.js' + +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + + +describe('getStudents', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => User.deleteMany()) + + it('succeds on getting students', () => + bcrypt.hash('12345678', 8) + .then(hash => Promise.all([User.create({ + name: 'Mocha', + surname: 'Chai', + email: 'Mocha@Chai.com', + username: 'MochaChai', + password: hash, + userType: 'teacher' + }), User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + }), + User.create({ + name: 'Test2', + surname: 'User2', + email: 'test2@user.com', + username: 'testuser2', + password: hash, + userType: 'teacher' + })])) + .then(([teacher, student1, student2]) => addStudent(teacher.id, student1.id) + .then(() => addStudent(teacher.id, student2.id) + .then(() => User.findById(teacher.id) + .then(user => getStudents(user.id)))) + .then(listStudents => { + expect(listStudents).to.be.an('array') + expect(listStudents).to.have.lengthOf(2) + + const usernames = listStudents.map(student => student.username) + + expect(usernames).to.include('testuser') + expect(usernames).to.include('testuser2') + }))) + + + it('fails on non-existing user', () => { + let errorThrown + + getStudents(new ObjectId().toString()) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + + try { + getStudents(1234567890) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + after(() => User.deleteMany().then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/getStudents.test.js b/staff/agustin-birman/project/api/logic/user/getStudents.test.js new file mode 100644 index 000000000..e34b32870 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getStudents.test.js @@ -0,0 +1,18 @@ +import mongoose from 'mongoose' +import 'dotenv/config' +import getStudents from './getStudents.js' + + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + getStudents('66a94dcb34505782bcd8cfd0') + .then(students => console.log(students)) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) diff --git a/staff/agustin-birman/project/api/logic/user/getTeachers.js b/staff/agustin-birman/project/api/logic/user/getTeachers.js new file mode 100644 index 000000000..3050fccb1 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getTeachers.js @@ -0,0 +1,31 @@ +import validate from "com/validate.js"; +import { User } from "../../data/index.js"; +import { NotFoundError, SystemError } from "com/errors.js"; + +const getTeachers = userId => { + validate.id(userId, 'userId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return User.find({ student: userId }).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(teachers => { + + const transformedTeachers = teachers.map(teacher => { + return { + id: teacher._id, + username: teacher.username, + name: teacher.name, + surname: teacher.surname + } + }) + + return transformedTeachers + }) + }) +} +export default getTeachers diff --git a/staff/agustin-birman/project/api/logic/user/getTeachers.spec.js b/staff/agustin-birman/project/api/logic/user/getTeachers.spec.js new file mode 100644 index 000000000..14533d152 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getTeachers.spec.js @@ -0,0 +1,87 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' + +import bcrypt from 'bcryptjs' + +import getTeachers from './getTeachers.js' +import { User } from '../../data/index.js' +import { expect } from 'chai' +import addStudent from './addStudent.js' +import { ContentError, NotFoundError } from 'com/errors.js' + +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + + +describe('getTeachers', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => User.deleteMany()) + + it('succeds on getting students', () => + bcrypt.hash('12345678', 8) + .then(hash => Promise.all([User.create({ + name: 'Mocha', + surname: 'Chai', + email: 'Mocha@Chai.com', + username: 'MochaChai', + password: hash, + userType: 'teacher' + }), User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + }), + User.create({ + name: 'Test2', + surname: 'User2', + email: 'test2@user.com', + username: 'testuser2', + password: hash, + userType: 'student' + })])) + .then(([teacher1, teacher2, student]) => addStudent(teacher1.id, student.id) + .then(() => addStudent(teacher2.id, student.id) + .then(() => User.findById(student.id) + .then(user => getTeachers(user.id)))) + .then(listTeachers => { + expect(listTeachers).to.be.an('array') + expect(listTeachers).to.have.lengthOf(2) + + const usernames = listTeachers.map(teacher => teacher.username) + + expect(usernames).to.include('MochaChai') + expect(usernames).to.include('testuser') + }))) + + + it('fails on non-existing user', () => { + let errorThrown + + getTeachers(new ObjectId().toString()) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + + try { + getTeachers(1234567890) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + after(() => User.deleteMany().then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/getTeachers.test.js b/staff/agustin-birman/project/api/logic/user/getTeachers.test.js new file mode 100644 index 000000000..f6475b16c --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getTeachers.test.js @@ -0,0 +1,17 @@ +import mongoose from 'mongoose' +import 'dotenv/config' +import getTeachers from './getTeachers.js' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + getTeachers('66a94dcb34505782bcd8cfd0') + .then(students => console.log(students)) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) diff --git a/staff/agustin-birman/project/api/logic/user/getUserInfo.js b/staff/agustin-birman/project/api/logic/user/getUserInfo.js new file mode 100644 index 000000000..88fd29816 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getUserInfo.js @@ -0,0 +1,27 @@ +import validate from "com/validate.js"; +import { User } from "../../data/index.js"; +import { NotFoundError, SystemError } from "com/errors.js"; + +const getUserInfo = (userId, userInfoId) => { + validate.id(userId, 'userId') + validate.id(userInfoId, 'userInfoId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('user not found') + + return User.findById(userInfoId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(userInfo => { + if (!userInfo) + throw new NotFoundError('userInfo not found') + userInfo.id = userInfo._id + delete userInfo._id + return userInfo + }) + }) +} + +export default getUserInfo \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/getUserInfo.spec.js b/staff/agustin-birman/project/api/logic/user/getUserInfo.spec.js new file mode 100644 index 000000000..5f2494cd3 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getUserInfo.spec.js @@ -0,0 +1,114 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' + +import bcrypt from 'bcryptjs' +import getUserInfo from './getUserInfo.js' +import { User } from '../../data/index.js' +import { NotFoundError, ContentError } from 'com/errors.js' + +import { expect } from 'chai' + +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + + +describe('getUserInfo', () => { + before(() => mongoose.connect(MONGODB_URL_TEST).then(() => User.deleteMany())) + + beforeEach(() => User.deleteMany()) + + it('succeeds get userName from existing user', () => + bcrypt.hash('1234', 8) + .then(hash => Promise.all([User.create({ + name: 'Mocha', + surname: 'Chai', + email: 'Mocha@Chai.com', + username: 'MochaChai', + password: hash, + userType: 'teacher' + }), User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + })])) + .then(([user, targetUser]) => getUserInfo(user.id, targetUser.id) + .then(userInfo => { + expect(userInfo).to.have.property('name').that.equals('Test'); + expect(userInfo).to.have.property('surname').that.equals('User'); + expect(userInfo).to.have.property('email').that.equals('test@user.com'); + expect(userInfo).to.have.property('username').that.equals('testuser'); + }) + )) + + it('fails on non-existing user', () => { + let errorThrown + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + }) + .then(userInfo => getUserInfo(new ObjectId().toString(), userInfo.id))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on non-existing targetUser', () => { + let errorThrown + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + }) + .then(user => getUserInfo(user.id, new ObjectId().toString()))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('userInfo not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + + try { + getUserInfo(12345, new ObjectId().toString()) + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid userInfo', () => { + let errorThrown + + try { + getUserInfo(new ObjectId().toString(), 12345) + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userInfoId is not valid') + } + }) + + after(() => User.deleteMany().then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/getUserInfo.test.js b/staff/agustin-birman/project/api/logic/user/getUserInfo.test.js new file mode 100644 index 000000000..eaaf3b0e8 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getUserInfo.test.js @@ -0,0 +1,16 @@ +import getUserInfo from './getUserInfo.js' +import mongoose from 'mongoose' +import 'dotenv/config' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + getUserInfo('66a94dcb34505782bcd8cfd0', '66abce851a0dc4acbe205e41') + .then(userInfo => console.log(userInfo)) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }).catch(error => { console.error(error) }) diff --git a/staff/agustin-birman/project/api/logic/user/getUserName.js b/staff/agustin-birman/project/api/logic/user/getUserName.js new file mode 100644 index 000000000..761a746a3 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getUserName.js @@ -0,0 +1,27 @@ +import validate from 'com/validate.js' +import { User } from '../../data/index.js' +import { SystemError, NotFoundError } from 'com/errors.js' + +const getUserName = (userId, targetUserId) => { + validate.id(userId, 'userId') + validate.id(targetUserId, 'targetUserId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) { + throw new NotFoundError('user not found') + } + + return User.findById(targetUserId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('targetUser not found') + + return user.name + }) + }) +} + +export default getUserName \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/getUserName.spec.js b/staff/agustin-birman/project/api/logic/user/getUserName.spec.js new file mode 100644 index 000000000..274e99023 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getUserName.spec.js @@ -0,0 +1,101 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' + +import bcrypt from 'bcryptjs' + +import getUserName from './getUserName.js' +import { User } from '../../data/index.js' +import { NotFoundError, ContentError } from 'com/errors.js' + +import { expect } from 'chai' + +const { MONGODB_URL_TEST } = process.env + +const { ObjectId } = Types + + +describe('getUserName', () => { + before(() => mongoose.connect(MONGODB_URL_TEST).then(() => User.deleteMany())) + + beforeEach(() => User.deleteMany()) + + it('succeeds get userName from existing user', () => + bcrypt.hash('1234', 8) + .then(hash => Promise.all([User.create({ + name: 'Mocha', + surname: 'Chai', + email: 'Mocha@Chai.com', + username: 'MochaChai', + password: hash, + userType: 'teacher' + }), User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + })])) + .then(([user, targetUser]) => getUserName(user.id, targetUser.id)) + .then(name => { + expect(name).to.be.a.string + expect(name).to.be.equal('Test') + }) + ) + + it('fails on non-existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(targetUser => getUserName(new ObjectId().toString(), targetUser.id)) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on non-existing targetUser', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'mocha@chai.es', username: 'mochachai', password: hash, userType: 'teacher' })) + .then(user => getUserName(user.id, new ObjectId().toString())) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('targetUser not found') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + + try { + getUserName(12345, new ObjectId().toString()) + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid targetUserId', () => { + let errorThrown + + try { + getUserName(new ObjectId().toString(), 12345) + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('targetUserId is not valid') + } + }) + + after(() => User.deleteMany().then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/getUserName.test.js b/staff/agustin-birman/project/api/logic/user/getUserName.test.js new file mode 100644 index 000000000..ef267d2a4 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getUserName.test.js @@ -0,0 +1,18 @@ +import mongoose from 'mongoose' +import getUserName from './getUserName.js' +import 'dotenv/config' + + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + getUserName('6685c5e29e4dca5e7bbf9c49', '6685c5e29e4dca5e7bbf9c49') + .then(name => console.log('user name retrieved', name)) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }) + .catch(error => console.error(error)) diff --git a/staff/agustin-birman/project/api/logic/user/getUserStats.js b/staff/agustin-birman/project/api/logic/user/getUserStats.js new file mode 100644 index 000000000..26aaf4050 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getUserStats.js @@ -0,0 +1,63 @@ +import validate from 'com/validate.js' +import { Activity, Answer, Exercise, User } from '../../data/index.js' +import { SystemError, NotFoundError } from 'com/errors.js' + +const getUserStats = (userId, targetUserId) => { + validate.id(userId, 'userId') + validate.id(targetUserId, 'targetUserId') + + return User.findById(userId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) { + throw new NotFoundError('user not found') + } + + return User.findById(targetUserId).lean() + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) + throw new NotFoundError('targetUser not found') + + return Answer.find({ student: targetUserId }) + .catch(error => { throw new SystemError(error.message) }) + .then(answers => { + const exerciseIds = answers.map(answer => answer.exercise) + + return Exercise.find({ _id: { $in: exerciseIds } }) + .catch(error => { throw new SystemError(error.message) }) + .then(exercises => { + + let countCorrectExercises = 0 + exercises.forEach(exercise => { + answers.forEach(answer => { + if (answer.exercise.toString() === exercise._id.toString()) { + switch (exercise.type) { + case 'completeSentence': + if (answer.answer === exercise.answer) { + countCorrectExercises += 1 + } + break + case 'orderSentence': + if (answer.answer === exercise.sentence) { + countCorrectExercises += 1 + } + break + } + } + }) + }) + const activityIds = exercises.map(exercise => exercise.activity) + return Activity.countDocuments({ _id: { $in: activityIds } }) + .catch(error => { throw new SystemError(error.message) }) + .then(countActivities => { + + return { countActivities, countExercises: exercises.length, countCorrectExercises } + }) + + }) + }) + }) + }) +} +export default getUserStats \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/getUserStats.test.js b/staff/agustin-birman/project/api/logic/user/getUserStats.test.js new file mode 100644 index 000000000..1769f5ea3 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/getUserStats.test.js @@ -0,0 +1,16 @@ +import getUserStats from './getUserStats.js' +import mongoose from 'mongoose' +import 'dotenv/config' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + getUserStats('66a94dcb34505782bcd8cfd0', '66a94dcb34505782bcd8cfd0') + .then(userInfo => console.log(userInfo)) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }).catch(error => { console.error(error) }) diff --git a/staff/agustin-birman/project/api/logic/user/registerUser.js b/staff/agustin-birman/project/api/logic/user/registerUser.js new file mode 100644 index 000000000..674aab110 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/registerUser.js @@ -0,0 +1,41 @@ +import validate from 'com/validate.js' +import { User } from '../../data/index.js' +import { SystemError, DuplicityError } from 'com/errors.js' +import bcrypt from 'bcryptjs' + +const registerUser = (name, surname, email, username, password, passwordRepeat, userType) => { + validate.name(name) + validate.name(surname, 'surname') + validate.email(email) + validate.username(username) + validate.password(password) + validate.passowrdsMatch(password, passwordRepeat) + validate.userType(userType) + + return User.findOne({ $or: [{ email }, { username }] }) + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (user) { + throw new DuplicityError('user already exists') + } + + return bcrypt.hash(password, 8) + .catch(error => { throw new SystemError(error.message) }) + .then(hash => { + const newUser = { + name, + surname, + email, + username, + password: hash, + userType, + } + + return User.create(newUser) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { }) + }) + }) +} + +export default registerUser \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/registerUser.spec.js b/staff/agustin-birman/project/api/logic/user/registerUser.spec.js new file mode 100644 index 000000000..c22ae1c68 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/registerUser.spec.js @@ -0,0 +1,135 @@ +import 'dotenv/config' +import mongoose from 'mongoose' +import bcrypt from 'bcryptjs' + +import { expect } from 'chai' + +import { User } from '../../data/index.js' + +import registerUser from './registerUser.js' +import { ContentError, CredentialsError, DuplicityError, MatchError } from 'com/errors.js' + +const { MONGODB_URL_TEST } = process.env + + +describe('registerUser', () => { + before(() => mongoose.connect(MONGODB_URL_TEST).then(() => User.deleteMany())) + + beforeEach(() => User.deleteMany()) + + it('succeeds on new user', () => + registerUser('Mocha', 'Chai', 'Mocha@Chai.com', 'MochaChai', '12345678', '12345678', 'teacher') + .then(() => User.findOne()) + .then(user => { + expect(user.name).to.equal('Mocha') + expect(user.surname).to.equal('Chai') + expect(user.email).to.equal('Mocha@Chai.com') + expect(user.username).to.equal('MochaChai') + + return bcrypt.compare('12345678', user.password) + }) + .then((match) => expect(match).to.be.true) + ) + + + it('fails on existing user', () => { + let errorThrown + + return bcrypt.hash('12345678', 8) + .then(hash => User.create({ name: 'Mocha', surname: 'Chai', email: 'Mocha@Chai.com', username: 'MochaChai', password: hash, userType: 'teacher' })) + .then(() => registerUser('Mocha', 'Chai', 'Mocha@Chai.com', 'MochaChai', '12345678', '12345678', 'teacher')) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.instanceOf(DuplicityError) + expect(errorThrown.message).to.equal('user already exists') + }) + }) + + it('fails on invalid name', () => { + let errorThrown + try { + registerUser(1234, 'Chai', 'Mocha@Chai.com', 'MochaChai', '12345678', '12345678') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('name is not valid') + } + }) + + it('fails on invalid surname', () => { + let errorThrown + try { + registerUser('Mocha', 1234, 'Mocha@Chai.com', 'MochaChai', '1234', '1234') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('surname is not valid') + } + }) + + it('fails on invalid email', () => { + let errorThrown + try { + registerUser('Mocha', 'Chai', 'MochaChai.com', 'MochaChai', '1234', '1234') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('email is not valid') + } + }) + + it('fails on invalid username', () => { + let errorThrown + try { + registerUser('Mocha', 'Chai', 'Mocha@Chai.com', 1234, '1234', '1234') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('username is not valid') + } + }) + + it('fails on invalid password', () => { + let errorThrown + try { + registerUser('Mocha', 'Chai', 'Mocha@Chai.com', 'MochaChai', 1234, '1234') + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('password is not valid') + } + }) + + it('fails on non-matching password repeat', () => { + let errorThrown + try { + registerUser('Mocha', 'Chai', 'Mocha@Chai.com', 'MochaChai', '12345678', 6666) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(MatchError) + expect(errorThrown.message).to.equal('passwords don\'t match') + } + }) + + it('fails on invalid userType', () => { + let errorThrown + try { + registerUser('Mocha', 'Chai', 'Mocha@Chai.com', 'MochaChai', '12345678', '12345678', 12345678) + } catch (error) { + errorThrown = error + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userType is not valid') + } + }) + + + + after(() => User.deleteMany().then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/registerUser.test.js b/staff/agustin-birman/project/api/logic/user/registerUser.test.js new file mode 100644 index 000000000..2c845e18f --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/registerUser.test.js @@ -0,0 +1,16 @@ +import registerUser from './registerUser.js' +import mongoose from 'mongoose' +import 'dotenv/config' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + registerUser('Peter', 'Pan', 'peter@pan.com', 'pepa', '123123123', '123123123', 'teacher') + .then(() => console.log('user registered')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }).catch(error => { console.error(error) }) diff --git a/staff/agustin-birman/project/api/logic/user/removeStudent.js b/staff/agustin-birman/project/api/logic/user/removeStudent.js new file mode 100644 index 000000000..9310e69d3 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/removeStudent.js @@ -0,0 +1,29 @@ +import { NotFoundError, SystemError } from "com/errors.js" +import validate from "com/validate.js" +import { User } from '../../data/index.js' + +const removeStudent = (userId, studentId) => { + validate.id(userId, 'userId') + validate.id(studentId, 'studentId') + + return User.findById(userId) + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) { + throw new NotFoundError('user not found') + } + + const index = user.student.indexOf(studentId) + if (index > -1) { + user.student.splice(index, 1) + + return User.updateOne({ _id: user._id }, { student: user.student }) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { }) + } else { + throw new NotFoundError('student is not in your list') + } + }) +} + +export default removeStudent diff --git a/staff/agustin-birman/project/api/logic/user/removeStudent.spec.js b/staff/agustin-birman/project/api/logic/user/removeStudent.spec.js new file mode 100644 index 000000000..338ecfa0f --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/removeStudent.spec.js @@ -0,0 +1,113 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' +import { expect } from 'chai' +import bcrypt from 'bcryptjs' + +import { User } from '../../data/index.js' +import removeStudent from './removeStudent.js' +import { ContentError, NotFoundError } from 'com/errors.js' + +const { MONGODB_URL_TEST } = process.env +const { ObjectId } = Types + +describe('removeStudent', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => User.deleteMany()) + + it('succeeds on removing student', () => + bcrypt.hash('12345678', 8) + .then(hash => User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + }) + .then(studentInfo => User.create({ + name: 'Mocha', + surname: 'Chai', + email: 'Mocha@Chai.com', + username: 'MochaChai', + password: hash, + userType: 'teacher', + student: studentInfo.id + }) + .then(teacher => removeStudent(teacher.id, studentInfo.id) + .then(() => User.findById(teacher.id))))) + .then(teacherInfo => { + expect(teacherInfo.student).to.be.an('array') + expect(teacherInfo.student).to.have.lengthOf(0) + }) + ) + + it('fails on non-existing teacher', () => { + + bcrypt.hash('12345678', 8) + .then(hash => User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + }) + .then(studentInfo => removeStudent(new ObjectId().toString(), studentInfo.id))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + }) + }) + + it('fails on non-existing student', () => { + + bcrypt.hash('12345678', 8) + .then(hash => User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + }) + .then(teacher => removeStudent(teacher.id, new ObjectId().toString()))) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('student is not in your list') + }) + }) + + it('fails on invalid userId', () => { + let errorThrown + + try { + removeStudent(12345, new ObjectId().toString()) + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid studentId', () => { + let errorThrown + + try { + removeStudent(new ObjectId().toString(), 12345) + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('studentId is not valid') + } + }) + + + after(() => User.deleteMany().then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/removeStudent.test.js b/staff/agustin-birman/project/api/logic/user/removeStudent.test.js new file mode 100644 index 000000000..90680eb5b --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/removeStudent.test.js @@ -0,0 +1,16 @@ +import removeStudent from './removeStudent.js' +import mongoose from 'mongoose' +import 'dotenv/config' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + removeStudent('66a94dcb34505782bcd8cfd0', '66abce851a0dc4acbe205e41') + .then(() => console.log('user removed')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }).catch(error => { console.error(error) }) diff --git a/staff/agustin-birman/project/api/logic/user/removeTeacher.js b/staff/agustin-birman/project/api/logic/user/removeTeacher.js new file mode 100644 index 000000000..c78831f9b --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/removeTeacher.js @@ -0,0 +1,45 @@ +import { NotFoundError, SystemError } from "com/errors.js" +import validate from "com/validate.js" +import { User } from '../../data/index.js' + +const removeTeacher = (userId, teacherId) => { + validate.id(userId, 'userId') + validate.id(teacherId, 'teacherId') + + return User.findById(userId) + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) { + throw new NotFoundError('user not found') + } + + return User.findById(teacherId) + .catch(error => { throw new SystemError(error.message) }) + .then(teacher => { + if (!teacher) { + throw new NotFoundError('teacher not found') + } + + return User.find({ _id: { $in: user.student } }) + .catch(error => { throw new SystemError(error.message) }) + .then(user => { + if (!user) { + throw new NotFoundError('user not found') + } + + const index = teacher.student.indexOf(userId) + if (index > -1) { + teacher.student.splice(index, 1) + + return User.updateOne({ _id: teacher._id }, { student: teacher.student }) + .catch(error => { throw new SystemError(error.message) }) + .then(() => { }) + } else { + throw new NotFoundError('Student is not in the teacher\'s student list') + } + }) + }) + }) +} + +export default removeTeacher diff --git a/staff/agustin-birman/project/api/logic/user/removeTeacher.spec.js b/staff/agustin-birman/project/api/logic/user/removeTeacher.spec.js new file mode 100644 index 000000000..4928cb1a7 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/removeTeacher.spec.js @@ -0,0 +1,114 @@ +import 'dotenv/config' +import mongoose, { Types } from 'mongoose' +import { expect } from 'chai' +import bcrypt from 'bcryptjs' + +import { User } from '../../data/index.js' +import removeTeacher from './removeTeacher.js' +import { ContentError, NotFoundError } from 'com/errors.js' + +const { MONGODB_URL_TEST } = process.env +const { ObjectId } = Types + + +describe('removeTeacher', () => { + before(() => mongoose.connect(MONGODB_URL_TEST)) + + beforeEach(() => User.deleteMany()) + + it('succeeds on removing teacher', () => + bcrypt.hash('12345678', 8) + .then(hash => User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'student' + }) + .then(student => User.create({ + name: 'Mocha', + surname: 'Chai', + email: 'Mocha@Chai.com', + username: 'MochaChai', + password: hash, + userType: 'teacher', + student: student.id + }) + .then(teacher => removeTeacher(student.id, teacher.id) + .then(() => User.findById(teacher.id))))) + .then(teacherInfo => { + expect(teacherInfo.student).to.be.an('array') + expect(teacherInfo.student).to.have.lengthOf(0) + }) + ) + + it('fails on non-existing user', () => { + + bcrypt.hash('12345678', 8) + .then(hash => User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + }) + .then(teacherInfo => removeTeacher(new ObjectId().toString(), teacherInfo.id)) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('user not found') + })) + }) + + it('fails on non-existing student', () => { + + bcrypt.hash('12345678', 8) + .then(hash => User.create({ + name: 'Test', + surname: 'User', + email: 'test@user.com', + username: 'testuser', + password: hash, + userType: 'teacher' + }) + .then(teacher => removeTeacher(teacher.id, new ObjectId().toString())) + .catch(error => errorThrown = error) + .finally(() => { + expect(errorThrown).to.be.an.instanceOf(NotFoundError) + expect(errorThrown.message).to.equal('student is not in your list') + })) + }) + + it('fails on invalid userId', () => { + let errorThrown + + try { + removeTeacher(12345, new ObjectId().toString()) + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('userId is not valid') + } + }) + + it('fails on invalid teacherId', () => { + let errorThrown + + try { + removeTeacher(new ObjectId().toString(), 12345) + } catch (error) { + errorThrown = error + + } finally { + expect(errorThrown).to.be.instanceOf(ContentError) + expect(errorThrown.message).to.equal('teacherId is not valid') + } + }) + + + after(() => User.deleteMany().then(() => mongoose.disconnect())) +}) \ No newline at end of file diff --git a/staff/agustin-birman/project/api/logic/user/removeTeacher.test.js b/staff/agustin-birman/project/api/logic/user/removeTeacher.test.js new file mode 100644 index 000000000..9d2ca6b82 --- /dev/null +++ b/staff/agustin-birman/project/api/logic/user/removeTeacher.test.js @@ -0,0 +1,16 @@ +import mongoose from 'mongoose' +import 'dotenv/config' +import removeTeacher from './removeTeacher.js' + +const { MONGODB_URL } = process.env + +mongoose.connect(MONGODB_URL) + .then(() => { + try { + removeTeacher('66abce851a0dc4acbe205e41', '66a94dcb34505782bcd8cfd0') + .then(() => console.log('user removed')) + .catch(error => console.error(error)) + } catch (error) { + console.error(error) + } + }).catch(error => { console.error(error) }) diff --git a/staff/agustin-birman/project/api/package-lock.json b/staff/agustin-birman/project/api/package-lock.json new file mode 100644 index 000000000..2d42df7b9 --- /dev/null +++ b/staff/agustin-birman/project/api/package-lock.json @@ -0,0 +1,2442 @@ +{ + "name": "api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "api", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "bcryptjs": "^2.4.3", + "com": "file:../com", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.5.1" + }, + "devDependencies": { + "c8": "^10.1.2", + "chai": "^5.1.1", + "mocha": "^10.7.0", + "monocart-coverage-reports": "^2.10.3" + } + }, + "../com": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.8.tgz", + "integrity": "sha512-qKwC/M/nNNaKUBMQ0nuzm47b7ZYWQHN3pcXq4IIcoSBc2hOIrflAxJduIvvqmhoz3gR2TacTAs8vlsCVPkiEdQ==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", + "integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-loose": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/acorn-loose/-/acorn-loose-8.4.0.tgz", + "integrity": "sha512-M0EUka6rb+QC4l9Z3T0nJEzNOO7JcoJlYMrBlyBCiFSXRyxjLKayd4TbQs2FDRWQU1h9FR7QVNHt+PEaoNL5rQ==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.3.tgz", + "integrity": "sha512-MxXdReSRhGO7VlFe1bRG/oI7/mdLV9B9JJT0N8vZOhF7gFRR5l3M8W9G8JxmKV+JC5mGqJ0QvqfSOLsCPa4nUw==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "dev": true, + "license": "ISC" + }, + "node_modules/bson": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz", + "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c8": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.2.tgz", + "integrity": "sha512-Qr6rj76eSshu5CgRYvktW0uM0CFY0yi4Fd5D0duDXO6sYinyopmftUiJVuzBQxQcwQLor7JWDVRP+dUfCmzgJw==", + "dev": true, + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@istanbuljs/schema": "^0.1.3", + "find-up": "^5.0.0", + "foreground-child": "^3.1.1", + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.6", + "test-exclude": "^7.0.1", + "v8-to-istanbul": "^9.0.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "c8": "bin/c8.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "monocart-coverage-reports": "^2" + }, + "peerDependenciesMeta": { + "monocart-coverage-reports": { + "optional": true + } + } + }, + "node_modules/c8/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/c8/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/c8/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/com": { + "resolved": "../com", + "link": true + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/console-grid": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/console-grid/-/console-grid-2.2.2.tgz", + "integrity": "sha512-ohlgXexdDTKLNsZz7DSJuCAwmRc8omSS61txOk39W3NOthgKGr1a1jJpZ5BCQe4PlrwMw01OvPQ1Bl3G7Y/uFg==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.6.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "license": "MIT" + }, + "node_modules/eight-colors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/eight-colors/-/eight-colors-1.3.0.tgz", + "integrity": "sha512-hVoK898cR71ADj7L1LZWaECLaSkzzPtqGXIaKv4K6Pzb72QgjLVsQaNI+ELDQQshzFvgp5xTPkaYkPGqw3YR+g==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", + "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.2", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.6.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/jwa": { + "version": "1.4.1", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/kareem": { + "version": "2.6.3", + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/lz-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lz-utils/-/lz-utils-2.1.0.tgz", + "integrity": "sha512-CMkfimAypidTtWjNDxY8a1bc1mJdyEh04V2FfEQ5Zh8Nx4v7k850EYa+dOWGn9hKG5xOyHP5MkuduAZCTHRvJw==", + "dev": true + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "license": "MIT" + }, + "node_modules/methods": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.1.6", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mocha": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.7.0.tgz", + "integrity": "sha512-v8/rBWr2VO5YkspYINnvu81inSz2y3ODJrhO175/Exzor1RcEZZkizgE2A+w/CAXXoESS8Kys5E62dOHGHzULA==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/mongodb": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.7.0.tgz", + "integrity": "sha512-TMKyHdtMcO0fYBNORiYdmM25ijsHs+Njs963r4Tro4OQZzqYigAzYQouwWRg4OIaiLRUEGUh/1UAcH5lxdSLIA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.5", + "bson": "^6.7.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", + "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/mongoose": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.5.1.tgz", + "integrity": "sha512-OhVcwVl91A1G6+XpjDcpkGP7l7ikZkxa0DylX7NT/lcEqAjggzSdqDxb48A+xsDxqNAr0ntSJ1yiE3+KJTOd5Q==", + "dependencies": { + "bson": "^6.7.0", + "kareem": "2.6.3", + "mongodb": "6.7.0", + "mpath": "0.9.0", + "mquery": "5.0.0", + "ms": "2.1.3", + "sift": "17.1.3" + }, + "engines": { + "node": ">=16.20.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mongoose" + } + }, + "node_modules/mongoose/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/monocart-coverage-reports": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/monocart-coverage-reports/-/monocart-coverage-reports-2.10.3.tgz", + "integrity": "sha512-CVBjRABy5ygNMVnk5IOVZyB2gfiCUG2xxZuFd5D3nuiP/ja2XWC9GJ8ddgr4fXwrbm8vMkSjOxXs/mfvgP9pSA==", + "dev": true, + "workspaces": [ + "test" + ], + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jridgewell/sourcemap-codec": "^1.5.0", + "acorn": "^8.12.1", + "acorn-loose": "^8.4.0", + "acorn-walk": "^8.3.3", + "commander": "^12.1.0", + "console-grid": "^2.2.2", + "eight-colors": "^1.3.0", + "foreground-child": "^3.3.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.1.7", + "lz-utils": "^2.1.0", + "monocart-locator": "^1.0.2" + }, + "bin": { + "mcr": "lib/cli.js" + } + }, + "node_modules/monocart-locator": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/monocart-locator/-/monocart-locator-1.0.2.tgz", + "integrity": "sha512-v8W5hJLcWMIxLCcSi/MHh+VeefI+ycFmGz23Froer9QzWjrbg4J3gFJBuI/T1VLNoYxF47bVPPxq8ZlNX4gVCw==", + "dev": true + }, + "node_modules/mpath": { + "version": "0.9.0", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mquery": { + "version": "5.0.0", + "license": "MIT", + "dependencies": { + "debug": "4.x" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/mquery/node_modules/debug": { + "version": "4.3.5", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mquery/node_modules/ms": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/parseurl": { + "version": "1.3.3", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.6.2", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "license": "MIT", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sift": { + "version": "17.1.3", + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/staff/agustin-birman/project/api/package.json b/staff/agustin-birman/project/api/package.json new file mode 100644 index 000000000..1106cf24c --- /dev/null +++ b/staff/agustin-birman/project/api/package.json @@ -0,0 +1,33 @@ +{ + "name": "api", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "start": "node .", + "watch": "node --watch .", + "inspect": "node --inspect-brk .", + "test": "mocha 'logic/**/*.spec.js'", + "test-inspect": "mocha --inspect-brk 'logic/**/*.spec.js'", + "test-coverage": "c8 --experimental-monocart --reporter=html --reporter=text mocha 'logic/**/*.spec.js'" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "c8": "^10.1.2", + "chai": "^5.1.1", + "mocha": "^10.7.0", + "monocart-coverage-reports": "^2.10.3" + }, + "dependencies": { + "bcryptjs": "^2.4.3", + "com": "file:../com", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "jsonwebtoken": "^9.0.2", + "mongoose": "^8.5.1" + } +} \ No newline at end of file diff --git a/staff/agustin-birman/project/api/util/jsonwebtoken-promised.js b/staff/agustin-birman/project/api/util/jsonwebtoken-promised.js new file mode 100644 index 000000000..8de1bcfb1 --- /dev/null +++ b/staff/agustin-birman/project/api/util/jsonwebtoken-promised.js @@ -0,0 +1,40 @@ +import jwt from 'jsonwebtoken' + +const { JsonWebTokenError, TokenExpiredError } = jwt + +function sign(payload, secret, options) { + return new Promise((resolve, reject) => { + jwt.sign(payload, secret, options, (error, token) => { + if (error) { + reject(error) + + return + } + + resolve(token) + }) + }) +} + +function verify(token, secret) { + return new Promise((resolve, reject) => { + jwt.verify(token, secret, (error, payload) => { + if (error) { + reject(error) + + return + } + + resolve(payload) + }) + }) +} + +const jsonwebtoken = { + sign, + verify, + JsonWebTokenError, + TokenExpiredError +} + +export default jsonwebtoken \ No newline at end of file diff --git a/staff/agustin-birman/project/app/.env b/staff/agustin-birman/project/app/.env new file mode 100644 index 000000000..6930a0aae --- /dev/null +++ b/staff/agustin-birman/project/app/.env @@ -0,0 +1,2 @@ +VITE_APP_URL = http://192.168.1.20:5173 +VITE_API_URL = http://192.168.1.20:8080 \ No newline at end of file diff --git a/staff/agustin-birman/project/app/.eslintrc.cjs b/staff/agustin-birman/project/app/.eslintrc.cjs new file mode 100644 index 000000000..3e212e1d4 --- /dev/null +++ b/staff/agustin-birman/project/app/.eslintrc.cjs @@ -0,0 +1,21 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, + settings: { react: { version: '18.2' } }, + plugins: ['react-refresh'], + rules: { + 'react/jsx-no-target-blank': 'off', + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/staff/agustin-birman/project/app/.gitignore b/staff/agustin-birman/project/app/.gitignore new file mode 100644 index 000000000..a547bf36d --- /dev/null +++ b/staff/agustin-birman/project/app/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/staff/agustin-birman/project/app/index.html b/staff/agustin-birman/project/app/index.html new file mode 100644 index 000000000..d76fdbc63 --- /dev/null +++ b/staff/agustin-birman/project/app/index.html @@ -0,0 +1,21 @@ + + + + + + + + Project + + + + +
+ + + + + \ No newline at end of file diff --git a/staff/agustin-birman/project/app/package-lock.json b/staff/agustin-birman/project/app/package-lock.json new file mode 100644 index 000000000..ba6b24401 --- /dev/null +++ b/staff/agustin-birman/project/app/package-lock.json @@ -0,0 +1,5130 @@ +{ + "name": "app", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "app", + "version": "0.0.0", + "dependencies": { + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", + "com": "file:../com", + "react": "^18.2.0", + "react-bootstrap": "^2.10.4", + "react-dom": "^18.2.0", + "react-qr-code": "^2.0.15", + "react-router-dom": "^6.25.1" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "postcss": "^8.4.41", + "tailwindcss": "^3.4.10", + "tailwindscss": "^0.3.0", + "vite": "^5.2.0" + } + }, + "../com": { + "version": "1.0.0", + "license": "ISC" + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.24.6", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.6", + "@babel/generator": "^7.24.6", + "@babel/helper-compilation-targets": "^7.24.6", + "@babel/helper-module-transforms": "^7.24.6", + "@babel/helpers": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/template": "^7.24.6", + "@babel/traverse": "^7.24.6", + "@babel/types": "^7.24.6", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.6", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.24.6", + "@babel/helper-validator-option": "^7.24.6", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-module-imports": "^7.24.6", + "@babel/helper-simple-access": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.24.6", + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.6", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.4.tgz", + "integrity": "sha512-DSgLeL/FNcpXuzav5wfYvHCGvynXkJbn3Zvc3823AEe9nPwW9IK4UoCSS5yGymmQzN0pCPvivtgS6/8U2kkm1w==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.6", + "@babel/generator": "^7.24.6", + "@babel/helper-environment-visitor": "^7.24.6", + "@babel/helper-function-name": "^7.24.6", + "@babel/helper-hoist-variables": "^7.24.6", + "@babel/helper-split-export-declaration": "^7.24.6", + "@babel/parser": "^7.24.6", + "@babel/types": "^7.24.6", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.24.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.24.6", + "@babel/helper-validator-identifier": "^7.24.6", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.10.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.0", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.14", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.5.tgz", + "integrity": "sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@remix-run/router": { + "version": "1.18.0", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@restart/ui": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@restart/ui/-/ui-1.8.0.tgz", + "integrity": "sha512-xJEOXUOTmT4FngTmhdjKFRrVVF0hwCLNPdatLCHkyS4dkiSK12cEu1Y0fjxktjJrdst9jJIc5J6ihMJCoWEN/g==", + "dependencies": { + "@babel/runtime": "^7.21.0", + "@popperjs/core": "^2.11.6", + "@react-aria/ssr": "^3.5.0", + "@restart/hooks": "^0.4.9", + "@types/warning": "^3.0.0", + "dequal": "^2.0.3", + "dom-helpers": "^5.2.0", + "uncontrollable": "^8.0.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, + "node_modules/@restart/ui/node_modules/uncontrollable": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-8.0.4.tgz", + "integrity": "sha512-ulRWYWHvscPFc0QQXvyJjY6LIXU56f0h8pQFvhxiKk5V1fcI8gp9Ht9leVAhrVjzqMw0BgjspBINx9r6oyJUvQ==", + "peerDependencies": { + "react": ">=16.14.0" + } + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.18.0", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/helpers": { + "version": "0.5.12", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.12.tgz", + "integrity": "sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/react-transition-group": { + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.11.tgz", + "integrity": "sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz", + "integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.24.5", + "@babel/plugin-transform-react-jsx-self": "^7.24.5", + "@babel/plugin-transform-react-jsx-source": "^7.24.1", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/bootstrap-icons": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.11.3.tgz", + "integrity": "sha512-+3lpHrCw/it2/7lBL15VR0HEumaBss0+f/Lb6ZvHISn1mlK83jjFpooTLsMWbIjJMDjDjOExMsTxnXSIT4k4ww==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ] + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/color-convert": { + "version": "1.9.3", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/com": { + "resolved": "../com", + "link": true + }, + "node_modules/commander": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "license": "MIT" + }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.13", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", + "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.0.19", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "iterator.prototype": "^1.1.2", + "safe-array-concat": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.35.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz", + "integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.2", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.0.19", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.0", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.9.tgz", + "integrity": "sha512-QK49YrBAo5CLNLseZ7sZgvgTy21E6NEw22eZqc4teZfH8pxV3yXc9XXOYfUI6JNpw7mfHNkAeWtBxrTyykB6HA==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.17.1", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.13.1", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "get-intrinsic": "^1.2.1", + "has-symbols": "^1.0.3", + "reflect.getprototypeof": "^1.0.4", + "set-function-name": "^2.0.1" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.0.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-import/node_modules/resolve": { + "version": "1.22.8", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types-extra": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/prop-types-extra/-/prop-types-extra-1.1.1.tgz", + "integrity": "sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==", + "dependencies": { + "react-is": "^16.3.2", + "warning": "^4.0.0" + }, + "peerDependencies": { + "react": ">=0.14.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qr.js": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/qr.js/-/qr.js-0.0.0.tgz", + "integrity": "sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ==" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-bootstrap": { + "version": "2.10.4", + "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-2.10.4.tgz", + "integrity": "sha512-W3398nBM2CBfmGP2evneEO3ZZwEMPtHs72q++eNw60uDGDAdiGn0f9yNys91eo7/y8CTF5Ke1C0QO8JFVPU40Q==", + "dependencies": { + "@babel/runtime": "^7.24.7", + "@restart/hooks": "^0.4.9", + "@restart/ui": "^1.6.9", + "@types/react-transition-group": "^4.4.6", + "classnames": "^2.3.2", + "dom-helpers": "^5.2.1", + "invariant": "^2.2.4", + "prop-types": "^15.8.1", + "prop-types-extra": "^1.1.0", + "react-transition-group": "^4.4.5", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "@types/react": ">=16.14.8", + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "license": "MIT" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-qr-code": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/react-qr-code/-/react-qr-code-2.0.15.tgz", + "integrity": "sha512-MkZcjEXqVKqXEIMVE0mbcGgDpkfSdd8zhuzXEl9QzYeNcw8Hq2oVIzDLWuZN2PQBwM5PWjc2S31K8Q1UbcFMfw==", + "dependencies": { + "prop-types": "^15.8.1", + "qr.js": "0.0.0" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.25.1", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.18.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.1.tgz", + "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==", + "dependencies": { + "@remix-run/router": "1.18.0", + "react-router": "6.25.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.1", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "globalthis": "^1.0.3", + "which-builtin-type": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.18.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.0", + "@rollup/rollup-android-arm64": "4.18.0", + "@rollup/rollup-darwin-arm64": "4.18.0", + "@rollup/rollup-darwin-x64": "4.18.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", + "@rollup/rollup-linux-arm-musleabihf": "4.18.0", + "@rollup/rollup-linux-arm64-gnu": "4.18.0", + "@rollup/rollup-linux-arm64-musl": "4.18.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", + "@rollup/rollup-linux-riscv64-gnu": "4.18.0", + "@rollup/rollup-linux-s390x-gnu": "4.18.0", + "@rollup/rollup-linux-x64-gnu": "4.18.0", + "@rollup/rollup-linux-x64-musl": "4.18.0", + "@rollup/rollup-win32-arm64-msvc": "4.18.0", + "@rollup/rollup-win32-ia32-msvc": "4.18.0", + "@rollup/rollup-win32-x64-msvc": "4.18.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.11", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.7", + "regexp.prototype.flags": "^1.5.2", + "set-function-name": "^2.0.2", + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", + "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/resolve": { + "version": "1.22.8", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindscss": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tailwindscss/-/tailwindscss-0.3.0.tgz", + "integrity": "sha512-fv6JSA5tW++MUeZ8SUwBc3pihiHPDdZZrQlwxTr7igvCZZQ5NkK9yXvI7CZsm+KesG//rcq4CEkgA9d+pJwHtg==", + "dev": true, + "bin": { + "tailwindscss": "bin/cli.js" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/thenify": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", + "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "function.prototype.name": "^1.1.5", + "has-tostringtag": "^1.0.0", + "is-async-function": "^2.0.0", + "is-date-object": "^1.0.5", + "is-finalizationregistry": "^1.0.2", + "is-generator-function": "^1.0.10", + "is-regex": "^1.1.4", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.4.5", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/staff/agustin-birman/project/app/package.json b/staff/agustin-birman/project/app/package.json new file mode 100644 index 000000000..2b5d3816e --- /dev/null +++ b/staff/agustin-birman/project/app/package.json @@ -0,0 +1,38 @@ +{ + "name": "app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "start": "vite --host", + "inspect": "vite --inspect-brk .", + "dev": "vite", + "build": "vite build", + "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": { + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", + "com": "file:../com", + "react": "^18.2.0", + "react-bootstrap": "^2.10.4", + "react-dom": "^18.2.0", + "react-qr-code": "^2.0.15", + "react-router-dom": "^6.25.1" + }, + "devDependencies": { + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.34.1", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "postcss": "^8.4.41", + "tailwindcss": "^3.4.10", + "tailwindscss": "^0.3.0", + "vite": "^5.2.0" + } +} diff --git a/staff/agustin-birman/project/app/postcss.config.js b/staff/agustin-birman/project/app/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/staff/agustin-birman/project/app/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/staff/agustin-birman/project/app/src/App.jsx b/staff/agustin-birman/project/app/src/App.jsx new file mode 100644 index 000000000..8739bb2f0 --- /dev/null +++ b/staff/agustin-birman/project/app/src/App.jsx @@ -0,0 +1,41 @@ +import { Routes, Route, useNavigate, Navigate } from 'react-router-dom' +import 'bootstrap/dist/css/bootstrap.min.css' +import 'bootstrap-icons/font/bootstrap-icons.css'; +import Alert from './components/library/Alert' +import Register from './views/Register' +import Login from './views/Login' +import Home from './views/Home' +import logic from './logic' +import './global.css' +import { Context } from './useContext' +import { useState } from 'react' + +function App() { + const [message, setMessage] = useState(null) + + const navigate = useNavigate() + + const handleGoToLogin = () => navigate('/login') + + const handleGoToHome = () => navigate('/') + + const handleGoToRegister = () => navigate('/register') + const handleAlertAccepted = () => setMessage(null) + + const handleMessage = message => setMessage(message) + + + return + + : } /> + + : } /> + + : } /> + + {message && } + + +} + +export default App \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/core/Button/index.css b/staff/agustin-birman/project/app/src/components/core/Button/index.css new file mode 100644 index 000000000..e8d06b153 --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/core/Button/index.css @@ -0,0 +1,13 @@ + +.Button { + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + cursor: pointer; + margin: 5px; + font-size: 1.2rem; + font-weight: 500; + cursor: pointer; +} + +.Header .Button { + padding: 0.3rem; +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/core/Button/index.jsx b/staff/agustin-birman/project/app/src/components/core/Button/index.jsx new file mode 100644 index 000000000..b2363d2c0 --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/core/Button/index.jsx @@ -0,0 +1,17 @@ +import './index.css'; +import { Button as BootstrapButton } from 'react-bootstrap'; + +function Button({ type, className, onClick, children, disabled }) { + return ( + + {children} + + ); +} + +export default Button; diff --git a/staff/agustin-birman/project/app/src/components/core/Field/index.css b/staff/agustin-birman/project/app/src/components/core/Field/index.css new file mode 100644 index 000000000..725a691af --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/core/Field/index.css @@ -0,0 +1,6 @@ +.Field { + display: flex; + flex-direction: column; + margin: .25rem 0; +} + diff --git a/staff/agustin-birman/project/app/src/components/core/Field/index.jsx b/staff/agustin-birman/project/app/src/components/core/Field/index.jsx new file mode 100644 index 000000000..9de2f674e --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/core/Field/index.jsx @@ -0,0 +1,13 @@ +import Label from '../Label' +import Input from '../Input' + +import './index.css' + +function Field({ id, type, placeholder, children, onChange }) { + return
+ + +
+} + +export default Field \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/core/FieldWithTextArea/index.css b/staff/agustin-birman/project/app/src/components/core/FieldWithTextArea/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/staff/agustin-birman/project/app/src/components/core/FieldWithTextArea/index.jsx b/staff/agustin-birman/project/app/src/components/core/FieldWithTextArea/index.jsx new file mode 100644 index 000000000..3df256a89 --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/core/FieldWithTextArea/index.jsx @@ -0,0 +1,13 @@ +import Label from '../Label' +import TextArea from '../TextArea' + +import '../Field/index.css' + +function FieldWithTextArea({ id, type, placeholder, children }) { + return
+ + +} + +export default TextArea \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/core/Time/index.css b/staff/agustin-birman/project/app/src/components/core/Time/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/staff/agustin-birman/project/app/src/components/core/Time/index.jsx b/staff/agustin-birman/project/app/src/components/core/Time/index.jsx new file mode 100644 index 000000000..3eaa0c917 --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/core/Time/index.jsx @@ -0,0 +1,8 @@ +function Time({ children: time }) { + const formattedTime = new Date(time).toLocaleString('de-DE', { timeZone: 'Europe/Berlin' }); + + + return +} + +export default Time \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/library/ActivitiesList/index.css b/staff/agustin-birman/project/app/src/components/library/ActivitiesList/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/staff/agustin-birman/project/app/src/components/library/ActivitiesList/index.jsx b/staff/agustin-birman/project/app/src/components/library/ActivitiesList/index.jsx new file mode 100644 index 000000000..e69de29bb diff --git a/staff/agustin-birman/project/app/src/components/library/Alert/index.jsx b/staff/agustin-birman/project/app/src/components/library/Alert/index.jsx new file mode 100644 index 000000000..bbd73b312 --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/Alert/index.jsx @@ -0,0 +1,6 @@ +export default ({ message, onAccept, level = 'warn' }) =>
+
+

{message}

+ +
+
\ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/library/ConfirmDelete/index.css b/staff/agustin-birman/project/app/src/components/library/ConfirmDelete/index.css new file mode 100644 index 000000000..49e145287 --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/ConfirmDelete/index.css @@ -0,0 +1,25 @@ +.ConfirmDelete { + border: 1px solid black; + border-radius: 20px; + top: 30%; + left: 10%; + width: 80%; + height: 30%; + position: fixed; + display: flex; /* Utiliza flexbox para organizar los elementos en el contenedor */ + flex-direction: column; /* Organiza los elementos en una columna */ + align-items: center; /* Centra los elementos horizontalmente */ + justify-content: center; /* Centra los elementos verticalmente */ + gap: 16px; /* Espacio entre los elementos (texto y botones) */ + text-align: center; /* Centra el texto dentro del contenedor */ + z-index: 10; + background-color: rgb(175, 222, 238); +} + +.ConfirmDeleteButtonContainer { + display: flex; /* Usa flexbox para organizar los botones horizontalmente */ + gap: 8px; /* Espacio entre los botones */ +} +.ConfirmDeleteButton { + margin: 0 8px; /* Espacio horizontal entre los botones */ +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/library/ConfirmDelete/index.jsx b/staff/agustin-birman/project/app/src/components/library/ConfirmDelete/index.jsx new file mode 100644 index 000000000..3bd09ac7e --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/ConfirmDelete/index.jsx @@ -0,0 +1,15 @@ +import Text from '../../core/Text' +import Button from '../../core/Button' +import './index.css' + +function ConfirmDelete({ message, onCancel, onAccept }) { + return
+ {message} +
+ + +
+
+} + +export default ConfirmDelete \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/library/Exercises/index.css b/staff/agustin-birman/project/app/src/components/library/Exercises/index.css new file mode 100644 index 000000000..fd7c20c12 --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/Exercises/index.css @@ -0,0 +1,12 @@ +.ListExerciseTitle{ + margin: 10px; +} + +.ListExercisesTable{ + height: auto; + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + border-radius: 10px; + margin: 10% 0; + width: 95%; + overflow-x: auto; +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/library/Exercises/index.jsx b/staff/agustin-birman/project/app/src/components/library/Exercises/index.jsx new file mode 100644 index 000000000..9b4f21b9a --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/Exercises/index.jsx @@ -0,0 +1,93 @@ +import { useEffect, useState } from "react" +import View from "../View" +import Heading from "../../core/Heading" +import ConfirmDelete from "../ConfirmDelete" +import logic from "../../../logic" +import './index.css' + +function Exercises({ activityId, onEditButton, updateExercises }) { + const [exercises, setExercises] = useState([]) + const [confirmDeleteExercise, setConfirmDeleteExercise] = useState(false) + + useEffect(() => + loadExercises() + , [updateExercises]) + + const loadExercises = () => { + try { + logic.getExercises(activityId) + .then(data => { + const { exercises } = data + setExercises(exercises) + }) + .catch(error => { + console.error(error) + + alert(error.message) //TODO hacer un alert mejor + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const handleDeletedExericise = (exerciseId) => { + try { + logic.deleteExercise(exerciseId) + .then(() => loadExercises()) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const toggleDeleteExercise = () => setConfirmDeleteExercise(prevState => !prevState) + + return + List Exercises + + + + + + + + + + + {exercises.map(exercise => + + + + + + + )} + +
SentenceEditDelete
{exercise.index + 1}{exercise.word !== undefined ? exercise.word : exercise.sentence} + onEditButton(exercise)} + title="Edit Exercise" + > + + {confirmDeleteExercise && handleDeletedExericise(exercise.id)} onCancel={toggleDeleteExercise}>} + +
+
+} + +export default Exercises \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/library/FormWithFeedback/index.css b/staff/agustin-birman/project/app/src/components/library/FormWithFeedback/index.css new file mode 100644 index 000000000..0e7015959 --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/FormWithFeedback/index.css @@ -0,0 +1,11 @@ +.FormWithFeedback { + padding: 1rem; +} + +.FormWithFeedback .Feedback { + color: tomato; +} + +.FormWithFeedback .Feedback.success { + color: greenyellow; +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/library/FormWithFeedback/index.jsx b/staff/agustin-birman/project/app/src/components/library/FormWithFeedback/index.jsx new file mode 100644 index 000000000..00007b416 --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/FormWithFeedback/index.jsx @@ -0,0 +1,12 @@ +import Form from '../../core/Form' +import Text from '../../core/Text' + +import './index.css' + +function FormWithFeedback({ className, onSubmit, children, message, level }) { + return
{children} + {message && {message}} +
+} + +export default FormWithFeedback \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/library/Header/index.css b/staff/agustin-birman/project/app/src/components/library/Header/index.css new file mode 100644 index 000000000..73c02b9a3 --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/Header/index.css @@ -0,0 +1,14 @@ +.Header { + display: flex; + gap: 1rem; + width: 100%; + justify-content: right; + align-items: center; + position: fixed; + top: 0; + padding: .5rem; + box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.2); + z-index: 1000; + background-color: aliceblue; +} + diff --git a/staff/agustin-birman/project/app/src/components/library/Header/index.jsx b/staff/agustin-birman/project/app/src/components/library/Header/index.jsx new file mode 100644 index 000000000..81f4a192a --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/Header/index.jsx @@ -0,0 +1,7 @@ +import './index.css' + +function Header({ children }) { + return
{children}
+} + +export default Header \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/library/MenuItem/index.css b/staff/agustin-birman/project/app/src/components/library/MenuItem/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/staff/agustin-birman/project/app/src/components/library/MenuItem/index.jsx b/staff/agustin-birman/project/app/src/components/library/MenuItem/index.jsx new file mode 100644 index 000000000..6c7454ca1 --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/MenuItem/index.jsx @@ -0,0 +1,7 @@ +import Button from '../../core/Button' + +function MenuItem({ onClick, children }) { + return +} + +export default MenuItem \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/library/RadiusButtonUser/index.jsx b/staff/agustin-birman/project/app/src/components/library/RadiusButtonUser/index.jsx new file mode 100644 index 000000000..9e45018e8 --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/RadiusButtonUser/index.jsx @@ -0,0 +1,33 @@ +import Input from '../../core/Input' +import Label from '../../core/Label' + +function RadiusButtonUser({ selectedValue, onChange }) { + return ( +
+
+ + +
+
+ + +
+
+ ) +} + +export default RadiusButtonUser \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/library/View/index.css b/staff/agustin-birman/project/app/src/components/library/View/index.css new file mode 100644 index 000000000..968ff685b --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/View/index.css @@ -0,0 +1,5 @@ +.View { + display: flex; + flex-direction: column; + align-items: center; +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/components/library/View/index.jsx b/staff/agustin-birman/project/app/src/components/library/View/index.jsx new file mode 100644 index 000000000..4a4b0cfee --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/View/index.jsx @@ -0,0 +1,7 @@ +import './index.css' + +function View({ tag: Tag = 'div', children, className }) { + return {children} +} + +export default View diff --git a/staff/agustin-birman/project/app/src/components/library/ViewActivityStudent/index.jsx b/staff/agustin-birman/project/app/src/components/library/ViewActivityStudent/index.jsx new file mode 100644 index 000000000..b90e2ba7b --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/ViewActivityStudent/index.jsx @@ -0,0 +1,160 @@ +import { useEffect, useState } from 'react' +import logic from '../../../logic' +import View from '../../../components/library/View' +import Heading from '../../../components/core/Heading' +import Text from '../../../components/core/Text' +import Button from '../../../components/core/Button' +import ConfirmDelete from '../../../components/library/ConfirmDelete' +import { Link, useNavigate, useParams } from 'react-router-dom' +import useContext from '../../../useContext' + +function ViewActivityStudent() { + const { activityId } = useParams() + const [activity, setActivity] = useState(null) + const [loading, setLoading] = useState(true) + const [confirmDeleteAnswers, setConfirmDeleteAnswers] = useState(false) + const [completedActivity, setCompletedActivity] = useState(false) + const [updateView, setUpdateView] = useState(0) + const [exercisesCount, setExercisesCount] = useState('') + const [exerciseType, SetExerciseType] = useState('') + const [userId, setUserId] = useState('') + + const { alert } = useContext() + + useEffect(() => { + loadActivity() + checkCompleteActivity() + getExercisesCount() + getUserId() + getExerciseType() + }, [updateView]) + + const loadActivity = () => { + try { + logic.getActivity(activityId) + .then(activity => setActivity(activity)) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } finally { + setLoading(false) + } + } + + const getUserId = () => { + try { + const userId = logic.getUserId() + setUserId(userId) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const getExerciseType = () => { + try { + logic.getExerciseType(activityId) + .then(type => SetExerciseType(type)) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const getExercisesCount = () => { + try { + logic.getExercisesCount(activityId) + .then(count => setExercisesCount(count)) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const checkCompleteActivity = () => { + try { + logic.checkCompleteActivity(activityId) + .then(result => setCompletedActivity(result)) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const handleDeleteAnswers = () => { + try { + logic.deleteAnswers(activityId) + .then(() => { + toggleDeleteAnswers() + setUpdateView(new Date()) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + if (loading) + return Loading... + + if (!activity) { + return Activity not found + } + + const toggleDeleteAnswers = () => setConfirmDeleteAnswers(prevState => !prevState) + + return + Activity + Title + {activity.title} + + Description + {activity.description} + + How many exercises in this activity? + {exercisesCount} + + Status + {completedActivity === true + ? Complete + : Incomplete} + + + {completedActivity === true && <> + + + } + + + {confirmDeleteAnswers && } + +} + +export default ViewActivityStudent + diff --git a/staff/agustin-birman/project/app/src/components/library/ViewActivityTeacher/index.css b/staff/agustin-birman/project/app/src/components/library/ViewActivityTeacher/index.css new file mode 100644 index 000000000..c6ff0fb3a --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/ViewActivityTeacher/index.css @@ -0,0 +1,46 @@ +.ActivityView { + display: flex; + flex-direction: column; + /* Otras propiedades si es necesario */ +} +.IconsContainer { + display: flex; + justify-content: flex-end; /* Alinea los íconos a la derecha */ + margin-bottom: 1rem; /* Espacio inferior para que no quede pegado al contenido */ + margin-right: 5%; +} +.IconsContainer i { + font-size: 1.5rem; + margin-left: 1rem; /* Espacio entre íconos */ +} +.ActivityTeacherTable { + border-collapse: separate; /* Asegúrate de que los bordes no se fusionen */ + border-spacing: 0 10px; /* Espacio entre filas */ + width: 90%; + + margin: 20px; +} + +.ActivityTeacherTable td { + border: 1px solid #ddd; /* Borde alrededor de las celdas */ + padding: 10px; /* Espacio dentro de cada celda */ + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + border-radius: 20px; +} + +.ActivityTeacherTable th { + border: 1px solid #ddd; /* Borde alrededor de las celdas */ + padding: 10px; /* Espacio dentro de cada celda */ + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + border-radius: 20px; +} + +.ActivityTeacherTable tr { + border-bottom: 2px solid #f4f4f4; /* Espacio entre filas */ +} + +.ActivityTeacherTitle { + padding: 3px; /* Reduce el relleno interno de la celda */ + border-radius: 20px; /* Estilo opcional para bordes redondeados */ +} + diff --git a/staff/agustin-birman/project/app/src/components/library/ViewActivityTeacher/index.jsx b/staff/agustin-birman/project/app/src/components/library/ViewActivityTeacher/index.jsx new file mode 100644 index 000000000..a880eb02b --- /dev/null +++ b/staff/agustin-birman/project/app/src/components/library/ViewActivityTeacher/index.jsx @@ -0,0 +1,179 @@ +import { useEffect, useState } from 'react' +import logic from '../../../logic' +import './index.css' +import View from '../../../components/library/View' +import Heading from '../../../components/core/Heading' +import Text from '../../../components/core/Text' +import Button from '../../../components/core/Button' +import Input from '../../../components/core/Input' +import ConfirmDelete from '../../../components/library/ConfirmDelete' +import { Link, useNavigate, useParams } from 'react-router-dom' +import useContext from '../../../useContext' + +function ViewActivityTeacher() { + const { activityId } = useParams() + const [activity, setActivity] = useState(null) + const [loading, setLoading] = useState(true) + const [editView, setEditView] = useState(false) + const [title, setTitle] = useState('') + const [description, setDescription] = useState('') + const [confirmDeleteActivity, setConfirmDeleteActivity] = useState(false) + const [exercisesCount, setExercisesCount] = useState('') + + const navigate = useNavigate() + const { alert } = useContext() + + useEffect(() => { + loadActivity() + getExercisesCount() + }, []) + + useEffect(() => { + if (activity) { + setTitle(activity.title) + setDescription(activity.description) + } + }, [activity]) + + const loadActivity = () => { + try { + logic.getActivity(activityId) + .then(activity => setActivity(activity)) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } finally { + setLoading(false) + } + } + + const getExercisesCount = () => { + try { + logic.getExercisesCount(activityId) + .then(count => setExercisesCount(count)) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + + const handleDeleteActivity = () => { + try { + logic.deleteActivity(activityId) + .then(() => navigate('/activities/list')) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const handleEditButton = () => { + setEditView(editView => !editView) + } + + const handleSaveChanges = () => { + try { + logic.editActivity(activityId, title, description) + .then(() => { + loadActivity() + setEditView(false) + }) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + if (loading) + return Loading... + + if (!activity) { + return Activity not found + } + + const toggleDeleteActivity = () => setConfirmDeleteActivity(prevState => !prevState) + + return
+ + Activity + + + + + + + + + {editView === false && + + + } + +
+ {editView + ? setTitle(e.target.value)} /> + : {activity.title}} +
+ {editView + ? setDescription(e.target.value)} /> + : {activity.description}} +
+ + N° exercises + {exercisesCount} + +
+
+ + {editView === false + ? <> +
+ + +
+ + + + + + : + + + + } + {confirmDeleteActivity && } +
+} + +export default ViewActivityTeacher + diff --git a/staff/agustin-birman/project/app/src/global.css b/staff/agustin-birman/project/app/src/global.css new file mode 100644 index 000000000..e47b20e84 --- /dev/null +++ b/staff/agustin-birman/project/app/src/global.css @@ -0,0 +1,67 @@ +@import url('https://fonts.googleapis.com/css2?family=Workbench&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Catamaran:wght@100..900&display=swap'); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --first-color: black; + --second-color: rgb(112, 233, 233); + } + + * { + font-family: catamaran; + color: var(--first-color); + } + + + h1{ + @apply text-5xl + } + h2{ + @apply text-3xl + } + h3{ + @apply text-2xl + } + h4{ + @apply text-xl + } + p{ + @apply text-lg + } + .View { + width: 100%; + height: auto; + } + table { + width: 100%; + border-collapse: collapse; /* Combina bordes adyacentes para un aspecto más limpio */ + } + + th, td { + border: 1px solid #ddd; /* Bordes ligeros para celdas */ + padding: 8px; /* Espaciado dentro de las celdas */ + text-align: center; /* Alineación del texto a la izquierda */ + } + + th { + background-color: #f4f4f4; /* Fondo gris claro para encabezados */ + font-weight: bold; /* Negrita para encabezados */ + } + + tr:nth-child(even) { + background-color: #f9f9f9; /* Fondo alternado para filas */ + } + + tr:hover { + background-color: #e0e0e0; /* Fondo ligeramente más oscuro al pasar el ratón */ + } + +} + + + diff --git a/staff/agustin-birman/project/app/src/logic/addStudent.js b/staff/agustin-birman/project/app/src/logic/addStudent.js new file mode 100644 index 000000000..7b86e4f48 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/addStudent.js @@ -0,0 +1,32 @@ +import errors, { SystemError } from "com/errors" +import validate from "../../../com/validate" + +const addStudent = studentId => { + validate.id(studentId, 'studentId') + + return fetch(`${import.meta.env.VITE_API_URL}/users`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${localStorage.token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ studentId }) + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 201) + return + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default addStudent \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/checkCompleteActivity.js b/staff/agustin-birman/project/app/src/logic/checkCompleteActivity.js new file mode 100644 index 000000000..8e0d34d12 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/checkCompleteActivity.js @@ -0,0 +1,32 @@ +import validate from 'com/validate' +import errors, { SystemError } from '../../../com/errors' + +const checkCompleteActivity = activityId => { + validate.id(activityId, 'activityId') + + return fetch(`${import.meta.env.VITE_API_URL}/activity/${activityId}/result`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server not found') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server not found') }) + .then(result => result) + } + + return response.json() + .catch(() => { throw new SystemError('server not found') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default checkCompleteActivity \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/createActivity.js b/staff/agustin-birman/project/app/src/logic/createActivity.js new file mode 100644 index 000000000..cd90c647c --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/createActivity.js @@ -0,0 +1,39 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' + +const createActivity = (title, description) => { + validate.text(title, 'title', 50) + validate.text(description, 'description', 200) + + return fetch(`${import.meta.env.VITE_API_URL}/activity`, { + method: 'POST', + headers: { + Authorization: `Bearer ${localStorage.token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ title, description }) + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 201) + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(data => { + const { activityId } = data + return activityId + }) + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default createActivity + diff --git a/staff/agustin-birman/project/app/src/logic/createCompleteSentenceExercise.js b/staff/agustin-birman/project/app/src/logic/createCompleteSentenceExercise.js new file mode 100644 index 000000000..9f12a8b12 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/createCompleteSentenceExercise.js @@ -0,0 +1,33 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' + +const createCompleteSentenceExercise = (activityId, sentence) => { + validate.id(activityId, 'activityId') + validate.textCompleteSentence(sentence, 'sentence', 200) + + return fetch(`${import.meta.env.VITE_API_URL}/exercise/complete-sentence`, { + method: 'POST', + headers: { + Authorization: `Bearer ${localStorage.token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ activityId, sentence }) + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 201) + return + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default createCompleteSentenceExercise \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/createOrderSentence.js b/staff/agustin-birman/project/app/src/logic/createOrderSentence.js new file mode 100644 index 000000000..a5c0b5502 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/createOrderSentence.js @@ -0,0 +1,34 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' + +const createOrderSentence = (activityId, sentence, translate) => { + validate.id(activityId, 'activityId') + validate.text(sentence, 'sentence', 200) + validate.text(translate, 'translate', 200) + + return fetch(`${import.meta.env.VITE_API_URL}/exercise/order-sentence`, { + method: 'POST', + headers: { + Authorization: `Bearer ${localStorage.token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ activityId, sentence, translate }) + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 201) + return + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default createOrderSentence \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/createVocabulary.js b/staff/agustin-birman/project/app/src/logic/createVocabulary.js new file mode 100644 index 000000000..1fc9878fe --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/createVocabulary.js @@ -0,0 +1,33 @@ +import errors, { SystemError } from "../../../com/errors" +import validate from "../../../com/validate" + +const createVocabulary = (activityId, word, answers) => { + validate.id(activityId, 'activityId') + validate.text(word, 'word', 60) + + return fetch(`${import.meta.env.VITE_API_URL}/exercise/vocabulary`, { + method: 'POST', + headers: { + Authorization: `Bearer ${localStorage.token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ activityId, word, answers }) + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 201) + return + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default createVocabulary \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/deleteActivity.js b/staff/agustin-birman/project/app/src/logic/deleteActivity.js new file mode 100644 index 000000000..06d42fa62 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/deleteActivity.js @@ -0,0 +1,30 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' + +const deleteActivity = activityId => { + validate.id(activityId, 'activityId') + + return fetch(`${import.meta.env.VITE_API_URL}/activity/${activityId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 204) + return + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default deleteActivity \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/deleteAnswers.js b/staff/agustin-birman/project/app/src/logic/deleteAnswers.js new file mode 100644 index 000000000..0d8b776d8 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/deleteAnswers.js @@ -0,0 +1,30 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' + +const deleteAnswers = activityId => { + validate.id(activityId, 'activityId') + + return fetch(`${import.meta.env.VITE_API_URL}/answer/${activityId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 204) + return + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default deleteAnswers \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/deleteExercise.js b/staff/agustin-birman/project/app/src/logic/deleteExercise.js new file mode 100644 index 000000000..ce43bb1b6 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/deleteExercise.js @@ -0,0 +1,30 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' + +const deleteExercise = exerciseId => { + validate.id(exerciseId, 'exerciseId') + + return fetch(`${import.meta.env.VITE_API_URL}/exercise/${exerciseId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 204) + return + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default deleteExercise \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/editActivity.js b/staff/agustin-birman/project/app/src/logic/editActivity.js new file mode 100644 index 000000000..806b93d8e --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/editActivity.js @@ -0,0 +1,38 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' + +const editActivity = (activityId, title, description) => { + validate.id(activityId, 'activityId') + if (title !== '') { + validate.text(title, 'title', 50) + } + if (description !== '') { + validate.text(description, 'description', 200) + } + + return fetch(`${import.meta.env.VITE_API_URL}/activity/${activityId}`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${localStorage.token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ activityId, title, description }) + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) + return + + return response.json() + .catch(() => { throw new SystemError(error.message) }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default editActivity \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/editExercise.js b/staff/agustin-birman/project/app/src/logic/editExercise.js new file mode 100644 index 000000000..002ad7ec8 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/editExercise.js @@ -0,0 +1,32 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' + +const editExercise = (exerciseId, updateData) => { + validate.id(exerciseId, 'exerciseId') + + return fetch(`${import.meta.env.VITE_API_URL}/exercise/${exerciseId}`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${localStorage.token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ exerciseId, ...updateData }) + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) + return + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default editExercise \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getActivities.js b/staff/agustin-birman/project/app/src/logic/getActivities.js new file mode 100644 index 000000000..b682ae376 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getActivities.js @@ -0,0 +1,29 @@ +import errors, { SystemError } from '../../../com/errors' + +const getActivities = () => { + return fetch(`${import.meta.env.VITE_API_URL}/activity`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(activities => activities) + } + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default getActivities \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getActivity.js b/staff/agustin-birman/project/app/src/logic/getActivity.js new file mode 100644 index 000000000..0f685dcaa --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getActivity.js @@ -0,0 +1,32 @@ +import validate from 'com/validate' +import errors, { SystemError } from '../../../com/errors' + +const getActivity = activityId => { + validate.id(activityId, 'activityId') + + return fetch(`${import.meta.env.VITE_API_URL}/activity/${activityId}`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server not found') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server not found') }) + .then(activity => activity) + } + + return response.json() + .catch(() => { throw new SystemError('server not found') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default getActivity \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getAnswers.js b/staff/agustin-birman/project/app/src/logic/getAnswers.js new file mode 100644 index 000000000..2a306b8f9 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getAnswers.js @@ -0,0 +1,31 @@ +import errors, { SystemError } from "../../../com/errors" +import validate from "../../../com/validate" + +const getAnswers = exerciseId => { + validate.id(exerciseId, 'exerciseId') + + return fetch(`${import.meta.env.VITE_API_URL}/answer/${exerciseId}`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(answers => answers) + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default getAnswers \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getExerciseType.js b/staff/agustin-birman/project/app/src/logic/getExerciseType.js new file mode 100644 index 000000000..60f769720 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getExerciseType.js @@ -0,0 +1,31 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' + +const getExerciseType = activityId => { + validate.id(activityId, 'activityId') + + return fetch(`${import.meta.env.VITE_API_URL}/activity/${activityId}/type`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(type => type) + } + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default getExerciseType \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getExercises.js b/staff/agustin-birman/project/app/src/logic/getExercises.js new file mode 100644 index 000000000..48987d29b --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getExercises.js @@ -0,0 +1,31 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' + +const getExercises = activityId => { + validate.id(activityId, 'activityId') + + return fetch(`${import.meta.env.VITE_API_URL}/exercise/${activityId}`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(({ exercises, count }) => ({ exercises, count })) + } + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default getExercises \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getExercisesCount.js b/staff/agustin-birman/project/app/src/logic/getExercisesCount.js new file mode 100644 index 000000000..255b810cd --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getExercisesCount.js @@ -0,0 +1,31 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' + +const getExercisesCount = activityId => { + validate.id(activityId, 'activityId') + + return fetch(`${import.meta.env.VITE_API_URL}/exercise/${activityId}/count`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(count => count) + } + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default getExercisesCount \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getStudents.js b/staff/agustin-birman/project/app/src/logic/getStudents.js new file mode 100644 index 000000000..90b90dd72 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getStudents.js @@ -0,0 +1,33 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' +import extractPayloadFromJWT from '../utils/extractPayloadFromJWT' + +const getStudents = () => { + + const { sub: userId } = extractPayloadFromJWT(localStorage.token) + + return fetch(`${import.meta.env.VITE_API_URL}/users/${userId}/students`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(students => students) + } + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default getStudents \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getTeachers.js b/staff/agustin-birman/project/app/src/logic/getTeachers.js new file mode 100644 index 000000000..5a9e5e032 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getTeachers.js @@ -0,0 +1,33 @@ +import errors, { SystemError } from 'com/errors' +import validate from 'com/validate' +import extractPayloadFromJWT from '../utils/extractPayloadFromJWT' + +const getTeachers = () => { + + const { sub: userId } = extractPayloadFromJWT(localStorage.token) + + return fetch(`${import.meta.env.VITE_API_URL}/users/${userId}/teachers`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(students => students) + } + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default getTeachers \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getTeachersActivities.js b/staff/agustin-birman/project/app/src/logic/getTeachersActivities.js new file mode 100644 index 000000000..a86991a77 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getTeachersActivities.js @@ -0,0 +1,33 @@ +import errors, { SystemError } from '../../../com/errors' +import extractPayloadFromJWT from '../utils/extractPayloadFromJWT' + +const getTeachersActivities = () => { + + const { sub: userId } = extractPayloadFromJWT(localStorage.token) + + return fetch(`${import.meta.env.VITE_API_URL}/activity/student/${userId}`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(activities => activities) + } + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default getTeachersActivities \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getUserId.js b/staff/agustin-birman/project/app/src/logic/getUserId.js new file mode 100644 index 000000000..e31b46b87 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getUserId.js @@ -0,0 +1,9 @@ +import extractPayloadFromJWT from '../utils/extractPayloadFromJWT' + +const getUserId = () => { + const { sub: userId } = extractPayloadFromJWT(localStorage.token) + + return userId +} + +export default getUserId \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getUserInfo.js b/staff/agustin-birman/project/app/src/logic/getUserInfo.js new file mode 100644 index 000000000..eeca53de4 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getUserInfo.js @@ -0,0 +1,33 @@ +import errors from 'com/errors' + +import validate from 'com/validate' + +const getUserInfo = studentId => { + validate.id(studentId, 'studentId') + + return fetch(`${import.meta.env.VITE_API_URL}/users/${studentId}/info`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(user => user) + } + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default getUserInfo \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getUserName.js b/staff/agustin-birman/project/app/src/logic/getUserName.js new file mode 100644 index 000000000..fe702c217 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getUserName.js @@ -0,0 +1,34 @@ +import errors from 'com/errors' + +import extractPayloadFromJWT from '../utils/extractPayloadFromJWT' + +const getUserName = () => { + + const { sub: userrId } = extractPayloadFromJWT(localStorage.token) + + return fetch(`${import.meta.env.VITE_API_URL}/users/${userrId}`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(name => name) + } + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default getUserName \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getUserRole.js b/staff/agustin-birman/project/app/src/logic/getUserRole.js new file mode 100644 index 000000000..f08cab983 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getUserRole.js @@ -0,0 +1,9 @@ +import extractPayloadFromJWT from '../utils/extractPayloadFromJWT' + +const getUserRole = () => { + const { role } = extractPayloadFromJWT(localStorage.token) + + return role +} + +export default getUserRole \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/getUserStats.js b/staff/agustin-birman/project/app/src/logic/getUserStats.js new file mode 100644 index 000000000..a11f422cb --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/getUserStats.js @@ -0,0 +1,33 @@ +import errors from 'com/errors' +import validate from 'com/validate' + +const getUserStats = (targetUserId) => { + validate.id(targetUserId, 'targetUserId') + + return fetch(`${import.meta.env.VITE_API_URL}/users/student/${targetUserId}/stats`, { + headers: { + Authorization: `Bearer ${localStorage.token}` + } + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server error') }) + // .then(({ countActivities, countExercises, countCorrectExercises }) => ({ countActivities, countExercises, countCorrectExercises })) + .then(stats => stats) + } + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default getUserStats \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/index.js b/staff/agustin-birman/project/app/src/logic/index.js new file mode 100644 index 000000000..a14e752b7 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/index.js @@ -0,0 +1,76 @@ +import registerUser from './registerUser' +import loginUser from './loginUser' +import getUserName from './getUserName' +import isUserLoggedIn from './isUserLoggedIn' +import logoutUser from './logoutUser' +import getUserInfo from './getUserInfo' +import addStudent from './addStudent' +import removeStudent from './removeStudent' +import getStudents from './getStudents' +import getUserRole from './getUserRole' +import getTeachers from './getTeachers' +import removeTeacher from './removeTeacher' +import getUserId from './getUserId' +import getUserStats from './getUserStats' + +import createActivity from './createActivity' +import getActivities from './getActivities' +import getActivity from './getActivity' +import deleteActivity from './deleteActivity' +import editActivity from './editActivity' +import getTeachersActivities from './getTeachersActivities' +import checkCompleteActivity from './checkCompleteActivity' + +import createCompleteSentenceExercise from './createCompleteSentenceExercise' +import createOrderSentence from './createOrderSentence' +import createVocabulary from './createVocabulary' +import getExercises from './getExercises' +import deleteExercise from './deleteExercise' +import editExercise from './editExercise' +import getExercisesCount from './getExercisesCount' +import getExerciseType from './getExerciseType' + +import submitAnswer from './submitAnswer' +import getAnswers from './getAnswers' +import deleteAnswers from './deleteAnswers' + + +const logic = { + registerUser, + loginUser, + getUserName, + isUserLoggedIn, + logoutUser, + getUserInfo, + addStudent, + removeStudent, + getStudents, + getUserRole, + getTeachers, + removeTeacher, + getUserId, + getUserStats, + + createActivity, + getActivities, + getActivity, + deleteActivity, + editActivity, + getTeachersActivities, + checkCompleteActivity, + + createCompleteSentenceExercise, + createOrderSentence, + createVocabulary, + getExercises, + deleteExercise, + editExercise, + getExercisesCount, + getExerciseType, + + submitAnswer, + getAnswers, + deleteAnswers +} + +export default logic \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/isUserLoggedIn.js b/staff/agustin-birman/project/app/src/logic/isUserLoggedIn.js new file mode 100644 index 000000000..13c34e2e5 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/isUserLoggedIn.js @@ -0,0 +1,3 @@ +const isUserLoggedIn = () => !!localStorage.token + +export default isUserLoggedIn \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/loginUser.js b/staff/agustin-birman/project/app/src/logic/loginUser.js new file mode 100644 index 000000000..5a071bf1e --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/loginUser.js @@ -0,0 +1,37 @@ +import validate from '../../../com/validate' +import errors, { SystemError } from 'com/errors' + +const loginUser = (username, password) => { + validate.username(username) + validate.password(password) + + return fetch(`${import.meta.env.VITE_API_URL}/users/auth`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ username, password }) + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 200) { + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(token => { localStorage.token = token }) + } + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + + }) + +} + +export default loginUser \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/logoutUser.js b/staff/agustin-birman/project/app/src/logic/logoutUser.js new file mode 100644 index 000000000..c4d6eb861 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/logoutUser.js @@ -0,0 +1 @@ +export default () => delete localStorage.token diff --git a/staff/agustin-birman/project/app/src/logic/registerUser.js b/staff/agustin-birman/project/app/src/logic/registerUser.js new file mode 100644 index 000000000..875128fd9 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/registerUser.js @@ -0,0 +1,38 @@ +import validate from '../../../com/validate' +import errors, { SystemError } from 'com/errors' + +const registerUser = (name, surname, email, username, password, passwordRepeat, userType) => { + validate.name(name) + validate.name(surname, 'surname') + validate.email(email) + validate.username(username) + validate.password(password) + validate.passowrdsMatch(password, passwordRepeat) + validate.userType(userType) + + return fetch(`${import.meta.env.VITE_API_URL}/users`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ name, surname, email, username, password, passwordRepeat, userType }) + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 201) return + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + + }) + +} + +export default registerUser \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/removeStudent.js b/staff/agustin-birman/project/app/src/logic/removeStudent.js new file mode 100644 index 000000000..e3061f291 --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/removeStudent.js @@ -0,0 +1,30 @@ +import errors, { SystemError } from "com/errors" +import validate from "../../../com/validate" + +const removeStudent = studentId => { + validate.id(studentId, 'studentId') + + return fetch(`${import.meta.env.VITE_API_URL}/users/teacher/${studentId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${localStorage.token}`, + }, + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 204) + return + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default removeStudent \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/removeTeacher.js b/staff/agustin-birman/project/app/src/logic/removeTeacher.js new file mode 100644 index 000000000..d3c7af8ae --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/removeTeacher.js @@ -0,0 +1,30 @@ +import errors, { SystemError } from "com/errors" +import validate from "../../../com/validate" + +const removeTeacher = teacherId => { + validate.id(teacherId, 'teacherId') + + return fetch(`${import.meta.env.VITE_API_URL}/users/student/${teacherId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${localStorage.token}`, + }, + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 204) + return + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} + +export default removeTeacher \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/logic/submitAnswer.js b/staff/agustin-birman/project/app/src/logic/submitAnswer.js new file mode 100644 index 000000000..8a8e233dc --- /dev/null +++ b/staff/agustin-birman/project/app/src/logic/submitAnswer.js @@ -0,0 +1,33 @@ +import errors, { SystemError } from "com/errors" +import validate from "com/validate" + +const submitAnswer = (activityId, exerciseId, answer) => { + validate.id(activityId, 'activityId') + validate.id(exerciseId, 'exerciseId') + validate.text(answer, 'answer') + + return fetch(`${import.meta.env.VITE_API_URL}/answer`, { + method: 'POST', + headers: { + Authorization: `Bearer ${localStorage.token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ activityId, exerciseId, answer }) + }) + .catch(() => { throw new SystemError('server error') }) + .then(response => { + if (response.status === 201) + return + + return response.json() + .catch(() => { throw new SystemError('server error') }) + .then(body => { + const { error, message } = body + + const constructor = errors[error] + + throw new constructor(message) + }) + }) +} +export default submitAnswer diff --git a/staff/agustin-birman/project/app/src/main.jsx b/staff/agustin-birman/project/app/src/main.jsx new file mode 100644 index 000000000..b286afd19 --- /dev/null +++ b/staff/agustin-birman/project/app/src/main.jsx @@ -0,0 +1,14 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App.jsx' +import { BrowserRouter as Router } from 'react-router-dom' + + + +ReactDOM.createRoot(document.getElementById('root')).render( + // + + + + // +) diff --git a/staff/agustin-birman/project/app/src/postcss.config.js b/staff/agustin-birman/project/app/src/postcss.config.js new file mode 100644 index 000000000..2e7af2b7f --- /dev/null +++ b/staff/agustin-birman/project/app/src/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/staff/agustin-birman/project/app/src/useContext.js b/staff/agustin-birman/project/app/src/useContext.js new file mode 100644 index 000000000..96c8e900b --- /dev/null +++ b/staff/agustin-birman/project/app/src/useContext.js @@ -0,0 +1,9 @@ +import { createContext, useContext } from 'react' + +export const Context = createContext() + +export default () => { + const context = useContext(Context) + + return context +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/utils/extractPayloadFromJWT.js b/staff/agustin-birman/project/app/src/utils/extractPayloadFromJWT.js new file mode 100644 index 000000000..2e62bd404 --- /dev/null +++ b/staff/agustin-birman/project/app/src/utils/extractPayloadFromJWT.js @@ -0,0 +1,29 @@ +import errors from 'com/errors' + +const { ContentError, MatchError } = errors + +const JWT_REGEX = /^[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+\.[A-Za-z0-9-_]+$/ + +function extractPayloadFromJWT(token) { + if (!JWT_REGEX.test(token)) throw new ContentError('invalid jwt') + + const [, payload64] = localStorage.token.split('.') + + const payloadJSON = atob(payload64) + + const payload = JSON.parse(payloadJSON) + + const { exp } = payload + + const nowSeconds = Date.now() / 1000 + + if (nowSeconds >= exp) { + delete localStorage.token + throw new MatchError('Token expired') + + } + + return payload +} + +export default extractPayloadFromJWT \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/Home.css b/staff/agustin-birman/project/app/src/views/Home.css new file mode 100644 index 000000000..e69de29bb diff --git a/staff/agustin-birman/project/app/src/views/Home.jsx b/staff/agustin-birman/project/app/src/views/Home.jsx new file mode 100644 index 000000000..2a2407353 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/Home.jsx @@ -0,0 +1,95 @@ +import { useState, useEffect } from 'react' + +import { Routes, Route, useNavigate, Link, useParams } from 'react-router-dom' + +import View from '../components/library/View' +import Header from '../components/library/Header' +import Button from '../components/core/Button' +import Heading from '../components/core/Heading' +import logic from '../logic' +import SelectActivity from './components/SelectActivity' +import MenuItem from './components/MenuItems' +import CompleteSentenceInfo from './components/CompleteSentenceInfo' +import CompleteSentence from './components/CompleteSentence' +import CreateActivity from './components/CreateActivity' +import ListActivities from './components/ListActivities' +import ViewActivity from './components/ViewActivity' +import ListExercises from './components/ListExercises' +import DoActivity from './components/DoActivity' +import ShowExerciseResults from './components/ShowExerciseResults' +import ShareQR from './components/ShareQR' +import AddStudent from './components/AddStudent' +import ListStudents from './components/ListStudents' +import ListTeachersActivities from './components/ListTeachersActivities' +import ListTeachers from './components/ListTeachers' +import OrderSentence from './components/OrderSentence' +import DoActivityOrderSentence from './components/DoActivityOrderSentence' +import ViewStudentStats from './components/ViewStudentStats' +import Vocabulary from './components/Vocabulary' + +function Home() { + const [name, setName] = useState('') + const navigate = useNavigate() + + const handleLogout = () => { + logic.logoutUser() + + navigate('/login') + } + + useEffect(() => { + try { + logic.getUserName() + .then(name => setName(name)) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + }, []) + + return +
+ {name} + +
+ + + + } /> + } /> + + } /> + + } /> + } /> + } /> + } /> + + } /> + } /> + + } /> + } /> + + } /> + } /> + } /> + + } /> + } /> + } /> + } /> + } /> + + +
+} + + +export default Home \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/Login.css b/staff/agustin-birman/project/app/src/views/Login.css new file mode 100644 index 000000000..742705acb --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/Login.css @@ -0,0 +1,36 @@ +/* Base Styles */ +@import 'tailwindcss/base'; +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; + + +main { + margin-top: 18% ; + width: 100%; + height: 100%; +} + +.LoginForm { + margin-top: 20%; + padding: 1rem; + margin-bottom: 10px; + width: auto; + height: auto; +} + +.LoginForm .Input { + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid black; + padding-left: 0.8rem; + margin: 1rem; +} + +.LoginForm .Button { + font-size: 1.125rem; + width: 1.25 rem; + padding: 0.5rem; + border: 1px solid black; + border-radius: 10px; +} diff --git a/staff/agustin-birman/project/app/src/views/Login.jsx b/staff/agustin-birman/project/app/src/views/Login.jsx new file mode 100644 index 000000000..fce08156a --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/Login.jsx @@ -0,0 +1,62 @@ +import { useState } from 'react' +import logic from '../logic' + +import './Login.css' + +import Field from '../components/core/Field' +import SubmitButton from '../components/core/SubmitButton' +import Link from '../components/core/Link' + +import FormWithFeedback from '../components/library/FormWithFeedback' +import View from '../components/library/View' +import Heading from '../components/core/Heading' + +function Login({ onUserLoggedIn, onRegisterLinkClick }) { + const [message, setMessage] = useState('') + + const handleLoginSubmit = event => { + event.preventDefault() + + const form = event.target + + const username = form.username.value + const password = form.password.value + + try { + logic.loginUser(username, password) + .then(() => onUserLoggedIn()) + .catch(error => { + console.log(error) + + setMessage(error.message) + }) + } catch (error) { + console.error(error) + + setMessage(error.message) + } + } + + const handleRegisterClick = event => { + event.preventDefault() + + onRegisterLinkClick() + } + + return + {/* Hello! */} +

Hello

+ + + Username + + Password + + Login + + + Register +
+} + +export default Login \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/Register.css b/staff/agustin-birman/project/app/src/views/Register.css new file mode 100644 index 000000000..d27ee1d89 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/Register.css @@ -0,0 +1,28 @@ +@import 'tailwindcss/base'; +@import 'tailwindcss/components'; +@import 'tailwindcss/utilities'; + +.RegisterForm { + padding: 1rem; + width: auto; + height: auto; + +} + +.RegisterForm .Input { + background-color: transparent; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid black; + padding-left: 10px; + margin: 15px; +} + +.RegisterForm .Button { + font-size: 1.25rem; + width: 80px; + padding: 0.3rem; + border: 1px solid black; + margin-top: 5%; +} diff --git a/staff/agustin-birman/project/app/src/views/Register.jsx b/staff/agustin-birman/project/app/src/views/Register.jsx new file mode 100644 index 000000000..ccada7fb7 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/Register.jsx @@ -0,0 +1,86 @@ +import logic from '../logic' + +import './Register.css' + +import View from '../components/library/View' +import Field from '../components/core/Field' +import SubmitButton from '../components/core/SubmitButton' +import FormWithFeedback from '../components/library/FormWithFeedback' +import Link from '../components/core/Link' +import { useState } from 'react' +import RadiusButtonUser from '../components/library/RadiusButtonUser' +import Heading from '../components/core/Heading' + +function Register({ onUserRegistered, onLoginLinkClick }) { + const [message, setMessage] = useState('') + const [userType, setUserType] = useState('') + + + const handleRegisterSubmit = event => { + event.preventDefault() + + const form = event.target + + const name = form.name.value + const surname = form.surname.value + const email = form.email.value + const username = form.username.value + const password = form.password.value + const passwordRepeat = form.passwordRepeat.value + + + try { + logic.registerUser(name, surname, email, username, password, passwordRepeat, userType) + .then(() => onUserRegistered()) + .catch(error => { + console.log(error) + + setMessage(error.message) + }) + } catch (error) { + console.error(error) + + setMessage(error.message) + } + } + + const handleLoginClick = event => { + event.preventDefault() + + onLoginLinkClick() + } + + const handleUserTypeChange = (event) => { + setUserType(event.target.value); + } + + return + Register + + + Name + + Surname + + Email + + Username + + Password + + Password Repeat + + User + + Register + + Login + +} + + +export default Register + + + + diff --git a/staff/agustin-birman/project/app/src/views/components/AddStudent/index.css b/staff/agustin-birman/project/app/src/views/components/AddStudent/index.css new file mode 100644 index 000000000..d528a86e9 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/AddStudent/index.css @@ -0,0 +1,17 @@ + + +.AddStudentText{ +margin: 5%; +} + +.AddStudentButton{ + margin: 8%; + } + +.AddStudentContainer{ + margin-top: 30%; + width: 90%; + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + border-radius: 20px; + padding: 15px; +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/AddStudent/index.jsx b/staff/agustin-birman/project/app/src/views/components/AddStudent/index.jsx new file mode 100644 index 000000000..a97c7d7c5 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/AddStudent/index.jsx @@ -0,0 +1,65 @@ +import logic from '../../../logic' +import View from '../../../components/library/View' +import Heading from '../../../components/core/Heading' +import Text from '../../../components/core/Text' +import { useNavigate, useParams } from 'react-router-dom' +import { useEffect, useState } from 'react' +import Button from '../../../components/core/Button' +import './index.css' + + +function AddStudent() { + const [userInfo, setUserInfo] = useState('') + const { userInfoId } = useParams() + const navigate = useNavigate() + + + + useEffect(() => { + getUserInfo() + }, []) + + const getUserInfo = () => { + try { + logic.getUserInfo(userInfoId) + .then(user => setUserInfo(user)) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const handleAddStudent = () => { + try { + logic.addStudent(userInfoId) + .then(() => navigate('/users/students')) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + return <> + + Student's Information + Name: {userInfo.name} + Surname: {userInfo.surname} + Username: {userInfo.username} + + + +} + +export default AddStudent \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/CompleteSentence/index.css b/staff/agustin-birman/project/app/src/views/components/CompleteSentence/index.css new file mode 100644 index 000000000..b7aafd7d6 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/CompleteSentence/index.css @@ -0,0 +1,34 @@ + .Input { + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid black; + padding-left: 0.8rem; + margin: 1rem; +} + +.CreateCompleteSentence .Button { + font-size: 1.125rem; + width: 1.25 rem; + padding: 0.5rem; + border: 1px solid black; + border-radius: 10px; + margin: 0 5px; +} + +.Exercise{ + width: 30%; + height: auto; +} + +.CreateCompleteSentence{ + text-align: center; +} + +.divButton { + gap: 10px; +} + +.CreateCompleteSentence .Text{ + margin: 10px; +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/CompleteSentence/index.jsx b/staff/agustin-birman/project/app/src/views/components/CompleteSentence/index.jsx new file mode 100644 index 000000000..6cb38563b --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/CompleteSentence/index.jsx @@ -0,0 +1,135 @@ +import './index.css' + +import { useEffect, useState } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' + +import Button from '../../../components/core/Button' +import Field from '../../../components/core/Field' +import Heading from '../../../components/core/Heading' +import Text from '../../../components/core/Text' +import FormWithFeedback from '../../../components/library/FormWithFeedback' +import Input from '../../../components/core/Input' + +import logic from '../../../logic' +import View from '../../../components/library/View' +import Exercises from '../../../components/library/Exercises' + +const ANSWER_REGEX = /\(([^)]+)\)/ + +function CreateCompleteSentence() { + const [message, setMessage] = useState('') + const [answerInput, setAnswerInput] = useState('Not answer found') + const [editView, setEditView] = useState(false) + const [sentence, setSentence] = useState('') + const [selectedExercise, setSelectedExercise] = useState(null) + const [updateExercises, setUpdateExercises] = useState(0) + + const { activityId } = useParams() + + useEffect(() => { + if (sentence) { + const removeAnswer = sentence.match(ANSWER_REGEX) + + if (removeAnswer && removeAnswer[1]) + setAnswerInput(removeAnswer[1]) + else + setAnswerInput('Not answer found') + } + } + , [editView]) + + const handleEditedExercise = () => { + try { + logic.editExercise(selectedExercise.id, { sentence }) + .then(() => { + setUpdateExercises(prev => prev + 1) + setEditView(false) + }) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const handleNextExercise = () => { + const form = document.querySelector('form') + + const sentence = form.sentence.value + + try { + logic.createCompleteSentenceExercise(activityId, sentence) + .then(() => { + form.reset() + setAnswerInput('Answer not found') + setUpdateExercises(prev => prev + 1) + }) + .catch(error => { + console.error(error) + + setMessage(error.message) + }) + } catch (error) { + console.error(error) + + setMessage(error.message) + } + } + + const handleAnswerInput = event => { + const sentence = event.target.value + + const removeAnswer = sentence.match(ANSWER_REGEX) + + if (removeAnswer && removeAnswer[1]) + setAnswerInput(removeAnswer[1]) + else + setAnswerInput('Not answer found') + } + + const handleEditButton = (exercise) => { + setSelectedExercise(exercise) + setSentence(exercise.sentence) + setEditView(editView => !editView) + } + + return + + + -Complete the sentence + + {editView === false + ? New Sentence + : <>{selectedExercise.index + 1}Sentence + { setSentence(e.target.value); handleAnswerInput(e) }} />} + -Answer + {answerInput} + + Please make sure the answer is in parentheses + + {editView === false + ? + + + + + + : + + + + } + + + {editView === false && <> + } + + +} +export default CreateCompleteSentence + diff --git a/staff/agustin-birman/project/app/src/views/components/CompleteSentenceInfo/index.css b/staff/agustin-birman/project/app/src/views/components/CompleteSentenceInfo/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/staff/agustin-birman/project/app/src/views/components/CompleteSentenceInfo/index.jsx b/staff/agustin-birman/project/app/src/views/components/CompleteSentenceInfo/index.jsx new file mode 100644 index 000000000..95217e983 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/CompleteSentenceInfo/index.jsx @@ -0,0 +1,61 @@ +import './index.css' + +import Text from '../../../components/core/Text' +import Button from '../../../components/core/Button' +import View from '../../../components/library/View' +import { Link } from 'react-router-dom' +import Heading from '../../../components/core/Heading' + +const instructions = ` +**Instructions:** + +The objective of this exercise is for students to practice sentence structure in German by filling in the missing words. Follow these steps to create and verify the exercise: + +1. **Write the Sentence:** + - Write a sentence in German. + - When you reach the word you want the students to complete, leave a blank space. + +2. **Leave a Blank Space:** + - Use the 'Leave Blank' button (or a similar tool) to create the blank space where students will write the correct word. + +3. **Write the Correct Word:** + - After finishing the sentence, write the correct word in a separate place. This will help you to have the correct answer for verification. + +**Example of the Process:** + +Let's say you want the students to complete the following sentence: 'Pepito hat es gegessen.' + +1. **Write the Sentence:** + - Write: 'Pepito ____ es gegessen.' + - Use the 'Leave Blank' button to create a space where students will write 'hat.' + +2. **Write the Correct Word:** + - In a separate place, write the correct word: 'hat.' + +**Verifying the Exercise:** + +1. **Completing the Exercise:** + - Students will read the sentence with the blank space and write the word they think is missing. + - For example, they will complete: 'Pepito **hat** es gegessen.' + +2. **Verifying the Answers:** + - Use the 'Check' button to verify if the word entered by the student is correct. + - If the word is correct, the system should indicate that the answer is correct. + - If the word is incorrect, the system should indicate that the answer is incorrect and students can try again. +`; + + + + + +//TODO podria hacer un form con 4 border radius para mejorar la experiencia de usuario +function CompleteSentence() { + + return + Complete the sentence + {instructions} + + +} + +export default CompleteSentence \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/CreateActivity/index.css b/staff/agustin-birman/project/app/src/views/components/CreateActivity/index.css new file mode 100644 index 000000000..9cbc44af5 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/CreateActivity/index.css @@ -0,0 +1,25 @@ +.CreateActivity { + margin-top: 20%; + padding: 1rem; + margin-bottom: 10px; + width: auto; + height: auto; +} + +.CreateActivity .Input { + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid black; + padding-left: 0.8rem; + margin: 1.2rem; +} + +.CreateActivity .Button { + font-size: 1.125rem; + width: 1.25 rem; + padding: 0.5rem; + border: 1px solid black; + border-radius: 10px; + margin: 0 5px; +} diff --git a/staff/agustin-birman/project/app/src/views/components/CreateActivity/index.jsx b/staff/agustin-birman/project/app/src/views/components/CreateActivity/index.jsx new file mode 100644 index 000000000..175d1a07a --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/CreateActivity/index.jsx @@ -0,0 +1,64 @@ +import { useState } from 'react' + +import './index.css' + +import logic from '../../../logic' + +import Heading from '../../../components/core/Heading' +import Button from '../../../components/core/Button' +import Field from '../../../components/core/Field' +import FieldWithTextArea from '../../../components/core/FieldWithTextArea' +import SubmitButton from '../../../components/core/SubmitButton' +import FormWithFeedback from '../../../components/library/FormWithFeedback' +import { Link, useNavigate, useParams } from 'react-router-dom' + +function CreateActivity({ }) { + const [message, setMessage] = useState('') + + const { exerciseType } = useParams() + + const navigate = useNavigate() + + + const handleCreatedActivity = event => { + event.preventDefault() + + const form = event.target + + const title = form.title.value + const description = form.description.value + + try { + logic.createActivity(title, description) + .then(activityId => { + navigate(`/activities/${activityId}/${exerciseType}`) + }) + .catch(error => { + console.error(error) + + setMessage(error.message) + }) + } catch (error) { + console.error(error) + + setMessage(error.message) + } + } + + + return + + Complete the Sentence + + Title + + Description + +
+ + +
+
+} + +export default CreateActivity \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/DoActivity/index.css b/staff/agustin-birman/project/app/src/views/components/DoActivity/index.css new file mode 100644 index 000000000..790271e9c --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/DoActivity/index.css @@ -0,0 +1,25 @@ +.DoActivityCompleteSentence{ + margin-top: 40%; +} +.DoExerciseText{ + font-size: 1.2rem; +} + +.DoActivityTitle{ + margin-bottom: 15px; +} +.ExerciseContainer{ + display:inline-flex; + align-items: center; + white-space: nowrap; + margin-bottom: 15px; +} + +.ExerciseInput{ + width: 6rem; + font-size: 1.2rem; +} + +.DoActivityButton{ + margin-bottom: 15px; +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/DoActivity/index.jsx b/staff/agustin-birman/project/app/src/views/components/DoActivity/index.jsx new file mode 100644 index 000000000..05ebbbd1c --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/DoActivity/index.jsx @@ -0,0 +1,122 @@ +import { useNavigate, useParams } from 'react-router-dom' +import View from '../../../components/library/View' +import { useEffect, useState } from 'react' +import Heading from '../../../components/core/Heading' +import Text from '../../../components/core/Text' +import logic from '../../../logic' +import Input from '../../../components/core/Input' +import Button from '../../../components/core/Button' +import './index.css' + +let SENTENCE_REGEX = /^(.*?)\s*\(.*?\)\s*(.*?)$/ + +function DoActivity() { + const [message, setMessage] = useState('') + const [exercises, setExercises] = useState([]) + const [answer, setAnswer] = useState('') + const [currentPage, setCurrentPage] = useState(1) + const [totalPages, setTotalPages] = useState(2) + const pageSize = 1 + const { activityId } = useParams() + const navigate = useNavigate() + + useEffect(() => { + loadExercises() + + }, []) + + useEffect(() => { + if (currentPage > totalPages) { + navigate(`/activities/${activityId}/results`) + } + }, [currentPage]) + + useEffect(() => { + setAnswer('') + setMessage('') + }, [currentPage]) + + const loadExercises = () => { + try { + logic.getExercises(activityId) + .then(data => { + const { exercises, count } = data + setCurrentPage(count + 1) + setTotalPages(Math.ceil(exercises.length / pageSize)) + setExercises(exercises) + }) + .catch(error => { + console.error(error) + + setMessage(error.message) + }) + } catch (error) { + console.error(error) + + setMessage(error.message) + } + } + + const handleSubmittedAnswer = (exerciseId) => { + try { + logic.submitAnswer(activityId, exerciseId, answer) + .then(() => { + handleChangePage(currentPage + 1) + + if (currentPage >= totalPages) + navigate(`/activities/${activityId}/results`) + }) + .catch(error => { + console.error(error) + + setMessage(error.message) + }) + } catch (error) { + console.error(error) + + setMessage(error.message) + } + } + + const handleChangePage = newPage => { + setCurrentPage(newPage) + } + + const currentExercises = exercises.slice((currentPage - 1) * pageSize, currentPage * pageSize) + + return ( + {currentExercises.map(exercise => { + let beforeParentheses = '' + let afterParentheses = '' + + let matches = exercise.sentence.match(SENTENCE_REGEX); + + if (matches) { + beforeParentheses = matches[1].trim() + afterParentheses = matches[2].trim() + } + + return ( + {exercise.index + 1} Exercise +
+ {beforeParentheses} + { setAnswer(e.target.value) }} /> + {afterParentheses} +
+ + { + currentPage > totalPages + ? + : + + } + {message} + + Page {currentPage} of {Math.ceil(exercises.length / pageSize)} +
) + })} +
+ ) +} + +export default DoActivity \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/DoActivityOrderSentence/index.css b/staff/agustin-birman/project/app/src/views/components/DoActivityOrderSentence/index.css new file mode 100644 index 000000000..513c1baf3 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/DoActivityOrderSentence/index.css @@ -0,0 +1,65 @@ +.DoActOrderSentenceTitle{ + margin-top: 40%; +} + +.DoActOrderSentence{ + margin: 5%; +} + +.SelectedWords, .MixedWords { + display: flex; + flex-wrap: wrap; + gap: 10px; + width: 100%; /* O un tamaño específico si prefieres */ + box-sizing: border-box; +} + +.SelectedWords { + margin-bottom: 20px; /* Espacio entre las listas */ +} + +.DoActOrderSentenceButton{ + margin: 5%; +} + +.btnOrderSentence { + padding: 10px 20px; + border: none; + border-radius: 4px; + font-size: 16px; + cursor: pointer; + transition: transform 0.3s, opacity 0.3s; + /* Evita que los botones se reduzcan más allá de su tamaño mínimo */ +} + +/* Animaciones para mover botones */ +.move-to-selected { + animation: moveToSelected 0.3s forwards; +} + +.move-to-mixed { + animation: moveToMixed 0.3s forwards; +} + +/* Ajustar animación */ +@keyframes moveToSelected { + from { + transform: translateY(10px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes moveToMixed { + from { + transform: translateY(-10px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} diff --git a/staff/agustin-birman/project/app/src/views/components/DoActivityOrderSentence/index.jsx b/staff/agustin-birman/project/app/src/views/components/DoActivityOrderSentence/index.jsx new file mode 100644 index 000000000..affad9439 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/DoActivityOrderSentence/index.jsx @@ -0,0 +1,173 @@ +import { useNavigate, useParams } from 'react-router-dom' +import View from '../../../components/library/View' +import { useEffect, useState } from 'react' +import Heading from '../../../components/core/Heading' +import Text from '../../../components/core/Text' +import logic from '../../../logic' +import './index.css' +import Button from '../../../components/core/Button' + +let SENTENCE_REGEX = /^(.*?)\s*\(.*?\)\s*(.*?)$/ + +function DoActivityOrderSentence() { + const [message, setMessage] = useState('') + const [selectedWords, setSelectedWords] = useState([]) + const [mixedWords, setMixedWords] = useState([]) + const [exercises, setExercises] = useState([]) + const [currentPage, setCurrentPage] = useState(1) + const [totalPages, setTotalPages] = useState(2) + const pageSize = 1 + const { activityId } = useParams() + const navigate = useNavigate() + + useEffect(() => { + loadExercises() + + }, []) + + useEffect(() => { + if (currentPage > totalPages) { + navigate(`/activities/${activityId}/results`) + } + }, [currentPage, totalPages, navigate, activityId]) + + useEffect(() => { + setMessage('') + }, [currentPage]) + + const loadExercises = () => { + try { + logic.getExercises(activityId) + .then(data => { + const { exercises, count } = data + setExercises(exercises) + setCurrentPage(count + 1) + setTotalPages(Math.ceil(exercises.length / pageSize)) + if (exercises.length > 0) { + const separatedWords = exercises[0].sentence.split(' ') + setMixedWords(shuffleArray(separatedWords)) + } + }) + .catch(error => { + console.error(error) + + setMessage(error.message) + }) + } catch (error) { + console.error(error) + + setMessage(error.message) + } + } + + const handleSubmittedAnswer = (exerciseId) => { + if (mixedWords.length > 0) { + setMessage('Please complete the order of the words before submitting your answer.') + return + } + const answer = selectedWords.join(' ') + try { + logic.submitAnswer(activityId, exerciseId, answer) + .then(() => { + handleChangePage(currentPage + 1) + + if (currentPage >= totalPages) + navigate(`/activities/${activityId}/results`) + }) + .catch(error => { + console.error(error) + + setMessage(error.message) + }) + } catch (error) { + console.error(error) + + setMessage(error.message) + } + } + + const handleWordClick = (word) => { + setMixedWords(prevMixedWords => { + // Guardar palabra en una lista temporal para animar + const wordToMove = prevMixedWords.find(w => w === word); + const newMixedWords = prevMixedWords.filter(w => w !== word); + setSelectedWords(prevSelectedWords => [...prevSelectedWords, wordToMove]); + return newMixedWords; + }); + }; + + const handleSelectedWordClick = (word) => { + setSelectedWords(prevSelectedWords => { + // Guardar palabra en una lista temporal para animar + const wordToMove = prevSelectedWords.find(w => w === word); + const newSelectedWords = prevSelectedWords.filter(w => w !== wordToMove); + setMixedWords(prevMixedWords => [...prevMixedWords, wordToMove]); + return newSelectedWords; + }); + }; + + const handleChangePage = newPage => { + if (newPage >= 1 && newPage <= totalPages) { + setCurrentPage(newPage) + const currentExercise = exercises[newPage - 1] + if (currentExercise) { + const separatedWords = currentExercise.sentence.split(' ') + setSelectedWords([]) + setMixedWords(shuffleArray(separatedWords)) + } + } + } + + const currentExercises = exercises.slice((currentPage - 1) * pageSize, currentPage * pageSize) + + function shuffleArray(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]] + } + return array + } + + return ( + {currentExercises.map((exercise, index) => ( + + {exercise.index + 1} Exercise + {exercise.translate} +
+
+ {selectedWords.map((word, selectedWordIndex) => ( + + ))} +
+
+ {mixedWords.map((word, wordIndex) => ( + + ))} +
+
+ { + currentPage > totalPages + ? + : + } + {message} + + Page {currentPage} of {Math.ceil(exercises.length / pageSize)} + + ))} + + ) +} + +export default DoActivityOrderSentence \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ListActivities/index.css b/staff/agustin-birman/project/app/src/views/components/ListActivities/index.css new file mode 100644 index 000000000..08924da8c --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ListActivities/index.css @@ -0,0 +1,34 @@ +@tailwind components; + +@layer components{ + + .ActitvitiesListTitle{ + margin-top: 5%; + } + .ActivitiesList { + width: 95%; + overflow-x: auto; + } + .ActivitiesList { + height: auto; + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + border-radius: 10px; + margin: 10% 0; + width: 95%; + overflow-x: auto; + } + + .Activity{ + padding: 3px; + display:flex; + justify-content: space-around; + border-bottom: solid 1px black; + } + + + .Activity:last-child{ + border-bottom: none + } + + +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ListActivities/index.jsx b/staff/agustin-birman/project/app/src/views/components/ListActivities/index.jsx new file mode 100644 index 000000000..783d93879 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ListActivities/index.jsx @@ -0,0 +1,65 @@ +import { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' +import './index.css' + +import Heading from '../../../components/core/Heading' +import Text from '../../../components/core/Text' +import Button from '../../../components/core/Button' +import View from '../../../components/library/View' +import logic from '../../../logic' + +function ListActivities() { + const [activities, setActivities] = useState([]) + + useEffect(() => + loadActivities() + , []) + + const loadActivities = () => { + try { + logic.getActivities() + .then(activities => setActivities(activities)) + .catch(error => { + console.error(error) + + alert(error.message) //TODO hacer un alert mejor + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + + + return + Activities List +
+ + + + + + + + + {activities.map(activity => + + + + + )} + +
TitleInfo
{activity.title} + +
+
+ +
+} + +export default ListActivities \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ListExercises/index.css b/staff/agustin-birman/project/app/src/views/components/ListExercises/index.css new file mode 100644 index 000000000..61bbe8c56 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ListExercises/index.css @@ -0,0 +1,12 @@ +.ExerciseList { + width: 95%; + height: auto; + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + border-radius: 10px; + margin: 10% 0; + overflow-x: auto; +} + +.ExerciseListTitle{ + margin-top: 10%; +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ListExercises/index.jsx b/staff/agustin-birman/project/app/src/views/components/ListExercises/index.jsx new file mode 100644 index 000000000..7175008ab --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ListExercises/index.jsx @@ -0,0 +1,82 @@ +import { useEffect, useState } from 'react' +import { Link, useParams } from 'react-router-dom' +import './index.css' + +import Heading from '../../../components/core/Heading' +import Button from '../../../components/core/Button' +import View from '../../../components/library/View' +import logic from '../../../logic' + + +function ListExercises() { + const { activityId } = useParams() + const [exercises, setExercises] = useState([]) + const [exerciseType, SetExerciseType] = useState('') + + useEffect(() => { + loadExercises() + getExerciseType() + }, []) + + const loadExercises = () => { + try { + logic.getExercises(activityId) + .then(data => { + const { exercises } = data + setExercises(exercises) + }) + .catch(error => { + console.error(error) + + alert(error.message) //TODO hacer un alert mejor + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const getExerciseType = () => { + try { + logic.getExerciseType(activityId) + .then(type => SetExerciseType(type)) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + return + Exercises List +
+ + + + + + + + + {exercises.map(exercise => + + + + + + )} + +
Sentence
{exercise.index + 1}{exercise.word !== undefined ? exercise.word : exercise.sentence}
+
+ + +
+} + +export default ListExercises \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ListStudents/index.css b/staff/agustin-birman/project/app/src/views/components/ListStudents/index.css new file mode 100644 index 000000000..f0b90950f --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ListStudents/index.css @@ -0,0 +1,13 @@ +.StudentsListTitle{ + margin-top: 20px; +} + +.StudentsList{ + width: 95%; + height: auto; + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + border-radius: 10px; + margin: 10% 0; + overflow-x: auto; +} + diff --git a/staff/agustin-birman/project/app/src/views/components/ListStudents/index.jsx b/staff/agustin-birman/project/app/src/views/components/ListStudents/index.jsx new file mode 100644 index 000000000..3ce4c7442 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ListStudents/index.jsx @@ -0,0 +1,99 @@ +import { useEffect, useState } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' +import './index.css' + +import Button from '../../../components/core/Button' +import Heading from '../../../components/core/Heading' +import View from '../../../components/library/View' +import logic from '../../../logic' +import ConfirmDelete from '../../../components/library/ConfirmDelete' + + +function ListStudents() { + const [students, setStudents] = useState([]) + const [confirmDeleteStudent, setConfirmDeleteStudent] = useState(false) + const navigate = useNavigate() + + + useEffect(() => + loadStudents() + , []) + + const loadStudents = () => { + try { + logic.getStudents() + .then(students => { + setStudents(students) + }) + .catch(error => { + console.error(error) + + alert(error.message) //TODO hacer un alert mejor + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const handleDeleteStudent = (studentId) => { + try { + logic.removeStudent(studentId) + .then(() => loadStudents()) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + + const handleUserStats = (studentId) => navigate(`/users/student/${studentId}/info`) + const toggleDeleteStudent = () => setConfirmDeleteStudent(prevState => !prevState) + return + Students List + + + + + + + + + + + + {students.map(student => + + + + + + + )} + +
NameSurnameInfoDelete
{student.name}{student.surname} handleUserStats(student.id)} + style={{ cursor: 'pointer', color: '#007bff' }} + > + + {confirmDeleteStudent && handleDeleteStudent(student.id)} onCancel={toggleDeleteStudent}>} +
+
+ +
+} + +export default ListStudents \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ListTeachers/index.css b/staff/agustin-birman/project/app/src/views/components/ListTeachers/index.css new file mode 100644 index 000000000..2d34c3fa5 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ListTeachers/index.css @@ -0,0 +1,29 @@ +@tailwind components; + +@layer components{ + + .TeachersListTitle{ + margin-top: 5%; + } + + .TeachersList { + width: 90%; + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + border-radius: 10px; + margin: 10% 0; + } + + .Teacher{ + padding: 3px; + display:flex; + justify-content: space-around; + border-bottom: solid 1px black; + } + + + .Teacher:last-child{ + border-bottom: none + } + + +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ListTeachers/index.jsx b/staff/agustin-birman/project/app/src/views/components/ListTeachers/index.jsx new file mode 100644 index 000000000..2f5363513 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ListTeachers/index.jsx @@ -0,0 +1,91 @@ +import { useEffect, useState } from 'react' +import { Link, useParams } from 'react-router-dom' + +import './index.css' +import Button from '../../../components/core/Button' +import Heading from '../../../components/core/Heading' +import View from '../../../components/library/View' +import logic from '../../../logic' +import ConfirmDelete from '../../../components/library/ConfirmDelete' + + +function ListTeachers() { + const [teachers, setTeachers] = useState([]) + const [confirmDeleteTeacher, setConfirmDeleteTeacher] = useState(false) + + useEffect(() => + loadTeachers() + , []) + + const loadTeachers = () => { + try { + logic.getTeachers() + .then(teachers => { + setTeachers(teachers) + }) + .catch(error => { + console.error(error) + + alert(error.message) //TODO hacer un alert mejor + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const handleDeleteTeacher = (teacherId) => { + try { + logic.removeTeacher(teacherId) + .then(() => loadTeachers()) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const toggleDeleteTeacher = () => setConfirmDeleteTeacher(prevState => !prevState) + return + Teachers List + + + + + + + + + + + {teachers.map(teacher => + + + + + + + )} + +
NameSurnameDelete
{teacher.name}{teacher.surname} + + {confirmDeleteTeacher && handleDeleteTeacher(teacher.id)} onCancel={toggleDeleteTeacher}>} +
+
+ + +
+} + +export default ListTeachers \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ListTeachersActivities/index.css b/staff/agustin-birman/project/app/src/views/components/ListTeachersActivities/index.css new file mode 100644 index 000000000..eb3ee58af --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ListTeachersActivities/index.css @@ -0,0 +1,15 @@ +.ActivitiesListTitle{ + margin-top: 5%; +} + +.ActivitiesTeachersList { + width: 95%; + height: auto; + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + border-radius: 10px; + margin: 10% 0; + overflow-x: auto; +} + + + diff --git a/staff/agustin-birman/project/app/src/views/components/ListTeachersActivities/index.jsx b/staff/agustin-birman/project/app/src/views/components/ListTeachersActivities/index.jsx new file mode 100644 index 000000000..81b153ca8 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ListTeachersActivities/index.jsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from 'react' +import { Link } from 'react-router-dom' +import './index.css' +import Heading from '../../../components/core/Heading' +import Text from '../../../components/core/Text' +import Button from '../../../components/core/Button' +import Input from '../../../components/core/Input' +import View from '../../../components/library/View' +import logic from '../../../logic' + +function ListTeachersActivities() { + const [activities, setActivities] = useState([]) + const [searchTerm, setSearchTerm] = useState('') + + useEffect(() => + loadActivities() + , []) + + const loadActivities = () => { + try { + logic.getTeachersActivities() + .then(activities => setActivities(activities)) + .catch(error => { + console.error(error) + + alert(error.message) //TODO hacer un alert mejor + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const filteredActivities = activities.filter(activity => + activity.title.toLowerCase().includes(searchTerm.toLowerCase()) || + activity.teacherUsername.toLowerCase().includes(searchTerm.toLowerCase()) + ) + + return + Activities List +
+ setSearchTerm(e.target.value)} + /> +
+
+ + + + + + + + + + {filteredActivities.length > 0 ? ( + filteredActivities.map(activity => ( + + + + + + )) + ) : ( + + + + )} + +
TitleTeacherInfo
{activity.title}{activity.teacherUsername} + +
No activities found
+
+ +
+} + +export default ListTeachersActivities \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/MenuItems/index.css b/staff/agustin-birman/project/app/src/views/components/MenuItems/index.css new file mode 100644 index 000000000..ba3cd3cc1 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/MenuItems/index.css @@ -0,0 +1,7 @@ +.MenuItem{ + width: 90%; + height: 15%; + padding: 10px; + border-radius: 20px; + margin-bottom: 1rem; +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/MenuItems/index.jsx b/staff/agustin-birman/project/app/src/views/components/MenuItems/index.jsx new file mode 100644 index 000000000..4809797d3 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/MenuItems/index.jsx @@ -0,0 +1,48 @@ +import Button from '../../../components/core/Button' +import View from '../../../components/library/View' +import { Link } from 'react-router-dom' +import './index.css' +import logic from '../../../logic' +import extractPayloadFromJWT from '../../../utils/extractPayloadFromJWT' +import { useEffect, useState } from 'react' + +function MenuItem() { + const [userRole, setUserRole] = useState('') + + const { sub: userId } = extractPayloadFromJWT(localStorage.token) // TODO logic para getUserId + + useEffect(() => { + getRole() + }, []) + + const getRole = () => { + try { + const role = logic.getUserRole() + setUserRole(role) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + return + + + {userRole === 'student' && <> + + + + + asd + } + + {userRole === 'teacher' && <> + + + + } + + +} + +export default MenuItem \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/OrderSentence/index.css b/staff/agustin-birman/project/app/src/views/components/OrderSentence/index.css new file mode 100644 index 000000000..e69de29bb diff --git a/staff/agustin-birman/project/app/src/views/components/OrderSentence/index.jsx b/staff/agustin-birman/project/app/src/views/components/OrderSentence/index.jsx new file mode 100644 index 000000000..271e9f2d0 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/OrderSentence/index.jsx @@ -0,0 +1,120 @@ +import './index.css' + +import { useEffect, useState } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' + +import Button from '../../../components/core/Button' +import Field from '../../../components/core/Field' +import Heading from '../../../components/core/Heading' +import Text from '../../../components/core/Text' +import FormWithFeedback from '../../../components/library/FormWithFeedback' +import Input from '../../../components/core/Input' + +import logic from '../../../logic' +import View from '../../../components/library/View' +import Exercises from '../../../components/library/Exercises' + +const ANSWER_REGEX = /\(([^)]+)\)/ + +function OrderSentence() { + const [message, setMessage] = useState('') + const [answerInput, setAnswerInput] = useState('Not answer found') + const [editView, setEditView] = useState(false) + const [sentence, setSentence] = useState('') + const [translate, setTranslate] = useState('') + const [selectedExercise, setSelectedExercise] = useState(null) + const [updateExercises, setUpdateExercises] = useState(0) + + const { activityId } = useParams() + + + + const handleEditedExercise = () => { + try { + logic.editExercise(selectedExercise.id, { sentence, translate }) + .then(() => { + setUpdateExercises(prev => prev + 1) + setEditView(false) + }) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const handleNextExercise = () => { + const form = document.querySelector('form') + + const sentence = form.sentence.value + const translate = form.translate.value + + try { + logic.createOrderSentence(activityId, sentence, translate) + .then(() => { + form.reset() + setUpdateExercises(prev => prev + 1) + }) + .catch(error => { + console.error(error) + + setMessage(error.message) + }) + } catch (error) { + console.error(error) + + setMessage(error.message) + } + } + + const handleEditButton = (exercise) => { + setSelectedExercise(exercise) + setSentence(exercise.sentence) + setTranslate(exercise.translate) + setEditView(editView => !editView) + } + + return + + + -Order the sentence + + {editView === false + ? New sentence + : <>{selectedExercise.index + 1} Sentence + { setSentence(e.target.value) }} />} + + {editView === false + ? New sentence + : <>{selectedExercise.index + 1} Sentence + { setTranslate(e.target.value) }} />} + + + Make sure that all the words are separated by a space + + {editView === false + ? + + + + + + : + + + + } + + + {editView === false && <> + } + + +} +export default OrderSentence + diff --git a/staff/agustin-birman/project/app/src/views/components/SelectActivity/index.css b/staff/agustin-birman/project/app/src/views/components/SelectActivity/index.css new file mode 100644 index 000000000..026d89398 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/SelectActivity/index.css @@ -0,0 +1,11 @@ +.SelectActivity { + margin-bottom: 10px; +} +.SelectActivity Button{ + width: 90%; + height: 15%; + padding: 10px; + border: 1px solid black; + border-radius: 20px; + margin-bottom: 1rem; +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/SelectActivity/index.jsx b/staff/agustin-birman/project/app/src/views/components/SelectActivity/index.jsx new file mode 100644 index 000000000..538e6aa03 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/SelectActivity/index.jsx @@ -0,0 +1,20 @@ +import './index.css' + +import Text from '../../../components/core/Text' +import Button from '../../../components/core/Button' +import View from '../../../components/library/View' +import { Link } from 'react-router-dom' + +//TODO podria hacer un form con 4 border radius para mejorar la experiencia de usuario +function SelectActivity() { + + return + Select the activity + + + + + +} + +export default SelectActivity diff --git a/staff/agustin-birman/project/app/src/views/components/ShareQR/index.css b/staff/agustin-birman/project/app/src/views/components/ShareQR/index.css new file mode 100644 index 000000000..d502c6b63 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ShareQR/index.css @@ -0,0 +1,11 @@ + +.QRContainer { + margin-top: 25%; + display: flex; + flex-direction: column; + align-items: center; +} + +.QR { + margin-bottom: 16px; +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ShareQR/index.jsx b/staff/agustin-birman/project/app/src/views/components/ShareQR/index.jsx new file mode 100644 index 000000000..5886fcf5f --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ShareQR/index.jsx @@ -0,0 +1,25 @@ +import QRCode from 'react-qr-code' +import extractPayloadFromJWT from '../../../utils/extractPayloadFromJWT' +import Text from '../../../components/core/Text' +import Button from '../../../components/core/Button' +import { Link } from 'react-router-dom' +import './index.css' + +function ShareQR() { + const { sub: userId } = extractPayloadFromJWT(localStorage.token) + const url = `${import.meta.env.VITE_APP_URL}/users/${userId}/add` + + return
+ < QRCode + className='QR' + value={url} + size={256} + bgColor="#ffffff" + fgColor="#000000" + /> + Ask to your teacher to scan this code! + +
+} + +export default ShareQR \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ShowExerciseResults/index.css b/staff/agustin-birman/project/app/src/views/components/ShowExerciseResults/index.css new file mode 100644 index 000000000..40f79812d --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ShowExerciseResults/index.css @@ -0,0 +1,25 @@ +.exerciseCorrect{ + background-color: lightgreen; +} + +.exerciseWrong{ + background-color:salmon; +} +.SentenceResult{ + text-align: center; +} +.ResultTitle{ + margin: 20px; +} + +.ResultTable td { + width: 50%; /* Ensures that each column is 50% width */ + +} + +.ResultTable { + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + width: 97%; + border-radius: 10px; + margin-bottom: 16px; +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ShowExerciseResults/index.jsx b/staff/agustin-birman/project/app/src/views/components/ShowExerciseResults/index.jsx new file mode 100644 index 000000000..baac878c3 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ShowExerciseResults/index.jsx @@ -0,0 +1,124 @@ +import { useEffect, useState } from "react" +import logic from "../../../logic" +import { useNavigate, useParams } from "react-router-dom" +import Heading from "../../../components/core/Heading" +import View from "../../../components/library/View" +import './index.css' +import Button from "../../../components/core/Button" + +function ShowExerciseResults() { + const [exercisesWithAnswers, setExercisesWithAnswers] = useState([]) + const [exerciseType, SetExerciseType] = useState('') + const { activityId } = useParams() + + const navigate = useNavigate() + + useEffect(() => { + loadExercisesWithAnswers() + getExerciseType() + }, [activityId]) + + const loadExercisesWithAnswers = () => { + try { + logic.getExercises(activityId) + .then(data => { + const { exercises } = data + const promises = exercises.map(exercise => + logic.getAnswers(exercise.id) + .then(answers => ({ + ...exercise, + answers + }))// TODO hacer una logica que haga todo esto en uno + ); + return Promise.all(promises) + }) + .then(exercisesWithAnswers => setExercisesWithAnswers(exercisesWithAnswers)) + .catch(error => { + console.error(error) + alert(error.message) // TODO: mejorar el alert + }); + } catch (error) { + console.error(error) + alert(error.message) + } + } + + const getExerciseType = () => { + try { + logic.getExerciseType(activityId) + .then(type => SetExerciseType(type)) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const handleDeleteAnswers = () => { + try { + logic.deleteAnswers(activityId) + .then(() => { navigate(`/activities/${activityId}/do-activity/${exerciseType}`) }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + return ( + + Results + {exercisesWithAnswers.map(exercise => ( + + + + + + + + + + + {exerciseType === 'completeSentence' || exerciseType === 'Vocabulary' + ? + + + + : + + + + } + + + {exerciseType === 'completeSentence' || exerciseType === 'Vocabulary' + ? + : null} + + + +
{exercise.index + 1} Sentence
{exercise.sentence}
AnswerYour answer
Your answer
{exercise.answer} + {exerciseType === 'completeSentence' || exerciseType === 'Vocabulary' + ? + exercise.answers.map((answer, index) => ( +
+ {answer.answer} +
+ )) + : exercise.answers.map((answer, index) => ( +
+ {answer.answer} +
))} +
+ ))} + Do you want to try it again? + +
+ ) +} +export default ShowExerciseResults diff --git a/staff/agustin-birman/project/app/src/views/components/ViewActivity/index.css b/staff/agustin-birman/project/app/src/views/components/ViewActivity/index.css new file mode 100644 index 000000000..387734248 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ViewActivity/index.css @@ -0,0 +1,32 @@ +@tailwind components; + +@layer components{ + + .ActivityHeading{ + @apply border-b border-black mb-[8%]; + } + + + .ActivityTitle{ + @apply border-b border-black mb-[3%]; + } + + .ActivityText{ + @apply mb-[4%]; + } + + .ActivityView{ + margin: 10px; + width: 98%; + } + + .CompleteResult{ + @apply mb-[4%] text-green-500; + } + + .IncompleteResult{ + @apply mb-[4%] text-red-600; + } + + +} \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ViewActivity/index.jsx b/staff/agustin-birman/project/app/src/views/components/ViewActivity/index.jsx new file mode 100644 index 000000000..149f60ea0 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ViewActivity/index.jsx @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react' +import logic from '../../../logic' +import './index.css' +import View from '../../../components/library/View' +import useContext from '../../../useContext' +import ViewActivityStudent from '../../../components/library/ViewActivityStudent' +import ViewActivityTeacher from '../../../components/library/ViewActivityTeacher' + +function ViewActivity() { + const [userRole, setUserRole] = useState('') + + const { alert } = useContext() + + useEffect(() => { + getRole() + }, []) + + const getRole = () => { + try { + const role = logic.getUserRole() + setUserRole(role) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + return + {userRole === 'student' && } + {userRole === 'teacher' && } + +} + +export default ViewActivity + diff --git a/staff/agustin-birman/project/app/src/views/components/ViewStudentStats/index.css b/staff/agustin-birman/project/app/src/views/components/ViewStudentStats/index.css new file mode 100644 index 000000000..66c2a9a32 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ViewStudentStats/index.css @@ -0,0 +1,21 @@ + + +.UserStatsText{ + margin: 5%; + } + + .UserStatsButton{ + margin: 8%; + } + + .UserStatsContainer{ + display: flex; + flex-direction: column; + align-items: center; + margin-top: 30%; + margin-bottom: 5%; + width: 90%; + box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.3); + border-radius: 20px; + padding: 15px; + } \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/ViewStudentStats/index.jsx b/staff/agustin-birman/project/app/src/views/components/ViewStudentStats/index.jsx new file mode 100644 index 000000000..c70f17774 --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/ViewStudentStats/index.jsx @@ -0,0 +1,83 @@ +import logic from '../../../logic' +import View from '../../../components/library/View' +import Heading from '../../../components/core/Heading' +import Text from '../../../components/core/Text' +import { Link, useNavigate, useParams } from 'react-router-dom' +import { useEffect, useState } from 'react' +import Button from '../../../components/core/Button' +import './index.css' + + +function ViewStudentStats() { + const [userInfo, setUserInfo] = useState('') + const [userStats, setUserStats] = useState('') + const [userRole, setUserRole] = useState('') + const { userId } = useParams() + + useEffect(() => { + getUserInfo() + getUserStats() + getRole() + }, []) + + const getUserInfo = () => { + try { + logic.getUserInfo(userId) + .then(user => setUserInfo(user)) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const getUserStats = () => { + try { + logic.getUserStats(userId) + .then(stats => setUserStats(stats)) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const getRole = () => { + try { + const role = logic.getUserRole() + setUserRole(role) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + return <> +
+ Student's Information + Name: {userInfo.name} + Surname: {userInfo.surname} + Username: {userInfo.username} + N° Activities: {userStats.countActivities} + N° Exercises: {userStats.countExercises} + N° CorrectExercises: {userStats.countCorrectExercises} +
+ + {userRole === 'teacher' && } + {userRole === 'student' && } + + +} + +export default ViewStudentStats \ No newline at end of file diff --git a/staff/agustin-birman/project/app/src/views/components/Vocabulary/index.jsx b/staff/agustin-birman/project/app/src/views/components/Vocabulary/index.jsx new file mode 100644 index 000000000..95a5a7e9a --- /dev/null +++ b/staff/agustin-birman/project/app/src/views/components/Vocabulary/index.jsx @@ -0,0 +1,123 @@ +// import './index.css' + +import { useEffect, useState } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' + +import Button from '../../../components/core/Button' +import Field from '../../../components/core/Field' +import Heading from '../../../components/core/Heading' +import Text from '../../../components/core/Text' +import FormWithFeedback from '../../../components/library/FormWithFeedback' +import Input from '../../../components/core/Input' + +import logic from '../../../logic' +import View from '../../../components/library/View' +import Exercises from '../../../components/library/Exercises' + + +function Vocabulary() { + const [message, setMessage] = useState('') + const [editView, setEditView] = useState(false) + const [word, setWord] = useState('') + const [answers, setAnswers] = useState('') + const [selectedExercise, setSelectedExercise] = useState(null) + const [updateExercises, setUpdateExercises] = useState(0) + + const { activityId } = useParams() + + + + const handleEditedExercise = () => { + try { + logic.editExercise(selectedExercise.id, { word, answers }) + .then(() => { + setUpdateExercises(prev => prev + 1) + setEditView(false) + }) + .catch(error => { + console.error(error) + + alert(error.message) + }) + } catch (error) { + console.error(error) + + alert(error.message) + } + } + + const handleNextExercise = () => { + const form = document.querySelector('form') + + const word = form.word.value + const answers = form.answers.value + const separatedAnswers = answers.split(' ') + + try { + logic.createVocabulary(activityId, word, separatedAnswers) + .then(() => { + form.reset() + setUpdateExercises(prev => prev + 1) + }) + .catch(error => { + console.error(error) + + setMessage(error.message) + }) + } catch (error) { + console.error(error) + + setMessage(error.message) + } + } + + const handleEditButton = (exercise) => { + if (editView === false) { + const answers = exercise.answer.join(' ') + setAnswers(answers) + } + + setSelectedExercise(exercise) + setWord(exercise.word) + setEditView(editView => !editView) + } + + return + + + -Vocabulary + + {editView === false + ? New word + : <>{selectedExercise.index + 1} Sentence + { setWord(e.target.value) }} />} + + {editView === false + ? Answers + : <>{selectedExercise.index + 1} Sentence + { setAnswers(e.target.value) }} />} + + + Make sure that all the words are separated by a space + + {editView === false + ? + + + + + + : + + + + } + + + {editView === false && <> + } + + +} +export default Vocabulary + diff --git a/staff/agustin-birman/project/app/src/vite.config.js b/staff/agustin-birman/project/app/src/vite.config.js new file mode 100644 index 000000000..5a33944a9 --- /dev/null +++ b/staff/agustin-birman/project/app/src/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/staff/agustin-birman/project/app/tailwind.config.js b/staff/agustin-birman/project/app/tailwind.config.js new file mode 100644 index 000000000..83c4a10fc --- /dev/null +++ b/staff/agustin-birman/project/app/tailwind.config.js @@ -0,0 +1,13 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './src/**/*.{js,jsx,ts,tsx}', // Esto cubre todos los archivos dentro de src + './staff/agustin-birman/app/src/components/**/*.{js,jsx,ts,tsx}', // Para todos los archivos en components + './staff/agustin-birman/app/src/views/**/*.{js,jsx,ts,tsx}', // Para todos los archivos en views + ], + theme: { + extend: {}, + }, + plugins: [], +} + diff --git a/staff/agustin-birman/project/app/vite.config.js b/staff/agustin-birman/project/app/vite.config.js new file mode 100644 index 000000000..5a33944a9 --- /dev/null +++ b/staff/agustin-birman/project/app/vite.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}) diff --git a/staff/agustin-birman/project/com/errors.js b/staff/agustin-birman/project/com/errors.js new file mode 100644 index 000000000..54f045aab --- /dev/null +++ b/staff/agustin-birman/project/com/errors.js @@ -0,0 +1,68 @@ +class ContentError extends Error { + constructor(message) { + super(message) + + this.name = this.constructor.name + } +} + +class MatchError extends Error { + constructor(message) { + super(message) + + this.name = this.constructor.name + } +} + +class DuplicityError extends Error { + constructor(message) { + super(message) + + this.name = this.constructor.name + } +} + +class SystemError extends Error { + constructor(message) { + super(message) + + this.name = this.constructor.name + } +} + +class CredentialsError extends Error { + constructor(message) { + super(message) + + this.name = this.constructor.name + } +} + +class NotFoundError extends Error { + constructor(message) { + super(message) + + this.name = this.constructor.name + } +} + +export { + ContentError, + MatchError, + DuplicityError, + SystemError, + CredentialsError, + NotFoundError +} + +const errors = { + ContentError, + MatchError, + DuplicityError, + SystemError, + CredentialsError, + NotFoundError +} + + +export default errors \ No newline at end of file diff --git a/staff/agustin-birman/project/com/package.json b/staff/agustin-birman/project/com/package.json new file mode 100644 index 000000000..764708be7 --- /dev/null +++ b/staff/agustin-birman/project/com/package.json @@ -0,0 +1,13 @@ +{ + "name": "com", + "version": "1.0.0", + "description": "", + "type": "module", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC" +} \ No newline at end of file diff --git a/staff/agustin-birman/project/com/validate.js b/staff/agustin-birman/project/com/validate.js new file mode 100644 index 000000000..8c66833e7 --- /dev/null +++ b/staff/agustin-birman/project/com/validate.js @@ -0,0 +1,80 @@ +import { ContentError, MatchError } from './errors.js' + +const NAME_REGEX = /^[a-zA-Z=\[\]\{\}\<\>\(\)]{1,}$/ +const USERNAME_REGEX = /^[\w-]+$/ +const PASSWORD_REGEX = /^[\w-$%&=\[\]\{\}\<\>\(\)]{8,}$/ +const EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@']+(\.[^<>()[\]\\.,;:\s@']+)*)|.('.+'))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ +const ID_REGEX = /^[0-9a-z]+$/ +const TEXT_COMPLETE_SENTENCE = /\((?:[A-Za-zäöüß]+)\)/ + + +function validateName(name, explain = 'name') { + if (typeof name !== 'string' || !NAME_REGEX.test(name)) + throw new ContentError(`${explain} is not valid`) +} + +function validateUsername(username, explain = 'username') { + if (typeof username !== 'string' || !USERNAME_REGEX.test(username)) + throw new ContentError(`${explain} is not valid`) +} + +function validatePassword(password) { + if (typeof password !== 'string' || !PASSWORD_REGEX.test(password)) + throw new ContentError('password is not valid') +} + +function validatePasswordsMatch(password, passwordRepeat) { + if (password !== passwordRepeat) + throw new MatchError('passwords don\'t match') +} + +function validateEmail(email) { + if (typeof email !== 'string' || !EMAIL_REGEX.test(email)) + throw new ContentError('email is not valid') +} + +function validateCallback(callback) { + if (typeof callback !== 'function') + throw new TypeError('callback is not a function') +} + +function validateText(text, explain = 'text', maxLength = Infinity) { + if (typeof text !== 'string' || !text.length || text.length > maxLength) + throw new ContentError(`${explain} is not valid`) +} + +function validateTextCompleteSentence(text, explain = 'text', maxLength = Infinity) { + if (typeof text !== 'string' || !text.length || text.length > maxLength || !TEXT_COMPLETE_SENTENCE.test(text)) + throw new ContentError(`${explain} is not valid`) +} + +function validateUrl(url, explain = 'url') { + if (typeof url !== 'string' || !url.startsWith('http')) + throw new ContentError(`${explain} is not valid`) +} + +function validateId(id, explain = 'id') { + if (typeof id !== 'string' || !ID_REGEX.test(id)) + throw new ContentError(`${explain} is not valid`) +} + +function validateUserType(userType) { + if (userType !== 'teacher' && userType !== 'student') + throw new ContentError('userType is not valid') +} + +const validate = { + name: validateName, + username: validateUsername, + password: validatePassword, + passowrdsMatch: validatePasswordsMatch, + email: validateEmail, + callback: validateCallback, + text: validateText, + url: validateUrl, + id: validateId, + userType: validateUserType, + textCompleteSentence: validateTextCompleteSentence +} + +export default validate \ No newline at end of file diff --git a/staff/agustin-birman/project/doc/README.md b/staff/agustin-birman/project/doc/README.md new file mode 100644 index 000000000..938299e15 --- /dev/null +++ b/staff/agustin-birman/project/doc/README.md @@ -0,0 +1,66 @@ +# Cool Steps + +An app for renting and lending ladders. + +![](https://media.giphy.com/media/m9pvbkBJzOY9Mt0dSm/giphy.gif?cid=790b761118teuaz0ojtj0vsytuoevmgff91t460gpic3jk80&ep=v1_gifs_search&rid=giphy.gif&ct=g) + +## Functional + +### Use Cases + +Student + +-share id with teacher (view QR id) //done +-list activities //done +-view activity //done +-submit exercise answer // done +-delete progress //done +-view stats + +Teacher + +-add student (scan QR id) //done +-list students //done +-list activities //done +-add activity //done +-edit activity //done +-delete activity //done +-add exercise to activity //done +-remove exercise from activity //done +-edit exercise from activity //done + +### UI Design + +[Figma](https://www.figma.com/design/FtmTtX9cZewWlv6yqsj4nu/demo-app?node-id=0-1&t=tNho9NZQl4l4RETJ-0) + +## Technical + +### Data Model + +User +- id (auto) +- name (string) +- surname (string) +- email (string) +- password (string) +- country (string, optional) +- role (string, enum: teacher| student) + +Activity +-id (auto) +-teacher (User.id) +-title (string) +-description (string, optional) + +Exercise +-id (auto) +-activity (Activity.id) +-sentence (string) +-answer (string) + +Answer +-id (auto) +-student (User.id) +-activity (Activity.id) +-exercise (Exercise.id) +-answer (string) \ No newline at end of file