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
+ {children}
+
+
+}
+
+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
+ {children}
+
+
+}
+
+export default FieldWithTextArea
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/Form/index.css b/staff/agustin-birman/project/app/src/components/core/Form/index.css
new file mode 100644
index 000000000..379cad316
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Form/index.css
@@ -0,0 +1,5 @@
+.Form {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/Form/index.jsx b/staff/agustin-birman/project/app/src/components/core/Form/index.jsx
new file mode 100644
index 000000000..b76b1ce99
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Form/index.jsx
@@ -0,0 +1,7 @@
+import './index.css'
+
+function Form({ className, onSubmit, children }) {
+ return
+}
+
+export default Form
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/Heading/index.css b/staff/agustin-birman/project/app/src/components/core/Heading/index.css
new file mode 100644
index 000000000..22df29383
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Heading/index.css
@@ -0,0 +1,5 @@
+@tailwind components;
+
+@layer components{
+
+}
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/Heading/index.jsx b/staff/agustin-birman/project/app/src/components/core/Heading/index.jsx
new file mode 100644
index 000000000..1e8905a3c
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Heading/index.jsx
@@ -0,0 +1,8 @@
+import './index.css'
+function Heading({ level, children, className }) {
+ const Tag = `h${level}`
+
+ return {children}
+}
+
+export default Heading
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/Image/index.css b/staff/agustin-birman/project/app/src/components/core/Image/index.css
new file mode 100644
index 000000000..d1abcb0ea
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Image/index.css
@@ -0,0 +1,3 @@
+.Image {
+ width: 100%;
+}
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/Image/index.jsx b/staff/agustin-birman/project/app/src/components/core/Image/index.jsx
new file mode 100644
index 000000000..6df9c1b78
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Image/index.jsx
@@ -0,0 +1,7 @@
+import './index.css'
+
+function Image({ src }) {
+ return
+}
+
+export default Image
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/Input/index.css b/staff/agustin-birman/project/app/src/components/core/Input/index.css
new file mode 100644
index 000000000..5d7caacef
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Input/index.css
@@ -0,0 +1,10 @@
+.Input {
+ padding: .4rem;
+ border-top: 0;
+ border-left: 0;
+ border-right: 0;
+ border-bottom: 1px solid black;
+ padding-left: 0.8rem;
+ margin: 1rem;
+}
+
diff --git a/staff/agustin-birman/project/app/src/components/core/Input/index.jsx b/staff/agustin-birman/project/app/src/components/core/Input/index.jsx
new file mode 100644
index 000000000..a02663402
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Input/index.jsx
@@ -0,0 +1,16 @@
+import './index.css'
+
+function Input({ id, type, name, placeholder, value, checked, onChange, className, required }) {
+ return
+}
+
+export default Input
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/Label/index.css b/staff/agustin-birman/project/app/src/components/core/Label/index.css
new file mode 100644
index 000000000..1eea60374
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Label/index.css
@@ -0,0 +1,7 @@
+.Input {
+ padding: .4rem;
+ background-color: transparent;
+ font-size: 1rem;
+ border-radius: 20px;
+ border: 1px solid var(--first-color);
+}
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/Label/index.jsx b/staff/agustin-birman/project/app/src/components/core/Label/index.jsx
new file mode 100644
index 000000000..f404aaf93
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Label/index.jsx
@@ -0,0 +1,5 @@
+function Label({ htmlFor, children }) {
+ return {children}
+}
+
+export default Label
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/Link/index.jsx b/staff/agustin-birman/project/app/src/components/core/Link/index.jsx
new file mode 100644
index 000000000..857ac7cd3
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Link/index.jsx
@@ -0,0 +1,5 @@
+function Link({ children, onClick }) {
+ return {children}
+}
+
+export default Link
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/SubmitButton/index.css b/staff/agustin-birman/project/app/src/components/core/SubmitButton/index.css
new file mode 100644
index 000000000..e69de29bb
diff --git a/staff/agustin-birman/project/app/src/components/core/SubmitButton/index.jsx b/staff/agustin-birman/project/app/src/components/core/SubmitButton/index.jsx
new file mode 100644
index 000000000..d0eb7e7d5
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/SubmitButton/index.jsx
@@ -0,0 +1,8 @@
+import Button from '../Button'
+
+
+function SubmitButton({ children }) {
+ return {children}
+}
+
+export default SubmitButton
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/Text/index.css b/staff/agustin-birman/project/app/src/components/core/Text/index.css
new file mode 100644
index 000000000..22df29383
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Text/index.css
@@ -0,0 +1,5 @@
+@tailwind components;
+
+@layer components{
+
+}
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/Text/index.jsx b/staff/agustin-birman/project/app/src/components/core/Text/index.jsx
new file mode 100644
index 000000000..86dff39d8
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/Text/index.jsx
@@ -0,0 +1,8 @@
+import './index.css'
+
+function Text({ children, className }) {
+ return {children}
+
+}
+
+export default Text
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/TextArea/index.css b/staff/agustin-birman/project/app/src/components/core/TextArea/index.css
new file mode 100644
index 000000000..f0cb5a3b7
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/TextArea/index.css
@@ -0,0 +1,10 @@
+.TextArea {
+ height: 90px;
+ width: 300px;
+ background-color: transparent;
+ font-size: 1rem;
+ border-bottom: 1px solid black;
+ border-radius: 20px;
+ padding: 12px;
+ margin: 15px
+}
\ No newline at end of file
diff --git a/staff/agustin-birman/project/app/src/components/core/TextArea/index.jsx b/staff/agustin-birman/project/app/src/components/core/TextArea/index.jsx
new file mode 100644
index 000000000..463fb087a
--- /dev/null
+++ b/staff/agustin-birman/project/app/src/components/core/TextArea/index.jsx
@@ -0,0 +1,7 @@
+import './index.css'
+
+function TextArea({ id, type, placeholder }) {
+ 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 {formattedTime}
+}
+
+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' }) =>
\ 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}
+
+ Cancel
+ Confirm
+
+
+}
+
+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
+
+
+
+ N°
+ Sentence
+ Edit
+ Delete
+
+
+
+ {exercises.map(exercise =>
+
+ {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
+}
+
+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
+}
+
+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 {children}
+}
+
+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 }
+
+ Start Activity
+ {completedActivity === true && <>
+ View Results
+ Restart Activity
+ >}
+ Go Back
+
+ {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
+ ? <>
+
+
+
+
+
+ List Exercises
+ Go Back
+
+ >
+ :
+ Cancel Edit
+ Save Changes
+
+ }
+ {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
+
+
+
+
+ } />
+ } />
+
+ } />
+
+ } />
+ } />
+ } />
+ } />
+
+ } />
+ } />
+
+ } />
+ } />
+
+ } />
+ } />
+ } />
+
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+}
+
+
+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
+ View>
+}
+
+
+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}
+
+ Add Student
+ >
+}
+
+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
+ ?
+
+ Cancel
+ Add a new exercise
+
+
+ :
+ Cancel Edit
+ Save Changes
+
+ }
+
+
+ {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}
+ Continue
+
+}
+
+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
+
+
+ Cancel
+ Created
+
+
+}
+
+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
+ ? handleSubmittedAnswer(exercise.id)}>Finish
+ : handleSubmittedAnswer(exercise.id)}>Next Exercise
+
+ }
+ {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) => (
+ handleSelectedWordClick(word)}
+ >
+ {word}
+
+ ))}
+
+
+ {mixedWords.map((word, wordIndex) => (
+ { handleWordClick(word), console.log(word) }}>
+ {word}
+
+ ))}
+
+
+ {
+ currentPage > totalPages
+ ? handleSubmittedAnswer(exercise.id)}>Finish
+ : handleSubmittedAnswer(exercise.id)}>Next Exercise
+ }
+ {message}
+
+ Page {currentPage} of {Math.ceil(exercises.length / pageSize)}
+ View>
+ ))}
+ View>
+ )
+}
+
+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
+
+
+
+
+ Title
+ Info
+
+
+
+ {activities.map(activity =>
+
+ {activity.title}
+
+
+
+
+ )}
+
+
+
+ Go Back
+
+}
+
+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
+
+
+
+
+ N°
+ Sentence
+
+
+
+ {exercises.map(exercise =>
+
+ {exercise.index + 1}
+ {exercise.word !== undefined ? exercise.word : exercise.sentence}
+
+
+ )}
+
+
+
+ Add a new exercise or edit
+ Go back
+
+}
+
+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
+
+
+
+
+ Name
+ Surname
+ Info
+ Delete
+
+
+
+ {students.map(student =>
+
+ {student.name}
+ {student.surname}
+ handleUserStats(student.id)}
+ style={{ cursor: 'pointer', color: '#007bff' }}
+ >
+
+
+ {confirmDeleteStudent && handleDeleteStudent(student.id)} onCancel={toggleDeleteStudent}> }
+
+
+ )}
+
+
+
+ Go Home
+
+}
+
+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
+
+
+
+
+ Name
+ Surname
+ Delete
+
+
+
+ {teachers.map(teacher =>
+
+
+ {teacher.name}
+ {teacher.surname}
+
+
+ {confirmDeleteTeacher && handleDeleteTeacher(teacher.id)} onCancel={toggleDeleteTeacher}> }
+
+
+ )}
+
+
+
+ Go Home
+
+
+}
+
+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)}
+ />
+
+
+
+
+
+ Title
+ Teacher
+ Info
+
+
+
+ {filteredActivities.length > 0 ? (
+ filteredActivities.map(activity => (
+
+ {activity.title}
+ {activity.teacherUsername}
+
+
+
+
+ ))
+ ) : (
+
+ No activities found
+
+ )}
+
+
+
+ Go Back
+
+}
+
+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' && <>
+ View activities
+ View teachers
+ Share ID
+ View Stats
+ asd
+ >}
+
+ {userRole === 'teacher' && <>
+ Create a activity
+ View yours activities
+ View students
+ >}
+
+
+}
+
+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
+ ?
+
+ Cancel
+ Add a new exercise
+
+
+ :
+ Cancel Edit
+ Save Changes
+
+ }
+
+
+ {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
+ Complete the sentence
+ Order the sentence
+ Vocabulary
+ Back to home
+
+}
+
+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!
+ Go Home
+
+}
+
+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 => (
+
+
+
+ {exercise.index + 1} Sentence
+
+
+
+
+ {exercise.sentence}
+
+ {exerciseType === 'completeSentence' || exerciseType === 'Vocabulary'
+ ?
+ Answer
+ Your answer
+
+ :
+
+ Your answer
+
+ }
+
+
+ {exerciseType === 'completeSentence' || exerciseType === 'Vocabulary'
+ ? {exercise.answer}
+ : null}
+
+ {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?
+ YES
+
+ )
+}
+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' && Go Back }
+ {userRole === 'student' && Go Back }
+
+ >
+}
+
+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
+ ?
+
+ Cancel
+ Add a new exercise
+
+
+ :
+ Cancel Edit
+ Save Changes
+
+ }
+
+
+ {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